aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2004-11-24 01:04:44 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2004-11-24 01:04:44 +0000
commitdb045dbbf60b53dbe013ef25554fd013baf88134 (patch)
tree257830e3c76458c8ff3d1329de83f32b23926028 /activerecord
downloadrails-db045dbbf60b53dbe013ef25554fd013baf88134.tar.gz
rails-db045dbbf60b53dbe013ef25554fd013baf88134.tar.bz2
rails-db045dbbf60b53dbe013ef25554fd013baf88134.zip
Initial
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG757
-rw-r--r--activerecord/MIT-LICENSE20
-rwxr-xr-xactiverecord/README361
-rw-r--r--activerecord/RUNNING_UNIT_TESTS36
-rwxr-xr-xactiverecord/Rakefile126
-rw-r--r--activerecord/benchmarks/benchmark.rb26
-rw-r--r--activerecord/benchmarks/mysql_benchmark.rb19
-rw-r--r--activerecord/dev-utils/eval_debugger.rb14
-rw-r--r--activerecord/examples/associations.pngbin0 -> 40623 bytes
-rw-r--r--activerecord/examples/associations.rb87
-rw-r--r--activerecord/examples/shared_setup.rb15
-rw-r--r--activerecord/examples/validation.rb88
-rw-r--r--activerecord/install.rb60
-rwxr-xr-xactiverecord/lib/active_record.rb50
-rw-r--r--activerecord/lib/active_record/aggregations.rb165
-rwxr-xr-xactiverecord/lib/active_record/associations.rb576
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb129
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb107
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb102
-rwxr-xr-xactiverecord/lib/active_record/base.rb1051
-rwxr-xr-xactiverecord/lib/active_record/callbacks.rb337
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb371
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/mysql_adapter.rb131
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb170
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb105
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb298
-rw-r--r--activerecord/lib/active_record/deprecated_associations.rb70
-rwxr-xr-xactiverecord/lib/active_record/fixtures.rb208
-rw-r--r--activerecord/lib/active_record/observer.rb71
-rw-r--r--activerecord/lib/active_record/reflection.rb126
-rw-r--r--activerecord/lib/active_record/support/class_attribute_accessors.rb43
-rw-r--r--activerecord/lib/active_record/support/class_inheritable_attributes.rb37
-rw-r--r--activerecord/lib/active_record/support/clean_logger.rb10
-rw-r--r--activerecord/lib/active_record/support/inflector.rb78
-rw-r--r--activerecord/lib/active_record/transactions.rb119
-rwxr-xr-xactiverecord/lib/active_record/validations.rb205
-rw-r--r--activerecord/lib/active_record/vendor/mysql.rb1117
-rw-r--r--activerecord/lib/active_record/vendor/simple.rb702
-rw-r--r--activerecord/lib/active_record/wrappers/yaml_wrapper.rb15
-rw-r--r--activerecord/lib/active_record/wrappings.rb59
-rwxr-xr-xactiverecord/test/abstract_unit.rb22
-rw-r--r--activerecord/test/aggregations_test.rb34
-rwxr-xr-xactiverecord/test/all.sh8
-rwxr-xr-xactiverecord/test/associations_test.rb549
-rwxr-xr-xactiverecord/test/base_test.rb544
-rw-r--r--activerecord/test/class_inheritable_attributes_test.rb33
-rw-r--r--activerecord/test/connections/native_mysql/connection.rb24
-rw-r--r--activerecord/test/connections/native_postgresql/connection.rb24
-rw-r--r--activerecord/test/connections/native_sqlite/connection.rb34
-rw-r--r--activerecord/test/connections/native_sqlserver/connection.rb15
-rwxr-xr-xactiverecord/test/deprecated_associations_test.rb335
-rwxr-xr-xactiverecord/test/finder_test.rb67
-rw-r--r--activerecord/test/fixtures/accounts.yml8
-rw-r--r--activerecord/test/fixtures/auto_id.rb4
-rw-r--r--activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char1
-rw-r--r--activerecord/test/fixtures/bad_fixtures/attr_with_spaces1
-rw-r--r--activerecord/test/fixtures/bad_fixtures/blank_line3
-rw-r--r--activerecord/test/fixtures/bad_fixtures/duplicate_attributes3
-rw-r--r--activerecord/test/fixtures/bad_fixtures/missing_value1
-rw-r--r--activerecord/test/fixtures/column_name.rb3
-rwxr-xr-xactiverecord/test/fixtures/companies/first_client6
-rwxr-xr-xactiverecord/test/fixtures/companies/first_firm4
-rwxr-xr-xactiverecord/test/fixtures/companies/second_client6
-rwxr-xr-xactiverecord/test/fixtures/company.rb37
-rw-r--r--activerecord/test/fixtures/company_in_module.rb47
-rw-r--r--activerecord/test/fixtures/course.rb3
-rw-r--r--activerecord/test/fixtures/courses/java2
-rw-r--r--activerecord/test/fixtures/courses/ruby2
-rw-r--r--activerecord/test/fixtures/customer.rb30
-rw-r--r--activerecord/test/fixtures/customers/david6
-rwxr-xr-xactiverecord/test/fixtures/db_definitions/mysql.sql97
-rw-r--r--activerecord/test/fixtures/db_definitions/mysql2.sql4
-rw-r--r--activerecord/test/fixtures/db_definitions/postgresql.sql114
-rw-r--r--activerecord/test/fixtures/db_definitions/postgresql2.sql4
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlite.sql86
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlite2.sql4
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlserver.sql96
-rw-r--r--activerecord/test/fixtures/db_definitions/sqlserver2.sql4
-rw-r--r--activerecord/test/fixtures/default.rb2
-rw-r--r--activerecord/test/fixtures/developer.rb8
-rw-r--r--activerecord/test/fixtures/developers.yml13
-rw-r--r--activerecord/test/fixtures/developers_projects/david_action_controller3
-rw-r--r--activerecord/test/fixtures/developers_projects/david_active_record3
-rw-r--r--activerecord/test/fixtures/developers_projects/jamis_active_record2
-rw-r--r--activerecord/test/fixtures/entrant.rb3
-rw-r--r--activerecord/test/fixtures/entrants/first3
-rw-r--r--activerecord/test/fixtures/entrants/second3
-rw-r--r--activerecord/test/fixtures/entrants/third3
-rw-r--r--activerecord/test/fixtures/movie.rb5
-rw-r--r--activerecord/test/fixtures/movies/first2
-rw-r--r--activerecord/test/fixtures/movies/second2
-rw-r--r--activerecord/test/fixtures/project.rb4
-rw-r--r--activerecord/test/fixtures/projects/action_controller2
-rw-r--r--activerecord/test/fixtures/projects/active_record2
-rwxr-xr-xactiverecord/test/fixtures/reply.rb21
-rw-r--r--activerecord/test/fixtures/subscriber.rb5
-rw-r--r--activerecord/test/fixtures/subscribers/first2
-rw-r--r--activerecord/test/fixtures/subscribers/second2
-rwxr-xr-xactiverecord/test/fixtures/topic.rb20
-rwxr-xr-xactiverecord/test/fixtures/topics/first9
-rwxr-xr-xactiverecord/test/fixtures/topics/second8
-rwxr-xr-xactiverecord/test/fixtures_test.rb84
-rw-r--r--activerecord/test/inflector_test.rb121
-rwxr-xr-xactiverecord/test/inheritance_test.rb125
-rwxr-xr-xactiverecord/test/lifecycle_test.rb110
-rw-r--r--activerecord/test/modules_test.rb29
-rw-r--r--activerecord/test/multiple_db_test.rb46
-rw-r--r--activerecord/test/pk_test.rb59
-rw-r--r--activerecord/test/reflection_test.rb78
-rw-r--r--activerecord/test/thread_safety_test.rb33
-rw-r--r--activerecord/test/transactions_test.rb110
-rwxr-xr-xactiverecord/test/unconnected_test.rb24
-rwxr-xr-xactiverecord/test/validations_test.rb126
113 files changed, 11398 insertions, 0 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
new file mode 100644
index 0000000000..538acf172e
--- /dev/null
+++ b/activerecord/CHANGELOG
@@ -0,0 +1,757 @@
+*CVS*
+
+* Added ADO-based SQLServerAdapter (only works on Windows) [Joey Gibson]
+
+* Fixed problems with primary keys and postgresql sequences (#230) [Tim Bates]
+
+* Fixed problems with nested transactions (#231) [Tim Bates]
+
+* Added reloading for associations under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development.
+ This is turned on by default, but can be turned off with ActiveRecord::Base.reload_dependencies = false in production environments.
+
+ NOTE: This will only have an effect if you let the associations manage the requiring of model classes. All libraries loaded through
+ require will be "forever" cached. You can, however, use ActiveRecord::Base.load_or_require("library") to get this behavior outside of the
+ auto-loading associations.
+
+* Added ERB capabilities to the fixture files for dynamic fixture generation. You don't need to do anything, just include ERB blocks like:
+
+ david:
+ id: 1
+ name: David
+
+ jamis:
+ id: 2
+ name: Jamis
+
+ <% for digit in 3..10 %>
+ dev_<%= digit %>:
+ id: <%= digit %>
+ name: fixture_<%= digit %>
+ <% end %>
+
+* Changed the yaml fixture searcher to look in the root of the fixtures directory, so when you before could have something like:
+
+ fixtures/developers/fixtures.yaml
+ fixtures/accounts/fixtures.yaml
+
+ ...you now need to do:
+
+ fixtures/developers.yaml
+ fixtures/accounts.yaml
+
+* Changed the fixture format from:
+
+ name: david
+ data:
+ id: 1
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
+ ---
+ name: steve
+ data:
+ id: 2
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
+
+ ...to:
+
+ david:
+ id: 1
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
+
+ steve:
+ id: 2
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
+
+ The change is NOT backwards compatible. Fixtures written in the old YAML style needs to be rewritten!
+
+* All associations will now attempt to require the classes that they associate to. Relieving the need for most explicit 'require' statements.
+
+*1.1.0* (34)
+
+* Added automatic fixture setup and instance variable availability. Fixtures can also be automatically
+ instantiated in instance variables relating to their names using the following style:
+
+ class FixturesTest < Test::Unit::TestCase
+ fixtures :developers # you can add more with comma separation
+
+ def test_developers
+ assert_equal 3, @developers.size # the container for all the fixtures is automatically set
+ assert_kind_of Developer, @david # works like @developers["david"].find
+ assert_equal "David Heinemeier Hansson", @david.name
+ end
+ end
+
+* Added HasAndBelongsToManyAssociation#push_with_attributes(object, join_attributes) that can create associations in the join table with additional
+ attributes. This is really useful when you have information that's only relevant to the join itself, such as a "added_on" column for an association
+ between post and category. The added attributes will automatically be injected into objects retrieved through the association similar to the piggy-back
+ approach:
+
+ post.categories.push_with_attributes(category, :added_on => Date.today)
+ post.categories.first.added_on # => Date.today
+
+ NOTE: The categories table doesn't have a added_on column, it's the categories_post join table that does!
+
+* Fixed that :exclusively_dependent and :dependent can't be activated at the same time on has_many associations [bitsweat]
+
+* Fixed that database passwords couldn't be all numeric [bitsweat]
+
+* Fixed that calling id would create the instance variable for new_records preventing them from being saved correctly [bitsweat]
+
+* Added sanitization feature to HasManyAssociation#find_all so it works just like Base.find_all [Sam Stephenson/bitsweat]
+
+* Added that you can pass overlapping ids to find without getting duplicated records back [bitsweat]
+
+* Added that Base.benchmark returns the result of the block [bitsweat]
+
+* Fixed problem with unit tests on Windows with SQLite [paterno]
+
+* Fixed that quotes would break regular non-yaml fixtures [Dmitry Sabanin/daft]
+
+* Fixed fixtures on windows with line endings cause problems under unix / mac [Tobias Luetke]
+
+* Added HasAndBelongsToManyAssociation#find(id) that'll search inside the collection and find the object or record with that id
+
+* Added :conditions option to has_and_belongs_to_many that works just like the one on all the other associations
+
+* Added AssociationCollection#clear to remove all associations from has_many and has_and_belongs_to_many associations without destroying the records [geech]
+
+* Added type-checking and remove in 1-instead-of-N sql statements to AssociationCollection#delete [geech]
+
+* Added a return of self to AssociationCollection#<< so appending can be chained, like project << Milestone.create << Milestone.create [geech]
+
+* Added Base#hash and Base#eql? which means that all of the equality using features of array and other containers now works:
+
+ [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
+
+* Added :uniq as an option to has_and_belongs_to_many which will automatically ensure that AssociateCollection#uniq is called
+ before pulling records out of the association. This is especially useful for three-way (and above) has_and_belongs_to_many associations.
+
+* Added AssociateCollection#uniq which is especially useful for has_and_belongs_to_many associations that can include duplicates,
+ which is common on associations that also use metadata. Usage: post.categories.uniq
+
+* Fixed respond_to? to use a subclass specific hash instead of an Active Record-wide one
+
+* Fixed has_and_belongs_to_many to treat associations between classes in modules properly [Florian Weber]
+
+* Added a NoMethod exception to be raised when query and writer methods are called for attributes that doesn't exist [geech]
+
+* Added a more robust version of Fixtures that throws meaningful errors when on formatting issues [geech]
+
+* Added Base#transaction as a compliment to Base.transaction for prettier use in instance methods [geech]
+
+* Improved the speed of respond_to? by placing the dynamic methods lookup table in a hash [geech]
+
+* Added that any additional fields added to the join table in a has_and_belongs_to_many association
+ will be placed as attributes when pulling records out through has_and_belongs_to_many associations.
+ This is helpful when have information about the association itself that you want available on retrival.
+
+* Added better loading exception catching and RubyGems retries to the database adapters [alexeyv]
+
+* Fixed bug with per-model transactions [daniel]
+
+* Fixed Base#transaction so that it returns the result of the last expression in the transaction block [alexeyv]
+
+* Added Fixture#find to find the record corresponding to the fixture id. The record
+ class name is guessed by using Inflector#classify (also new) on the fixture directory name.
+
+ Before: Document.find(@documents["first"]["id"])
+ After : @documents["first"].find
+
+* Fixed that the table name part of column names ("TABLE.COLUMN") wasn't removed properly [Andreas Schwarz]
+
+* Fixed a bug with Base#size when a finder_sql was used that didn't capitalize SELECT and FROM [geech]
+
+* Fixed quoting problems on SQLite by adding quote_string to the AbstractAdapter that can be overwritten by the concrete
+ adapters for a call to the dbm. [Andreas Schwarz]
+
+* Removed RubyGems backup strategy for requiring SQLite-adapter -- if people want to use gems, they're already doing it with AR.
+
+
+*1.0.0 (35)*
+
+* Added OO-style associations methods [Florian Weber]. Examples:
+
+ Project#milestones_count => Project#milestones.size
+ Project#build_to_milestones => Project#milestones.build
+ Project#create_for_milestones => Project#milestones.create
+ Project#find_in_milestones => Project#milestones.find
+ Project#find_all_in_milestones => Project#milestones.find_all
+
+* Added serialize as a new class method to control when text attributes should be YAMLized or not. This means that automated
+ serialization of hashes, arrays, and so on WILL NO LONGER HAPPEN (#10). You need to do something like this:
+
+ class User < ActiveRecord::Base
+ serialize :settings
+ end
+
+ This will assume that settings is a text column and will now YAMLize any object put in that attribute. You can also specify
+ an optional :class_name option that'll raise an exception if a serialized object is retrieved as a descendent of a class not in
+ the hierarchy. Example:
+
+ class User < ActiveRecord::Base
+ serialize :settings, :class_name => "Hash"
+ end
+
+ user = User.create("settings" => %w( one two three ))
+ User.find(user.id).settings # => raises SerializationTypeMismatch
+
+* Added the option to connect to a different database for one model at a time. Just call establish_connection on the class
+ you want to have connected to another database than Base. This will automatically also connect decendents of that class
+ to the different database [Renald Buter].
+
+* Added transactional protection for Base#save. Validations can now check for values knowing that it happens in a transaction and callbacks
+ can raise exceptions knowing that the save will be rolled back. [Suggested by Alexey Verkhovsky]
+
+* Added column name quoting so reserved words, such as "references", can be used as column names [Ryan Platte]
+
+* Added the possibility to chain the return of what happened inside a logged block [geech]:
+
+ This now works:
+ log { ... }.map { ... }
+
+ Instead of doing:
+ result = []
+ log { result = ... }
+ result.map { ... }
+
+* Added "socket" option for the MySQL adapter, so you can change it to something else than "/tmp/mysql.sock" [Anna Lissa Cruz]
+
+* Added respond_to? answers for all the attribute methods. So if Person has a name attribute retrieved from the table schema,
+ person.respond_to? "name" will return true.
+
+* Added Base.benchmark which can be used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block.
+ Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all):
+
+ Project.benchmark("Creating project") do
+ project = Project.create("name" => "stuff")
+ project.create_manager("name" => "David")
+ project.milestones << Milestone.find_all
+ end
+
+* Added logging of invalid SQL statements [Suggested by Daniel Von Fange]
+
+* Added alias Errors#[] for Errors#on, so you can now say person.errors["name"] to retrieve the errors for name [Andreas Schwarz]
+
+* Added RubyGems require attempt if sqlite-ruby is not available through regular methods.
+
+* Added compatibility with 2.x series of sqlite-ruby drivers. [Jamis Buck]
+
+* Added type safety for association assignments, so a ActiveRecord::AssociationTypeMismatch will be raised if you attempt to
+ assign an object that's not of the associated class. This cures the problem with nil giving id = 4 and fixnums giving id = 1 on
+ mistaken association assignments. [Reported by Andreas Schwarz]
+
+* Added the option to keep many fixtures in one single YAML document [what-a-day]
+
+* Added the class method "inheritance_column" that can be overwritten to return the name of an alternative column than "type" for storing
+ the type for inheritance hierarchies. [Dave Steinberg]
+
+* Added [] and []= as an alternative way to access attributes when the regular methods have been overwritten [Dave Steinberg]
+
+* Added the option to observer more than one class at the time by specifying observed_class as an array
+
+* Added auto-id propagation support for tables with arbitrary primary keys that have autogenerated sequences associated with them
+ on PostgreSQL. [Dave Steinberg]
+
+* Changed that integer and floats set to "" through attributes= remain as NULL. This was especially a problem for scaffolding and postgresql. (#49)
+
+* Changed the MySQL Adapter to rely on MySQL for its defaults for socket, host, and port [Andreas Schwarz]
+
+* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue.
+
+* Changed class inheritable attributes to not use eval [Caio Chassot]
+
+* Changed Errors#add to now use "invalid" as the default message instead of true, which means full_messages work with those [Marcel Molina Jr]
+
+* Fixed spelling on Base#add_on_boundry_breaking to Base#add_on_boundary_breaking (old naming still works) [Marcel Molina Jr.]
+
+* Fixed that entries in the has_and_belongs_to_many join table didn't get removed when an associated object was destroyed.
+
+* Fixed unnecessary calls to SET AUTOCOMMIT=0/1 for MySQL adapter [Andreas Schwarz]
+
+* Fixed PostgreSQL defaults are now handled gracefully [Dave Steinberg]
+
+* Fixed increment/decrement_counter are now atomic updates [Andreas Schwarz]
+
+* Fixed the problems the Inflector had turning Attachment into attuchments and Cases into Casis [radsaq/Florian Gross]
+
+* Fixed that cloned records would point attribute references on the parent object [Andreas Schwarz]
+
+* Fixed SQL for type call on inheritance hierarchies [Caio Chassot]
+
+* Fixed bug with typed inheritance [Florian Weber]
+
+* Fixed a bug where has_many collection_count wouldn't use the conditions specified for that association
+
+
+*0.9.5*
+
+* Expanded the table_name guessing rules immensely [Florian Green]. Documentation:
+
+ Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
+ directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
+ to guess the table name from even when called on Reply. The guessing rules are as follows:
+ * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table.
+ * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies",
+ so a Category class becomes a categories table.
+ * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table.
+ * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table.
+ * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table.
+ * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table.
+ * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table.
+ * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table.
+ * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table.
+ * Class name ends in an "s": No additional characters are added or removed.
+ * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table.
+ * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table.
+ Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended.
+ So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts".
+
+ You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a
+ "mice" table. Example:
+
+ class Mouse < ActiveRecord::Base
+ def self.table_name() "mice" end
+ end
+
+ This conversion is now done through an external class called Inflector residing in lib/active_record/support/inflector.rb.
+
+* Added find_all_in_collection to has_many defined collections. Works like this:
+
+ class Firm < ActiveRecord::Base
+ has_many :clients
+ end
+
+ firm.id # => 1
+ firm.find_all_in_clients "revenue > 1000" # SELECT * FROM clients WHERE firm_id = 1 AND revenue > 1000
+
+ [Requested by Dave Thomas]
+
+* Fixed finders for inheritance hierarchies deeper than one level [Florian Weber]
+
+* Added add_on_boundry_breaking to errors to accompany add_on_empty as a default validation method. It's used like this:
+
+ class Person < ActiveRecord::Base
+ protected
+ def validation
+ errors.add_on_boundry_breaking "password", 3..20
+ end
+ end
+
+ This will add an error to the tune of "is too short (min is 3 characters)" or "is too long (min is 20 characters)" if
+ the password is outside the boundry. The messages can be changed by passing a third and forth parameter as message strings.
+
+* Implemented a clone method that works properly with AR. It returns a clone of the record that
+ hasn't been assigned an id yet and is treated as a new record.
+
+* Allow for domain sockets in PostgreSQL by not assuming localhost when no host is specified [Scott Barron]
+
+* Fixed that bignums are saved properly instead of attempted to be YAMLized [Andreas Schwartz]
+
+* Fixed a bug in the GEM where the rdoc options weren't being passed according to spec [Chad Fowler]
+
+* Fixed a bug with the exclusively_dependent option for has_many
+
+
+*0.9.4*
+
+* Correctly guesses the primary key when the class is inside a module [Dave Steinberg].
+
+* Added [] and []= as alternatives to read_attribute and write_attribute [Dave Steinberg]
+
+* has_and_belongs_to_many now accepts an :order key to determine in which order the collection is returned [radsaq].
+
+* The ids passed to find and find_on_conditions are now automatically sanitized.
+
+* Added escaping of plings in YAML content.
+
+* Multi-parameter assigns where all the parameters are empty will now be set to nil instead of a new instance of their class.
+
+* Proper type within an inheritance hierarchy is now ensured already at object initialization (instead of first at create)
+
+
+*0.9.3*
+
+* Fixed bug with using a different primary key name together with has_and_belongs_to_many [Investigation by Scott]
+
+* Added :exclusively_dependent option to the has_many association macro. The doc reads:
+
+ If set to true all the associated object are deleted in one SQL statement without having their
+ before_destroy callback run. This should only be used on associations that depend solely on
+ this class and don't need to do any clean-up in before_destroy. The upside is that it's much
+ faster, especially if there's a counter_cache involved.
+
+* Added :port key to connection options, so the PostgreSQL and MySQL adapters can connect to a database server
+ running on another port than the default.
+
+* Converted the new natural singleton methods that prevented AR objects from being saved by PStore
+ (and hence be placed in a Rails session) to a module. [Florian Weber]
+
+* Fixed the use of floats (was broken since 0.9.0+)
+
+* Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with
+ Action Pack scaffolding.
+
+* Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) [radsaq]
+
+
+*0.9.2*
+
+* Added static method for instantly updating a record
+
+* Treat decimal and numeric as Ruby floats [Andreas Schwartz]
+
+* Treat chars as Ruby strings (fixes problem for Action Pack form helpers too)
+
+* Removed debugging output accidently left in (which would screw web applications)
+
+
+*0.9.1*
+
+* Added MIT license
+
+* Added natural object-style assignment for has_and_belongs_to_many associations. Consider the following model:
+
+ class Event < ActiveRecord::Base
+ has_one_and_belongs_to_many :sponsors
+ end
+
+ class Sponsor < ActiveRecord::Base
+ has_one_and_belongs_to_many :sponsors
+ end
+
+ Earlier, you'd have to use synthetic methods for creating associations between two objects of the above class:
+
+ roskilde_festival.add_to_sponsors(carlsberg)
+ roskilde_festival.remove_from_sponsors(carlsberg)
+
+ nike.add_to_events(world_cup)
+ nike.remove_from_events(world_cup)
+
+ Now you can use regular array-styled methods:
+
+ roskilde_festival.sponsors << carlsberg
+ roskilde_festival.sponsors.delete(carlsberg)
+
+ nike.events << world_cup
+ nike.events.delete(world_cup)
+
+* Added delete method for has_many associations. Using this will nullify an association between the has_many and the belonging
+ object by setting the foreign key to null. Consider this model:
+
+ class Post < ActiveRecord::Base
+ has_many :comments
+ end
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post
+ end
+
+ You could do something like:
+
+ funny_comment.has_post? # => true
+ announcement.comments.delete(funny_comment)
+ funny_comment.has_post? # => false
+
+
+*0.9.0*
+
+* Active Record is now thread safe! (So you can use it with Cerise and WEBrick applications)
+ [Implementation idea by Michael Neumann, debugging assistance by Jamis Buck]
+
+* Improved performance by roughly 400% on a basic test case of pulling 100 records and querying one attribute.
+ This brings the tax for using Active Record instead of "riding on the metal" (using MySQL-ruby C-driver directly) down to ~50%.
+ Done by doing lazy type conversions and caching column information on the class-level.
+
+* Added callback objects and procs as options for implementing the target for callback macros.
+
+* Added "counter_cache" option to belongs_to that automates the usage of increment_counter and decrement_counter. Consider:
+
+ class Post < ActiveRecord::Base
+ has_many :comments
+ end
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post
+ end
+
+ Iterating over 100 posts like this:
+
+ <% for post in @posts %>
+ <%= post.title %> has <%= post.comments_count %> comments
+ <% end %>
+
+ Will generate 100 SQL count queries -- one for each call to post.comments_count. If you instead add a "comments_count" int column
+ to the posts table and rewrite the comments association macro with:
+
+ class Comment < ActiveRecord::Base
+ belongs_to :post, :counter_cache => true
+ end
+
+ Those 100 SQL count queries will be reduced to zero. Beware that counter caching is only appropriate for objects that begin life
+ with the object it's specified to belong with and is destroyed like that as well. Typically objects where you would also specify
+ :dependent => true. If your objects switch from one belonging to another (like a post that can be move from one category to another),
+ you'll have to manage the counter yourself.
+
+* Added natural object-style assignment for has_one and belongs_to associations. Consider the following model:
+
+ class Project < ActiveRecord::Base
+ has_one :manager
+ end
+
+ class Manager < ActiveRecord::Base
+ belongs_to :project
+ end
+
+ Earlier, assignments would work like following regardless of which way the assignment told the best story:
+
+ active_record.manager_id = david.id
+
+ Now you can do it either from the belonging side:
+
+ david.project = active_record
+
+ ...or from the having side:
+
+ active_record.manager = david
+
+ If the assignment happens from the having side, the assigned object is automatically saved. So in the example above, the
+ project_id attribute on david would be set to the id of active_record, then david would be saved.
+
+* Added natural object-style assignment for has_many associations [Florian Weber]. Consider the following model:
+
+ class Project < ActiveRecord::Base
+ has_many :milestones
+ end
+
+ class Milestone < ActiveRecord::Base
+ belongs_to :project
+ end
+
+ Earlier, assignments would work like following regardless of which way the assignment told the best story:
+
+ deadline.project_id = active_record.id
+
+ Now you can do it either from the belonging side:
+
+ deadline.project = active_record
+
+ ...or from the having side:
+
+ active_record.milestones << deadline
+
+ The milestone is automatically saved with the new foreign key.
+
+* API CHANGE: Attributes for text (or blob or similar) columns will now have unknown classes stored using YAML instead of using
+ to_s. (Known classes that won't be yamelized are: String, NilClass, TrueClass, FalseClass, Fixnum, Date, and Time).
+ Likewise, data pulled out of text-based attributes will be attempted converged using Yaml if they have the "--- " header.
+ This was primarily done to be enable the storage of hashes and arrays without wrapping them in aggregations, so now you can do:
+
+ user = User.find(1)
+ user.preferences = { "background" => "black", "display" => large }
+ user.save
+
+ User.find(1).preferences # => { "background" => "black", "display" => large }
+
+ Please note that this method should only be used when you don't care about representing the object in proper columns in
+ the database. A money object consisting of an amount and a currency is still a much better fit for a value object done through
+ aggregations than this new option.
+
+* POSSIBLE CODE BREAKAGE: As a consequence of the lazy type conversions, it's a bad idea to reference the @attributes hash
+ directly (it always was, but now it's paramount that you don't). If you do, you won't get the type conversion. So to implement
+ new accessors for existing attributes, use read_attribute(attr_name) and write_attribute(attr_name, value) instead. Like this:
+
+ class Song < ActiveRecord::Base
+ # Uses an integer of seconds to hold the length of the song
+
+ def length=(minutes)
+ write_attribute("length", minutes * 60)
+ end
+
+ def length
+ read_attribute("length") / 60
+ end
+ end
+
+ The clever kid will notice that this opens a door to sidestep the automated type conversion by using @attributes directly.
+ This is not recommended as read/write_attribute may be granted additional responsibilities in the future, but if you think
+ you know what you're doing and aren't afraid of future consequences, this is an option.
+
+* Applied a few minor bug fixes reported by Daniel Von Fange.
+
+
+*0.8.4*
+
+_Reflection_
+
+* Added ActiveRecord::Reflection with a bunch of methods and classes for reflecting in aggregations and associations.
+
+* Added Base.columns and Base.content_columns which returns arrays of column description (type, default, etc) objects.
+
+* Added Base#attribute_names which returns an array of names for the attributes available on the object.
+
+* Added Base#column_for_attribute(name) which returns the column description object for the named attribute.
+
+
+_Misc_
+
+* Added multi-parameter assignment:
+
+ # Instantiate objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
+ # s for String, and a for Array.
+
+ This is incredibly useful for assigning dates from HTML drop-downs of month, year, and day.
+
+* Fixed bug with custom primary key column name and Base.find on multiple parameters.
+
+* Fixed bug with dependent option on has_one associations if there was no associated object.
+
+
+*0.8.3*
+
+_Transactions_
+
+* Added transactional protection for destroy (important for the new :dependent option) [Suggested by Carl Youngblood]
+
+* Fixed so transactions are ignored on MyISAM tables for MySQL (use InnoDB to get transactions)
+
+* Changed transactions so only exceptions will cause a rollback, not returned false.
+
+
+_Mapping_
+
+* Added support for non-integer primary keys [Aredridel/earlier work by Michael Neumann]
+
+ User.find "jdoe"
+ Product.find "PDKEY-INT-12"
+
+* Added option to specify naming method for primary key column. ActiveRecord::Base.primary_key_prefix_type can either
+ be set to nil, :table_name, or :table_name_with_underscore. :table_name will assume that Product class has a primary key
+ of "productid" and :table_name_with_underscore will assume "product_id". The default nil will just give "id".
+
+* Added an overwriteable primary_key method that'll instruct AR to the name of the
+ id column [Aredridele/earlier work by Guan Yang]
+
+ class Project < ActiveRecord::Base
+ def self.primary_key() "project_id" end
+ end
+
+* Fixed that Active Records can safely associate inside and out of modules.
+
+ class MyApplication::Account < ActiveRecord::Base
+ has_many :clients # will look for MyApplication::Client
+ has_many :interests, :class_name => "Business::Interest" # will look for Business::Interest
+ end
+
+* Fixed that Active Records can safely live inside modules [Aredridel]
+
+ class MyApplication::Account < ActiveRecord::Base
+ end
+
+
+_Misc_
+
+* Added freeze call to value object assignments to ensure they remain immutable [Spotted by Gavin Sinclair]
+
+* Changed interface for specifying observed class in observers. Was OBSERVED_CLASS constant, now is
+ observed_class() class method. This is more consistant with things like self.table_name(). Works like this:
+
+ class AuditObserver < ActiveRecord::Observer
+ def self.observed_class() Account end
+ def after_update(account)
+ AuditTrail.new(account, "UPDATED")
+ end
+ end
+
+ [Suggested by Gavin Sinclair]
+
+* Create new Active Record objects by setting the attributes through a block. Like this:
+
+ person = Person.new do |p|
+ p.name = 'Freddy'
+ p.age = 19
+ end
+
+ [Suggested by Gavin Sinclair]
+
+
+*0.8.2*
+
+* Added inheritable callback queues that can ensure that certain callback methods or inline fragments are
+ run throughout the entire inheritance hierarchy. Regardless of whether a descendent overwrites the callback
+ method:
+
+ class Topic < ActiveRecord::Base
+ before_destroy :destroy_author, 'puts "I'm an inline fragment"'
+ end
+
+ Learn more in link:classes/ActiveRecord/Callbacks.html
+
+* Added :dependent option to has_many and has_one, which will automatically destroy associated objects when
+ the holder is destroyed:
+
+ class Album < ActiveRecord::Base
+ has_many :tracks, :dependent => true
+ end
+
+ All the associated tracks are destroyed when the album is.
+
+* Added Base.create as a factory that'll create, save, and return a new object in one step.
+
+* Automatically convert strings in config hashes to symbols for the _connection methods. This allows you
+ to pass the argument hashes directly from yaml. (Luke)
+
+* Fixed the install.rb to include simple.rb [Spotted by Kevin Bullock]
+
+* Modified block syntax to better follow our code standards outlined in
+ http://www.rubyonrails.org/CodingStandards
+
+
+*0.8.1*
+
+* Added object-level transactions [Thanks to Austin Ziegler for Transaction::Simple]
+
+* Changed adapter-specific connection methods to use centralized ActiveRecord::Base.establish_connection,
+ which is parametized through a config hash with symbol keys instead of a regular parameter list.
+ This will allow for database connections to be opened in a more generic fashion. (Luke)
+
+ NOTE: This requires all *_connections to be updated! Read more in:
+ http://ar.rubyonrails.org/classes/ActiveRecord/Base.html#M000081
+
+* Fixed SQLite adapter so objects fetched from has_and_belongs_to_many have proper attributes
+ (t.name is now name). [Spotted by Garrett Rooney]
+
+* Fixed SQLite adapter so dates are returned as Date objects, not Time objects [Spotted by Gavin Sinclair]
+
+* Fixed requirement of date class, so date conversions are succesful regardless of whether you
+ manually require date or not.
+
+
+*0.8.0*
+
+* Added transactions
+
+* Changed Base.find to also accept either a list (1, 5, 6) or an array of ids ([5, 7])
+ as parameter and then return an array of objects instead of just an object
+
+* Fixed method has_collection? for has_and_belongs_to_many macro to behave as a
+ collection, not an association
+
+* Fixed SQLite adapter so empty or nil values in columns of datetime, date, or time type
+ aren't treated as current time [Spotted by Gavin Sinclair]
+
+
+*0.7.6*
+
+* Fixed the install.rb to create the lib/active_record/support directory [Spotted by Gavin Sinclair]
+* Fixed that has_association? would always return true [Spotted by Daniel Von Fange]
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
new file mode 100644
index 0000000000..5919c288e4
--- /dev/null
+++ b/activerecord/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2004 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/activerecord/README b/activerecord/README
new file mode 100755
index 0000000000..258b98f296
--- /dev/null
+++ b/activerecord/README
@@ -0,0 +1,361 @@
+= Active Record -- Object-relation mapping put on rails
+
+Active Record connects business objects and database tables to create a persistable
+domain model where logic and data is presented in one wrapping. It's an implementation
+of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html]
+by the same name as described by Martin Fowler:
+
+ "An object that wraps a row in a database table or view, encapsulates
+ the database access, and adds domain logic on that data."
+
+Active Records main contribution to the pattern is to relieve the original of two stunting problems:
+lack of associations and inheritance. By adding a simple domain language-like set of macros to describe
+the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the
+gap of functionality between the data mapper and active record approach.
+
+A short rundown of the major features:
+
+* Automated mapping between classes and tables, attributes and columns.
+
+ class Product < ActiveRecord::Base; end
+
+ ...is automatically mapped to the table named "products", such as:
+
+ CREATE TABLE products (
+ id int(11) NOT NULL auto_increment,
+ name varchar(255),
+ PRIMARY KEY (id)
+ );
+
+ ...which again gives Product#name and Product#name=(new_name)
+
+ Learn more in link:classes/ActiveRecord/Base.html
+
+
+* Associations between objects controlled by simple meta-programming macros.
+
+ class Firm < ActiveRecord::Base
+ has_many :clients
+ has_one :account
+ belongs_to :conglomorate
+ end
+
+ Learn more in link:classes/ActiveRecord/Associations/ClassMethods.html
+
+
+* Aggregations of value objects controlled by simple meta-programming macros.
+
+ class Account < ActiveRecord::Base
+ composed_of :balance, :class_name => "Money",
+ :mapping => %w(balance amount)
+ composed_of :address,
+ :mapping => [%w(address_street street), %w(address_city city)]
+ end
+
+ Learn more in link:classes/ActiveRecord/Aggregations/ClassMethods.html
+
+
+* Validation rules that can differ for new or existing objects.
+
+ class Post < ActiveRecord::Base
+ def validate # validates on both creates and updates
+ errors.add_on_empty "title"
+ end
+
+ def validate_on_update
+ errors.add_on_empty "password"
+ end
+ end
+
+ Learn more in link:classes/ActiveRecord/Validations.html
+
+
+* Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc).
+
+ class Person < ActiveRecord::Base
+ def before_destroy # is called just before Person#destroy
+ CreditCard.find(credit_card_id).destroy
+ end
+ end
+
+ class Account < ActiveRecord::Base
+ after_find :eager_load, 'self.class.announce(#{id})'
+ end
+
+ Learn more in link:classes/ActiveRecord/Callbacks.html
+
+
+* Observers for the entire lifecycle
+
+ class CommentObserver < ActiveRecord::Observer
+ def after_create(comment) # is called just after Comment#save
+ NotificationService.send_email("david@loudthinking.com", comment)
+ end
+ end
+
+ Learn more in link:classes/ActiveRecord/Observer.html
+
+
+* Inheritance hierarchies
+
+ class Company < ActiveRecord::Base; end
+ class Firm < Company; end
+ class Client < Company; end
+ class PriorityClient < Client; end
+
+ Learn more in link:classes/ActiveRecord/Base.html
+
+
+* Transaction support on both a database and object level. The latter is implemented
+ by using Transaction::Simple[http://www.halostatue.ca/ruby/Transaction__Simple.html]
+
+ # Just database transaction
+ Account.transaction do
+ david.withdrawal(100)
+ mary.deposit(100)
+ end
+
+ # Database and object transaction
+ Account.transaction(david, mary) do
+ david.withdrawal(100)
+ mary.deposit(100)
+ end
+
+ Learn more in link:classes/ActiveRecord/Transactions/ClassMethods.html
+
+
+* Reflections on columns, associations, and aggregations
+
+ reflection = Firm.reflect_on_association(:clients)
+ reflection.klass # => Client (class)
+ Firm.columns # Returns an array of column descriptors for the firms table
+
+ Learn more in link:classes/ActiveRecord/Reflection/ClassMethods.html
+
+
+* Direct manipulation (instead of service invocation)
+
+ So instead of (Hibernate[http://www.hibernate.org/] example):
+
+ long pkId = 1234;
+ DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
+ // something interesting involving a cat...
+ sess.save(cat);
+ sess.flush(); // force the SQL INSERT
+
+ Active Record lets you:
+
+ pkId = 1234
+ cat = Cat.find(pkId)
+ # something even more interesting involving a the same cat...
+ cat.save
+
+ Learn more in link:classes/ActiveRecord/Base.html
+
+
+* Database abstraction through simple adapters (~100 lines) with a shared connector
+
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite", :dbfile => "dbfile")
+
+ ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "me",
+ :password => "secret",
+ :database => "activerecord"
+ )
+
+ Learn more in link:classes/ActiveRecord/Base.html#M000081
+
+
+* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]
+
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
+ ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
+
+
+== Simple example (1/2): Defining tables and classes (using MySQL)
+
+Data definitions are specified only in the database. Active Record queries the database for
+the column names (that then serves to determine which attributes are valid) on regular
+objects instantiation through the new constructor and relies on the column names in the rows
+with the finders.
+
+ # CREATE TABLE companies (
+ # id int(11) unsigned NOT NULL auto_increment,
+ # client_of int(11),
+ # name varchar(255),
+ # type varchar(100),
+ # PRIMARY KEY (id)
+ # )
+
+Active Record automatically links the "Company" object to the "companies" table
+
+ class Company < ActiveRecord::Base
+ has_many :people, :class_name => "Person"
+ end
+
+ class Firm < Company
+ has_many :clients
+
+ def people_with_all_clients
+ clients.inject([]) { |people, client| people + client.people }
+ end
+ end
+
+The foreign_key is only necessary because we didn't use "firm_id" in the data definition
+
+ class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+ end
+
+ # CREATE TABLE people (
+ # id int(11) unsigned NOT NULL auto_increment,
+ # name text,
+ # company_id text,
+ # PRIMARY KEY (id)
+ # )
+
+Active Record will also automatically link the "Person" object to the "people" table
+
+ class Person < ActiveRecord::Base
+ belongs_to :company
+ end
+
+== Simple example (2/2): Using the domain
+
+Picking a database connection for all the active records
+
+ ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "me",
+ :password => "secret",
+ :database => "activerecord"
+ )
+
+Create some fixtures
+
+ firm = Firm.new("name" => "Next Angle")
+ # SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm")
+ firm.save
+
+ client = Client.new("name" => "37signals", "client_of" => firm.id)
+ # SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm")
+ client.save
+
+Lots of different finders
+
+ # SQL: SELECT * FROM companies WHERE id = 1
+ next_angle = Company.find(1)
+
+ # SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm'
+ next_angle = Firm.find(1)
+
+ # SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle'
+ next_angle = Company.find_first "name = 'Next Angle'"
+
+ next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
+
+The supertype, Company, will return subtype instances
+
+ Firm === next_angle
+
+All the dynamic methods added by the has_many macro
+
+ next_angle.clients.empty? # true
+ next_angle.clients.size # total number of clients
+ all_clients = next_angle.clients
+
+Constrained finds makes access security easier when ID comes from a web-app
+
+ # SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2
+ thirty_seven_signals = next_angle.clients.find(2)
+
+Bi-directional associations thanks to the "belongs_to" macro
+
+ thirty_seven_signals.firm.nil? # true
+
+
+== Examples
+
+Active Record ships with a couple of examples that should give you a good feel for
+operating usage. Be sure to edit the <tt>examples/shared_setup.rb</tt> file for your
+own database before running the examples. Possibly also the table definition SQL in
+the examples themselves.
+
+It's also highly recommended to have a look at the unit tests. Read more in link:files/RUNNING_UNIT_TESTS.html
+
+
+== Database support
+
+Active Record ships with adapters for MySQL/Ruby[http://www.tmtm.org/en/mysql/ruby/]
+(compatible with Ruby/MySQL[http://www.tmtm.org/ruby/mysql/README_en.html]),
+PostgreSQL[http://www.postgresql.jp/interfaces/ruby/], and
+SQLite[http://rubyforge.org/projects/sqlite-ruby/] (needs SQLite 2.8.13+ and SQLite-Ruby 1.1.2+).
+The adapters are around 100 lines of code fulfilling the interface specified by
+ActiveRecord::ConnectionAdapters::AbstractAdapter. Writing a new adapter should be a small task --
+especially considering the extensive test suite that'll make sure you're fulfilling the contract.
+
+
+== Philosophy
+
+Active Record attempts to provide a coherent wrapping for the inconvenience that is
+object-relational mapping. The prime directive for this mapping has been to minimize
+the amount of code needed to built a real-world domain model. This is made possible
+by relying on a number of conventions that make it easy for Active Record to infer
+complex relations and structures from a minimal amount of explicit direction.
+
+Convention over Configuration:
+* No XML-files!
+* Lots of reflection and run-time extension
+* Magic is not inherently a bad word
+
+Admit the Database:
+* Lets you drop down to SQL for odd cases and performance
+* Doesn't attempt to duplicate or replace data definitions
+
+
+== Download
+
+The latest version of Active Record can be found at
+
+* http://rubyforge.org/project/showfiles.php?group_id=182
+
+Documentation can be found at
+
+* http://ar.rubyonrails.org
+
+
+== Installation
+
+The prefered method of installing Active Record is through its GEM file. You'll need to have
+RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have,
+then use:
+
+ % [sudo] gem install activerecord-0.9.0.gem
+
+You can also install Active Record the old-fashion way with the following command:
+
+ % [sudo] ruby install.rb
+
+from its distribution directory.
+
+
+== License
+
+Active Record is released under the same license as Ruby.
+
+
+== Support
+
+The Active Record homepage is http://activerecord.rubyonrails.org. You can find the Active Record
+RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says:
+
+ Feel free to submit commits or feature requests. If you send a patch,
+ remember to update the corresponding unit tests. If fact, I prefer
+ new feature to be submitted in the form of new unit tests.
+
+For other information, feel free to ask on the ruby-talk mailing list
+(which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.
+
diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS
new file mode 100644
index 0000000000..393db82afb
--- /dev/null
+++ b/activerecord/RUNNING_UNIT_TESTS
@@ -0,0 +1,36 @@
+== Creating the test database
+
+The default names for the test databases are "activerecord_unittest" and
+"activerecord_unittest2". If you want to use another database name then be sure
+to update the connection adapter setups you want to test with in
+test/connections/<your database>/connection.rb.
+When you have the database online, you can import the fixture tables with
+the test/fixtures/db_definitions/*.sql files.
+
+Make sure that you create database objects with the same user that you specified in i
+connection.rb otherwise (on Postgres, at least) tests for default values will fail
+(see http://dev.rubyonrails.org/trac.cgi/ticket/118)
+
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for all the adapters. You can also run the suite on just
+one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
+or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+Unit tests are located in test directory. If you only want to run a single test suite,
+or don't want to bother with Rake, you can do so with something like:
+
+ cd test; ruby -I "connections/native_mysql" base_test.rb
+
+That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
+and test suite name as needed.
+
+You can also run all the suites on a specific adapter with:
+
+ cd test; all.sh "connections/native_mysql"
+
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
new file mode 100755
index 0000000000..6b17f6a61e
--- /dev/null
+++ b/activerecord/Rakefile
@@ -0,0 +1,126 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'activerecord'
+PKG_VERSION = '1.1.0' + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+PKG_FILES = FileList[
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "rakefile"
+].exclude(/\bCVS\b|~$/)
+
+
+desc "Default Task"
+task :default => [ :test_ruby_mysql, :test_mysql_ruby, :test_sqlite, :test_postgresql ]
+
+# Run the unit tests
+
+Rake::TestTask.new("test_ruby_mysql") { |t|
+ t.libs << "test" << "test/connections/native_mysql"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+Rake::TestTask.new("test_mysql_ruby") { |t|
+ t.libs << "test" << "test/connections/native_mysql"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+Rake::TestTask.new("test_postgresql") { |t|
+ t.libs << "test" << "test/connections/native_postgresql"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+Rake::TestTask.new("test_sqlite") { |t|
+ t.libs << "test" << "test/connections/native_sqlite"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+# Generate the RDoc documentation
+
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "Active Record -- Object-relation mapping put on rails"
+ rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object'
+ rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('dev-utils/*.rb')
+}
+
+
+# Publish beta gem
+desc "Publish the beta gem"
+task :pgem => [:package] do
+ Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh davidhh@one.textdrive.com './gemupdate.sh'`
+end
+
+# Publish documentation
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/ar", "doc").upload
+end
+
+
+# Create compressed packages
+
+dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Implements the ActiveRecord pattern for ORM."
+ 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.}
+
+ s.files = [ "rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG" ]
+ dist_dirs.each do |dir|
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "CVS" ) }
+ end
+ s.files.delete "test/fixtures/fixture_database.sqlite"
+ s.require_path = 'lib'
+ s.autorequire = 'active_record'
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = %w( README )
+ s.rdoc_options.concat ['--main', 'README']
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://activerecord.rubyonrails.org"
+ s.rubyforge_project = "activerecord"
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+
+task :lines do
+ lines = 0
+ codelines = 0
+ Dir.foreach("lib/active_record") { |file_name|
+ next unless file_name =~ /.*rb/
+
+ f = File.open("lib/active_record/" + file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ }
+ puts "Lines #{lines}, LOC #{codelines}"
+end
diff --git a/activerecord/benchmarks/benchmark.rb b/activerecord/benchmarks/benchmark.rb
new file mode 100644
index 0000000000..241d915208
--- /dev/null
+++ b/activerecord/benchmarks/benchmark.rb
@@ -0,0 +1,26 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+if ARGV[2]
+ require 'rubygems'
+ require_gem 'activerecord', ARGV[2]
+else
+ require 'active_record'
+end
+
+ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp")
+
+class Post < ActiveRecord::Base; end
+
+require 'benchmark'
+
+RUNS = ARGV[0].to_i
+if ARGV[1] == "profile" then require 'profile' end
+
+runtime = Benchmark::measure {
+ RUNS.times {
+ Post.find_all(nil,nil,100).each { |p| p.title }
+ }
+}
+
+puts "Runs: #{RUNS}"
+puts "Avg. runtime: #{runtime.real / RUNS}"
+puts "Requests/second: #{RUNS / runtime.real}"
diff --git a/activerecord/benchmarks/mysql_benchmark.rb b/activerecord/benchmarks/mysql_benchmark.rb
new file mode 100644
index 0000000000..2f9e0e6999
--- /dev/null
+++ b/activerecord/benchmarks/mysql_benchmark.rb
@@ -0,0 +1,19 @@
+require 'mysql'
+
+conn = Mysql::real_connect("localhost", "root", "", "basecamp")
+
+require 'benchmark'
+
+require 'profile' if ARGV[1] == "profile"
+RUNS = ARGV[0].to_i
+
+runtime = Benchmark::measure {
+ RUNS.times {
+ result = conn.query("SELECT * FROM posts LIMIT 100")
+ result.each_hash { |p| p["title"] }
+ }
+}
+
+puts "Runs: #{RUNS}"
+puts "Avg. runtime: #{runtime.real / RUNS}"
+puts "Requests/second: #{RUNS / runtime.real}" \ No newline at end of file
diff --git a/activerecord/dev-utils/eval_debugger.rb b/activerecord/dev-utils/eval_debugger.rb
new file mode 100644
index 0000000000..833bc6e052
--- /dev/null
+++ b/activerecord/dev-utils/eval_debugger.rb
@@ -0,0 +1,14 @@
+# Require this file to see the methods Active Record generates as they are added.
+class Module
+ alias :old_module_eval :module_eval
+ def module_eval(*args, &block)
+ if args[0]
+ puts "----"
+ print "module_eval in #{self.name}"
+ print ": file #{args[1]}" if args[1]
+ print " on line #{args[2]}" if args[2]
+ puts "\n#{args[0]}"
+ end
+ old_module_eval(*args, &block)
+ end
+end
diff --git a/activerecord/examples/associations.png b/activerecord/examples/associations.png
new file mode 100644
index 0000000000..661c7a8bbc
--- /dev/null
+++ b/activerecord/examples/associations.png
Binary files differ
diff --git a/activerecord/examples/associations.rb b/activerecord/examples/associations.rb
new file mode 100644
index 0000000000..b0df367321
--- /dev/null
+++ b/activerecord/examples/associations.rb
@@ -0,0 +1,87 @@
+require File.dirname(__FILE__) + '/shared_setup'
+
+logger = Logger.new(STDOUT)
+
+# Database setup ---------------
+
+logger.info "\nCreate tables"
+
+[ "DROP TABLE companies", "DROP TABLE people", "DROP TABLE people_companies",
+ "CREATE TABLE companies (id int(11) auto_increment, client_of int(11), name varchar(255), type varchar(100), PRIMARY KEY (id))",
+ "CREATE TABLE people (id int(11) auto_increment, name varchar(100), PRIMARY KEY (id))",
+ "CREATE TABLE people_companies (person_id int(11), company_id int(11), PRIMARY KEY (person_id, company_id))",
+].each { |statement|
+ # Tables doesn't necessarily already exist
+ begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end
+}
+
+
+# Class setup ---------------
+
+class Company < ActiveRecord::Base
+ has_and_belongs_to_many :people, :class_name => "Person", :join_table => "people_companies", :table_name => "people"
+end
+
+class Firm < Company
+ has_many :clients, :foreign_key => "client_of"
+
+ def people_with_all_clients
+ clients.inject([]) { |people, client| people + client.people }
+ end
+end
+
+class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+end
+
+class Person < ActiveRecord::Base
+ has_and_belongs_to_many :companies, :join_table => "people_companies"
+ def self.table_name() "people" end
+end
+
+
+# Usage ---------------
+
+logger.info "\nCreate fixtures"
+
+Firm.new("name" => "Next Angle").save
+Client.new("name" => "37signals", "client_of" => 1).save
+Person.new("name" => "David").save
+
+
+logger.info "\nUsing Finders"
+
+next_angle = Company.find(1)
+next_angle = Firm.find(1)
+next_angle = Company.find_first "name = 'Next Angle'"
+next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
+
+Firm === next_angle
+
+
+logger.info "\nUsing has_many association"
+
+next_angle.has_clients?
+next_angle.clients_count
+all_clients = next_angle.clients
+
+thirty_seven_signals = next_angle.find_in_clients(2)
+
+
+logger.info "\nUsing belongs_to association"
+
+thirty_seven_signals.has_firm?
+thirty_seven_signals.firm?(next_angle)
+
+
+logger.info "\nUsing has_and_belongs_to_many association"
+
+david = Person.find(1)
+david.add_companies(thirty_seven_signals, next_angle)
+david.companies.include?(next_angle)
+david.companies_count == 2
+
+david.remove_companies(next_angle)
+david.companies_count == 1
+
+thirty_seven_signals.people.include?(david) \ No newline at end of file
diff --git a/activerecord/examples/shared_setup.rb b/activerecord/examples/shared_setup.rb
new file mode 100644
index 0000000000..6ede4b1d35
--- /dev/null
+++ b/activerecord/examples/shared_setup.rb
@@ -0,0 +1,15 @@
+# Be sure to change the mysql_connection details and create a database for the example
+
+$: << File.dirname(__FILE__) + '/../lib'
+
+require 'active_record'
+require 'logger'; class Logger; def format_message(severity, timestamp, msg, progname) "#{msg}\n" end; end
+
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "root",
+ :password => "",
+ :database => "activerecord_examples"
+)
diff --git a/activerecord/examples/validation.rb b/activerecord/examples/validation.rb
new file mode 100644
index 0000000000..334a1685f7
--- /dev/null
+++ b/activerecord/examples/validation.rb
@@ -0,0 +1,88 @@
+require File.dirname(__FILE__) + '/shared_setup'
+
+logger = Logger.new(STDOUT)
+
+# Database setup ---------------
+
+logger.info "\nCreate tables"
+
+[ "DROP TABLE people",
+ "CREATE TABLE people (id int(11) auto_increment, name varchar(100), pass varchar(100), email varchar(100), PRIMARY KEY (id))"
+].each { |statement|
+ begin; ActiveRecord::Base.connection.execute(statement); rescue ActiveRecord::StatementInvalid; end # Tables doesn't necessarily already exist
+}
+
+
+# Class setup ---------------
+
+class Person < ActiveRecord::Base
+ # Active Record can only guess simple table names like Card/cards, Company/companies
+ def self.table_name() "people" end
+
+ # Using
+ def self.authenticate(name, pass)
+ # find_first "name = '#{name}' AND pass = '#{pass}'" would be open to sql-injection (in a web-app scenario)
+ find_first [ "name = '%s' AND pass = '%s'", name, pass ]
+ end
+
+ def self.name_exists?(name, id = nil)
+ if id.nil?
+ condition = [ "name = '%s'", name ]
+ else
+ # Check if anyone else than the person identified by person_id has that user_name
+ condition = [ "name = '%s' AND id <> %d", name, id ]
+ end
+
+ !find_first(condition).nil?
+ end
+
+ def email_address_with_name
+ "\"#{name}\" <#{email}>"
+ end
+
+ protected
+ def validate
+ errors.add_on_empty(%w(name pass email))
+ errors.add("email", "must be valid") unless email_address_valid?
+ end
+
+ def validate_on_create
+ if attribute_present?("name") && Person.name_exists?(name)
+ errors.add("name", "is already taken by another person")
+ end
+ end
+
+ def validate_on_update
+ if attribute_present?("name") && Person.name_exists?(name, id)
+ errors.add("name", "is already taken by another person")
+ end
+ end
+
+ private
+ def email_address_valid?() email =~ /\w[-.\w]*\@[-\w]+[-.\w]*\.\w+/ end
+end
+
+# Usage ---------------
+
+logger.info "\nCreate fixtures"
+david = Person.new("name" => "David Heinemeier Hansson", "pass" => "", "email" => "")
+unless david.save
+ puts "There was #{david.errors.count} error(s)"
+ david.errors.each_full { |error| puts error }
+end
+
+david.pass = "something"
+david.email = "invalid_address"
+unless david.save
+ puts "There was #{david.errors.count} error(s)"
+ puts "It was email with: " + david.errors.on("email")
+end
+
+david.email = "david@loudthinking.com"
+if david.save then puts "David finally made it!" end
+
+
+another_david = Person.new("name" => "David Heinemeier Hansson", "pass" => "xc", "email" => "david@loudthinking")
+unless another_david.save
+ puts "Error on name: " + another_david.errors.on("name")
+end \ No newline at end of file
diff --git a/activerecord/install.rb b/activerecord/install.rb
new file mode 100644
index 0000000000..52e162a707
--- /dev/null
+++ b/activerecord/install.rb
@@ -0,0 +1,60 @@
+require 'rbconfig'
+require 'find'
+require 'ftools'
+
+include Config
+
+# this was adapted from rdoc's install.rb by ways of Log4r
+
+$sitedir = CONFIG["sitelibdir"]
+unless $sitedir
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
+ if !$sitedir
+ $sitedir = File.join($libdir, "site_ruby")
+ elsif $sitedir !~ Regexp.quote(version)
+ $sitedir = File.join($sitedir, version)
+ end
+end
+
+makedirs = %w{ active_record/associations active_record/connection_adapters active_record/support active_record/vendor }
+makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
+
+# deprecated files that should be removed
+# deprecated = %w{ }
+
+# files to install in library path
+files = %w-
+ active_record.rb
+ active_record/aggregations.rb
+ active_record/associations.rb
+ active_record/associations/association_collection.rb
+ active_record/associations/has_and_belongs_to_many_association.rb
+ active_record/associations/has_many_association.rb
+ active_record/base.rb
+ active_record/callbacks.rb
+ active_record/connection_adapters/abstract_adapter.rb
+ active_record/connection_adapters/mysql_adapter.rb
+ active_record/connection_adapters/postgresql_adapter.rb
+ active_record/connection_adapters/sqlite_adapter.rb
+ active_record/deprecated_associations.rb
+ active_record/fixtures.rb
+ active_record/observer.rb
+ active_record/reflection.rb
+ active_record/support/class_attribute_accessors.rb
+ active_record/support/class_inheritable_attributes.rb
+ active_record/support/clean_logger.rb
+ active_record/support/inflector.rb
+ active_record/transactions.rb
+ active_record/validations.rb
+ active_record/vendor/mysql.rb
+ active_record/vendor/simple.rb
+-
+
+# the acual gruntwork
+Dir.chdir("lib")
+# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
+files.each {|f|
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
+}
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
new file mode 100755
index 0000000000..9ce79284cd
--- /dev/null
+++ b/activerecord/lib/active_record.rb
@@ -0,0 +1,50 @@
+#--
+# Copyright (c) 2004 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+
+$:.unshift(File.dirname(__FILE__))
+
+require 'active_record/support/clean_logger'
+
+require 'active_record/base'
+require 'active_record/observer'
+require 'active_record/validations'
+require 'active_record/callbacks'
+require 'active_record/associations'
+require 'active_record/aggregations'
+require 'active_record/transactions'
+require 'active_record/reflection'
+
+ActiveRecord::Base.class_eval do
+ include ActiveRecord::Validations
+ include ActiveRecord::Callbacks
+ include ActiveRecord::Associations
+ include ActiveRecord::Aggregations
+ include ActiveRecord::Transactions
+ include ActiveRecord::Reflection
+end
+
+require 'active_record/connection_adapters/mysql_adapter'
+require 'active_record/connection_adapters/postgresql_adapter'
+require 'active_record/connection_adapters/sqlite_adapter'
+require 'active_record/connection_adapters/sqlserver_adapter' \ No newline at end of file
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
new file mode 100644
index 0000000000..82011018a2
--- /dev/null
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -0,0 +1,165 @@
+module ActiveRecord
+ module Aggregations # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
+ # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
+ # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
+ # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
+ # and how it can be turned back into attributes (when the entity is saved to the database). Example:
+ #
+ # class Customer < ActiveRecord::Base
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # end
+ #
+ # The customer class now has the following methods to manipulate the value objects:
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
+ # * <tt>Customer#address, Customer#address=(address)</tt>
+ #
+ # These methods will operate with value objects like the ones described below:
+ #
+ # class Money
+ # include Comparable
+ # attr_reader :amount, :currency
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
+ #
+ # def initialize(amount, currency = "USD")
+ # @amount, @currency = amount, currency
+ # end
+ #
+ # def exchange_to(other_currency)
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
+ # Money.new(exchanged_amount, other_currency)
+ # end
+ #
+ # def ==(other_money)
+ # amount == other_money.amount && currency == other_money.currency
+ # end
+ #
+ # def <=>(other_money)
+ # if currency == other_money.currency
+ # amount <=> amount
+ # else
+ # amount <=> other_money.exchange_to(currency).amount
+ # end
+ # end
+ # end
+ #
+ # class Address
+ # attr_reader :street, :city
+ # def initialize(street, city)
+ # @street, @city = street, city
+ # end
+ #
+ # def close_to?(other_address)
+ # city == other_address.city
+ # end
+ #
+ # def ==(other_address)
+ # city == other_address.city && street == other_address.street
+ # end
+ # end
+ #
+ # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
+ # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
+ # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
+ #
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
+ # customer.balance # => Money value object
+ # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
+ # customer.balance > Money.new(10) # => true
+ # customer.balance == Money.new(20) # => true
+ # customer.balance < Money.new(5) # => false
+ #
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
+ # determine the order of the parameters. Example:
+ #
+ # customer.address_street = "Hyancintvej"
+ # customer.address_city = "Copenhagen"
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
+ # customer.address = Address.new("May Street", "Chicago")
+ # customer.address_street # => "May Street"
+ # customer.address_city # => "Chicago"
+ #
+ # == Writing value objects
+ #
+ # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
+ # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
+ # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
+ # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
+ #
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
+ # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
+ # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
+ # changed through other means than the writer method.
+ #
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
+ # change it afterwards will result in a TypeError.
+ #
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
+ # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
+ module ClassMethods
+ # Adds the a reader and writer method for manipulating a value object, so
+ # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
+ # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
+ # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
+ # to a constructor parameter on the value class.
+ #
+ # Option examples:
+ # composed_of :temperature, :mapping => %w(reading celsius)
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ def composed_of(part_id, options = {})
+ validate_options([ :class_name, :mapping ], options.keys)
+
+ name = part_id.id2name
+ class_name = options[:class_name] || name_to_class_name(name)
+ mapping = options[:mapping]
+
+ reader_method(name, class_name, mapping)
+ writer_method(name, class_name, mapping)
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def name_to_class_name(name)
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ end
+
+ def reader_method(name, class_name, mapping)
+ module_eval <<-end_eval
+ def #{name}(force_reload = false)
+ if @#{name}.nil? || force_reload
+ @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
+ end
+
+ return @#{name}
+ end
+ end_eval
+ end
+
+ def writer_method(name, class_name, mapping)
+ module_eval <<-end_eval
+ def #{name}=(part)
+ @#{name} = part.freeze
+ #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
+ end
+ end_eval
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
new file mode 100755
index 0000000000..6285a59882
--- /dev/null
+++ b/activerecord/lib/active_record/associations.rb
@@ -0,0 +1,576 @@
+require 'active_record/associations/association_collection'
+require 'active_record/associations/has_many_association'
+require 'active_record/associations/has_and_belongs_to_many_association'
+require 'active_record/deprecated_associations'
+
+module ActiveRecord
+ module Associations # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
+ # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
+ # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
+ # methods. Example:
+ #
+ # class Project < ActiveRecord::Base
+ # belongs_to :portfolio
+ # has_one :project_manager
+ # has_many :milestones
+ # has_and_belongs_to_many :categories
+ # end
+ #
+ # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
+ # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
+ # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
+ # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
+ # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
+ # <tt>Project#categories.delete(category1)</tt>
+ #
+ # == Example
+ #
+ # link:../examples/associations.png
+ #
+ # == Is it belongs_to or has_one?
+ #
+ # Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
+ # saying belongs_to. Example:
+ #
+ # class Post < ActiveRecord::Base
+ # has_one :author
+ # end
+ #
+ # class Author < ActiveRecord::Base
+ # belongs_to :post
+ # end
+ #
+ # The tables for these classes could look something like:
+ #
+ # CREATE TABLE posts (
+ # id int(11) NOT NULL auto_increment,
+ # title varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # CREATE TABLE authors (
+ # id int(11) NOT NULL auto_increment,
+ # post_id int(11) default NULL,
+ # name varchar default NULL,
+ # PRIMARY KEY (id)
+ # )
+ #
+ # == Caching
+ #
+ # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
+ # instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
+ # worrying too much about performance at the first go. Example:
+ #
+ # project.milestones # fetches milestones from the database
+ # project.milestones.size # uses the milestone cache
+ # project.milestones.empty? # uses the milestone cache
+ # project.milestones(true).size # fetches milestones from the database
+ # project.milestones # uses the milestone cache
+ #
+ # == Modules
+ #
+ # By default, associations will look for objects within the current module scope. Consider:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base
+ # has_many :clients
+ # end
+ #
+ # class Company < ActiveRecord::Base; end
+ # end
+ # end
+ #
+ # When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
+ # with a class in another module scope this can be done by specifying the complete class name, such as:
+ #
+ # module MyApplication
+ # module Business
+ # class Firm < ActiveRecord::Base; end
+ # end
+ #
+ # module Billing
+ # class Account < ActiveRecord::Base
+ # belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+ # end
+ # end
+ # end
+ #
+ # == Type safety with ActiveRecord::AssociationTypeMismatch
+ #
+ # If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
+ # get a ActiveRecord::AssociationTypeMismatch.
+ #
+ # == Options
+ #
+ # All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
+ # possible.
+ module ClassMethods
+ # Adds the following methods for retrival and query of collections of associated objects.
+ # +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
+ # An empty array is returned if none are found.
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
+ # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
+ # * <tt>collection.size</tt> - returns the number of associated objects.
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
+ # meets the condition that it has to be associated with this object.
+ # * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
+ # criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
+ # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
+ # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
+ #
+ # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients<<</tt>
+ # * <tt>Firm#clients.delete</tt>
+ # * <tt>Firm#clients.clear</tt>
+ # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
+ # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
+ # * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
+ # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
+ # if the real class name is +SpecialProduct+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
+ # sql fragment, such as "price > 5 AND name LIKE 'B%'".
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
+ # such as "last_name, first_name DESC"
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
+ # as the default foreign_key.
+ # * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object.
+ # May not be set if :exclusively_dependent is also set.
+ # * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
+ # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
+ # clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
+ # May not be set if :dependent is also set.
+ # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
+ # associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
+ #
+ # Option examples:
+ # has_many :comments, :order => "posted_on"
+ # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
+ # has_many :tracks, :order => "position", :dependent => true
+ # has_many :subscribers, :class_name => "Person", :finder_sql =>
+ # 'SELECT DISTINCT people.* ' +
+ # 'FROM people p, post_subscriptions ps ' +
+ # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
+ # 'ORDER BY p.first_name'
+ def has_many(association_id, options = {})
+ validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
+ association_name, association_class_name, association_class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
+
+ require_association_class(association_class_name)
+
+ if options[:dependent] and options[:exclusively_dependent]
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
+ elsif options[:dependent]
+ module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
+ elsif options[:exclusively_dependent]
+ module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
+ end
+
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ if association.nil?
+ association = HasManyAssociation.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, options)
+ instance_variable_set("@#{association_name}", association)
+ end
+ association.reload if force_reload
+ association
+ end
+
+ # deprecated api
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ deprecated_find_in_collection_method(association_name)
+ deprecated_find_all_in_collection_method(association_name)
+ deprecated_create_method(association_name)
+ deprecated_build_method(association_name)
+ end
+
+ # Adds the following methods for retrival and query of a single associated object.
+ # +association+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # and saves the associate object.
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * <tt>association.nil?</tt> - returns true if there is no associated object.
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
+ #
+ # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
+ # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
+ # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
+ # * <tt>Account#beneficiary.nil?</tt>
+ # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
+ # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
+ # if the real class name is +Person+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "rank = 5".
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
+ # as the default foreign_key.
+ #
+ # Option examples:
+ # has_one :credit_card, :dependent => true
+ # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
+ # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
+ def has_one(association_id, options = {})
+ options.merge!({ :remote => true })
+ belongs_to(association_id, options)
+
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+
+ require_association_class(association_class_name)
+
+ has_one_writer_method(association_name, association_class_name, class_primary_key_name)
+ build_method("build_", association_name, association_class_name, class_primary_key_name)
+ create_method("create_", association_name, association_class_name, class_primary_key_name)
+
+ module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
+ end
+
+ # Adds the following methods for retrival and query for a single associated object that this object holds an id to.
+ # +association+ is replaced with the symbol passed as the first argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
+ # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * <tt>association.nil?</tt> - returns true if there is no associated object.
+ #
+ # Example: An Post class declares <tt>has_one :author</tt>, which will add:
+ # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
+ # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
+ # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
+ # * <tt>Post#author.nil?</tt>
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
+ # if the real class name is +Person+, you'll have to specify it with this option.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
+ # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
+ # +Boss+ class will use "boss_id" as the default foreign_key.
+ # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
+ # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
+ # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
+ # is used on the associate class (such as a Post class).
+ #
+ # Option examples:
+ # belongs_to :firm, :foreign_key => "client_of"
+ # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
+ # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
+ # :conditions => 'discounts > #{payments_count}'
+ def belongs_to(association_id, options = {})
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
+
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+
+ require_association_class(association_class_name)
+
+ association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
+
+ if options[:remote]
+ association_finder = <<-"end_eval"
+ #{association_class_name}.find_first(
+ "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
+ #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
+ )
+ end_eval
+ else
+ association_finder = options[:conditions] ?
+ "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
+ "#{association_class_name}.find(#{association_class_primary_key_name})"
+ end
+
+ has_association_method(association_name)
+ association_reader_method(association_name, association_finder)
+ belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
+ association_comparison_method(association_name, association_class_name)
+
+ if options[:counter_cache]
+ module_eval(
+ "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
+ " if has_#{association_name}?'"
+ )
+
+ module_eval(
+ "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
+ " if has_#{association_name}?'"
+ )
+ end
+ end
+
+ # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
+ # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
+ # will give the default join table name of "developers_projects" because "D" outranks "P".
+ #
+ # Any additional fields added to the join table will be placed as attributes when pulling records out through
+ # has_and_belongs_to_many associations. This is helpful when have information about the association itself
+ # that you want available on retrival.
+ #
+ # Adds the following methods for retrival and query.
+ # +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
+ # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
+ # An empty array is returned if none is found.
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
+ # (collection.push and collection.concat are aliases to this method).
+ # * <tt>collection.push_with_attributes(object, join_attributes)</tt> - adds one to the collection by creating an association in the join table that
+ # also holds the attributes from <tt>join_attributes</tt> (should be a hash with the column names as keys). This can be used to have additional
+ # attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
+ # (collection.concat_with_attributes is an alias to this method).
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
+ # This does not destroy the objects.
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
+ # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
+ # * <tt>collection.size</tt> - returns the number of associated objects.
+ #
+ # Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
+ # * <tt>Developer#projects</tt>
+ # * <tt>Developer#projects<<</tt>
+ # * <tt>Developer#projects.delete</tt>
+ # * <tt>Developer#projects.clear</tt>
+ # * <tt>Developer#projects.empty?</tt>
+ # * <tt>Developer#projects.size</tt>
+ # * <tt>Developer#projects.find(id)</tt>
+ # The declaration may include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
+ # +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
+ # * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
+ # WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
+ # has_and_belongs_to_many declaration in order to work.
+ # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
+ # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
+ # will use "person_id" as the default foreign_key.
+ # * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
+ # guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
+ # that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * <tt>:uniq</tt> - if set to true, duplicate associated objects will be ignored by accessors and query methods
+ # * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
+ # * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
+ # classes with a manual one
+ # * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
+ # with a manual one
+ #
+ # Option examples:
+ # has_and_belongs_to_many :projects
+ # has_and_belongs_to_many :nations, :class_name => "Country"
+ # has_and_belongs_to_many :categories, :join_table => "prods_cats"
+ def has_and_belongs_to_many(association_id, options = {})
+ validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
+ association_name, association_class_name, association_class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key])
+
+ require_association_class(association_class_name)
+
+ join_table = options[:join_table] ||
+ join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
+
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ if association.nil?
+ association = HasAndBelongsToManyAssociation.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, join_table, options)
+ instance_variable_set("@#{association_name}", association)
+ end
+ association.reload if force_reload
+ association
+ end
+
+ before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
+ module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
+
+ # deprecated api
+ deprecated_collection_count_method(association_name)
+ deprecated_add_association_relation(association_name)
+ deprecated_remove_association_relation(association_name)
+ deprecated_has_collection_method(association_name)
+ end
+
+ private
+ # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
+ def validate_options(valid_option_keys, supplied_option_keys)
+ unknown_option_keys = supplied_option_keys - valid_option_keys
+ raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
+ end
+
+ def join_table_name(first_table_name, second_table_name)
+ if first_table_name < second_table_name
+ join_table = "#{first_table_name}_#{second_table_name}"
+ else
+ join_table = "#{second_table_name}_#{first_table_name}"
+ end
+
+ table_name_prefix + join_table + table_name_suffix
+ end
+
+ def associate_identification(association_id, association_class_name, foreign_key, plural = true)
+ if association_class_name !~ /::/
+ association_class_name = type_name_with_module(
+ association_class_name ||
+ Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
+ )
+ end
+
+ primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
+
+ return association_id.id2name, association_class_name, primary_key_name
+ end
+
+ def association_comparison_method(association_name, association_class_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}?(comparison_object, force_reload = false)
+ if comparison_object.kind_of?(#{association_class_name})
+ #{association_name}(force_reload) == comparison_object
+ else
+ raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
+ end
+ end
+ end_eval
+ end
+
+ def association_reader_method(association_name, association_finder)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}(force_reload = false)
+ if @#{association_name}.nil? || force_reload
+ begin
+ @#{association_name} = #{association_finder}
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
+ nil
+ end
+ end
+
+ return @#{association_name}
+ end
+ end_eval
+ end
+
+ def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}=(association)
+ if association.nil?
+ @#{association_name}.#{class_primary_key_name} = nil
+ @#{association_name}.save(false)
+ @#{association_name} = nil
+ else
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
+ association.#{class_primary_key_name} = id
+ association.save(false)
+ @#{association_name} = association
+ end
+ end
+ end_eval
+ end
+
+ def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}=(association)
+ if association.nil?
+ @#{association_name} = self.#{association_class_primary_key_name} = nil
+ else
+ raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
+ @#{association_name} = association
+ self.#{association_class_primary_key_name} = association.id
+ end
+ end
+ end_eval
+ end
+
+ def has_association_method(association_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{association_name}?(force_reload = false)
+ !#{association_name}(force_reload).nil?
+ end
+ end_eval
+ end
+
+ def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = {})
+ association = #{collection_class_name}.new
+ association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
+ association
+ end
+ end_eval
+ end
+
+ def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = nil)
+ #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
+ end
+ end_eval
+ end
+
+ def require_association_class(class_name)
+ begin
+ require(Inflector.underscore(class_name))
+ rescue LoadError
+ if logger
+ logger.info "#{self.to_s} failed to require #{class_name}"
+ else
+ STDERR << "#{self.to_s} failed to require #{class_name}\n"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
new file mode 100644
index 0000000000..a60b9ddab5
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -0,0 +1,129 @@
+module ActiveRecord
+ module Associations
+ class AssociationCollection #:nodoc:
+ alias_method :proxy_respond_to?, :respond_to?
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
+
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @owner = owner
+ @options = options
+ @association_name = association_name
+ @association_class = eval(association_class_name)
+ @association_class_primary_key_name = association_class_primary_key_name
+ end
+
+ def method_missing(symbol, *args, &block)
+ load_collection
+ @collection.send(symbol, *args, &block)
+ end
+
+ def to_ary
+ load_collection
+ @collection.to_ary
+ end
+
+ def respond_to?(symbol)
+ proxy_respond_to?(symbol) || [].respond_to?(symbol)
+ end
+
+ def loaded?
+ !@collection.nil?
+ end
+
+ def reload
+ @collection = nil
+ end
+
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
+ def <<(*records)
+ flatten_deeper(records).each do |record|
+ raise_on_type_mismatch(record)
+ insert_record(record)
+ @collection << record if loaded?
+ end
+ self
+ end
+
+ alias_method :push, :<<
+ alias_method :concat, :<<
+
+ # Remove +records+ from this association. Does not destroy +records+.
+ def delete(*records)
+ records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+ delete_records(records)
+ records.each { |record| @collection.delete(record) } if loaded?
+ end
+
+ def destroy_all
+ each { |record| record.destroy }
+ @collection = []
+ end
+
+ def size
+ if loaded? then @collection.size else count_records end
+ end
+
+ def empty?
+ size == 0
+ end
+
+ def uniq(collection = self)
+ collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
+ end
+
+ alias_method :length, :size
+
+ protected
+ def loaded?
+ not @collection.nil?
+ end
+
+ def quoted_record_ids(records)
+ records.map { |record| "'#{@association_class.send(:sanitize, record.id)}'" }.join(',')
+ end
+
+ def interpolate_sql_options!(options, *keys)
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
+ end
+
+ def interpolate_sql(sql, record = nil)
+ @owner.send(:interpolate_sql, sql, record)
+ end
+
+ private
+ def load_collection
+ begin
+ @collection = find_all_records unless loaded?
+ rescue ActiveRecord::RecordNotFound
+ @collection = []
+ end
+ end
+
+ def raise_on_type_mismatch(record)
+ raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
+ end
+
+
+ def load_collection_to_array
+ return unless @collection_array.nil?
+ begin
+ @collection_array = find_all_records
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
+ @collection_array = []
+ end
+ end
+
+ def duplicated_records_array(records)
+ records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
+ records.dup
+ end
+
+ # Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
+ def flatten_deeper(array)
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
new file mode 100644
index 0000000000..946f238f21
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -0,0 +1,107 @@
+module ActiveRecord
+ module Associations
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
+ super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+
+ @association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name.downcase)) + "_id"
+ association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
+ @join_table = join_table
+ @order = options[:order] || "t.#{@owner.class.primary_key}"
+
+ interpolate_sql_options!(options, :finder_sql, :delete_sql)
+ @finder_sql = options[:finder_sql] ||
+ "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
+ "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
+ "j.#{association_class_primary_key_name} = '#{@owner.id}' " +
+ (options[:conditions] ? " AND " + options[:conditions] : "") + " " +
+ "ORDER BY #{@order}"
+ end
+
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
+ def clear
+ return self if size == 0 # forces load_collection if hasn't happened already
+
+ if sql = @options[:delete_sql]
+ each { |record| @owner.connection.execute(sql) }
+ elsif @options[:conditions]
+ sql =
+ "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' " +
+ "AND #{@association_foreign_key} IN (#{collect { |record| record.id }.join(", ")})"
+ @owner.connection.execute(sql)
+ else
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
+ @owner.connection.execute(sql)
+ end
+
+ @collection = []
+ self
+ end
+
+ def find(association_id = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find(&block)
+ else
+ if loaded?
+ find_all { |record| record.id == association_id.to_i }.first
+ else
+ find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} = '#{association_id}' ORDER BY")).first
+ end
+ end
+ end
+
+ def push_with_attributes(record, join_attributes = {})
+ raise_on_type_mismatch(record)
+ insert_record_with_join_attributes(record, join_attributes)
+ join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
+ @collection << record if loaded?
+ self
+ end
+
+ alias :concat_with_attributes :push_with_attributes
+
+ def size
+ @options[:uniq] ? count_records : super
+ end
+
+ protected
+ def find_all_records(sql = @finder_sql)
+ records = @association_class.find_by_sql(sql)
+ @options[:uniq] ? uniq(records) : records
+ end
+
+ def count_records
+ load_collection
+ @collection.size
+ end
+
+ def insert_record(record)
+ if @options[:insert_sql]
+ @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
+ else
+ sql = "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) VALUES ('#{@owner.id}','#{record.id}')"
+ @owner.connection.execute(sql)
+ end
+ end
+
+ def insert_record_with_join_attributes(record, join_attributes)
+ attributes = { @association_class_primary_key_name => @owner.id, @association_foreign_key => record.id }.update(join_attributes)
+ sql =
+ "INSERT INTO #{@join_table} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
+ "VALUES (#{attributes.values.collect { |value| @owner.send(:quote, value) }.join(', ')})"
+ @owner.connection.execute(sql)
+ end
+
+ def delete_records(records)
+ if sql = @options[:delete_sql]
+ records.each { |record| @owner.connection.execute(sql) }
+ else
+ ids = quoted_record_ids(records)
+ sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_foreign_key} IN (#{ids})"
+ @owner.connection.execute(sql)
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
new file mode 100644
index 0000000000..947862ad37
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -0,0 +1,102 @@
+module ActiveRecord
+ module Associations
+ class HasManyAssociation < AssociationCollection #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @conditions = @association_class.send(:sanitize_conditions, options[:conditions])
+
+ if options[:finder_sql]
+ @finder_sql = interpolate_sql(options[:finder_sql])
+ @counter_sql = @finder_sql.gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
+ else
+ @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
+ @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
+ end
+ end
+
+ def create(attributes = {})
+ # Can't use Base.create since the foreign key may be a protected attribute.
+ record = build(attributes)
+ record.save
+ @collection << record if loaded?
+ record
+ end
+
+ def build(attributes = {})
+ record = @association_class.new(attributes)
+ record[@association_class_primary_key_name] = @owner.id
+ record
+ end
+
+ def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find_all(&block)
+ else
+ @association_class.find_all(
+ "#{@association_class_primary_key_name} = '#{@owner.id}' " +
+ "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + @association_class.send(:sanitize_conditions, runtime_conditions) : ""}",
+ orderings,
+ limit,
+ joins
+ )
+ end
+ end
+
+ def find(association_id = nil, &block)
+ if block_given? || @options[:finder_sql]
+ load_collection
+ @collection.find(&block)
+ else
+ @association_class.find_on_conditions(association_id,
+ "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
+ )
+ end
+ end
+
+ # Removes all records from this association. Returns +self+ so
+ # method calls may be chained.
+ def clear
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}'")
+ @collection = []
+ self
+ end
+
+ protected
+ def find_all_records
+ if @options[:finder_sql]
+ @association_class.find_by_sql(@finder_sql)
+ else
+ @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
+ end
+ end
+
+ def count_records
+ if has_cached_counter?
+ @owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif @options[:finder_sql]
+ @association_class.count_by_sql(@counter_sql)
+ else
+ @association_class.count(@counter_sql)
+ end
+ end
+
+ def has_cached_counter?
+ @owner.attribute_present?(cached_counter_attribute_name)
+ end
+
+ def cached_counter_attribute_name
+ "#{@association_name}_count"
+ end
+
+ def insert_record(record)
+ record.update_attribute(@association_class_primary_key_name, @owner.id)
+ end
+
+ def delete_records(records)
+ ids = quoted_record_ids(records)
+ @association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = '#{@owner.id}' AND #{@association_class.primary_key} IN (#{ids})")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
new file mode 100755
index 0000000000..3312d41d06
--- /dev/null
+++ b/activerecord/lib/active_record/base.rb
@@ -0,0 +1,1051 @@
+require 'active_record/support/class_attribute_accessors'
+require 'active_record/support/class_inheritable_attributes'
+require 'active_record/support/inflector'
+require 'yaml'
+
+module ActiveRecord #:nodoc:
+ class ActiveRecordError < StandardError #:nodoc:
+ end
+ class AssociationTypeMismatch < ActiveRecordError #:nodoc:
+ end
+ class SerializationTypeMismatch < ActiveRecordError #:nodoc:
+ end
+ class AdapterNotSpecified < ActiveRecordError # :nodoc:
+ end
+ class AdapterNotFound < ActiveRecordError # :nodoc:
+ end
+ class ConnectionNotEstablished < ActiveRecordError #:nodoc:
+ end
+ class ConnectionFailed < ActiveRecordError #:nodoc:
+ end
+ class RecordNotFound < ActiveRecordError #:nodoc:
+ end
+ class StatementInvalid < ActiveRecordError #:nodoc:
+ end
+
+ # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
+ # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
+ # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
+ # database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
+ #
+ # See the mapping rules in table_name and the full example in link:files/README.html for more insight.
+ #
+ # == Creation
+ #
+ # Active Records accepts constructor parameters either in a hash or as a block. The hash method is especially useful when
+ # you're receiving the data from somewhere else, like a HTTP request. It works like this:
+ #
+ # user = User.new("name" => "David", "occupation" => "Code Artist")
+ # user.name # => "David"
+ #
+ # You can also use block initialization:
+ #
+ # user = User.new do |u|
+ # u.name = "David"
+ # u.occupation = "Code Artist"
+ # end
+ #
+ # And of course you can just create a bare object and specify the attributes after the fact:
+ #
+ # user = User.new
+ # user.name = "David"
+ # user.occupation = "Code Artist"
+ #
+ # == Conditions
+ #
+ # Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
+ # The array form is to be used when the condition input is tainted and requires sanitization. The string form can
+ # be used for statements that doesn't involve tainted data. Examples:
+ #
+ # User < ActiveRecord::Base
+ # def self.authenticate_unsafely(user_name, password)
+ # find_first("user_name = '#{user_name}' AND password = '#{password}'")
+ # end
+ #
+ # def self.authenticate_safely(user_name, password)
+ # find_first([ "user_name = '%s' AND password = '%s'", user_name, password ])
+ # end
+ # end
+ #
+ # The +authenticate_unsafely+ method inserts the parameters directly into the query and is thus susceptible to SQL-injection
+ # attacks if the +user_name+ and +password+ parameters come directly from a HTTP request. The +authenticate_safely+ method, on
+ # the other hand, will sanitize the +user_name+ and +password+ before inserting them in the query, which will ensure that
+ # an attacker can't escape the query and fake the login (or worse).
+ #
+ # == Overwriting default accessors
+ #
+ # All column values are automatically available through basic accessors on the Active Record object, but some times you
+ # want to specialize this behavior. This can be done by either by overwriting the default accessors (using the same
+ # name as the attribute) calling read_attribute(attr_name) and write_attribute(attr_name, value) to actually change things.
+ # Example:
+ #
+ # class Song < ActiveRecord::Base
+ # # Uses an integer of seconds to hold the length of the song
+ #
+ # def length=(minutes)
+ # write_attribute("length", minutes * 60)
+ # end
+ #
+ # def length
+ # read_attribute("length") / 60
+ # end
+ # end
+ #
+ # == Saving arrays, hashes, and other non-mappeable objects in text columns
+ #
+ # Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
+ # This makes it possible to store arrays, hashes, and other non-mappeable objects without doing any additional work. Example:
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences
+ # end
+ #
+ # user = User.create("preferences" => { "background" => "black", "display" => large })
+ # User.find(user.id).preferences # => { "background" => "black", "display" => large }
+ #
+ # You can also specify an optional :class_name option that'll raise an exception if a serialized object is retrieved as a
+ # descendent of a class not in the hierarchy. Example:
+ #
+ # class User < ActiveRecord::Base
+ # serialize :preferences, :class_name => "Hash"
+ # end
+ #
+ # user = User.create("preferences" => %w( one two three ))
+ # User.find(user.id).preferences # raises SerializationTypeMismatch
+ #
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by default is called "type" (can be changed
+ # by overwriting <tt>Base.inheritance_column</tt>). This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do Firm.create("name" => "37signals"), this record with be saved in the companies table with type = "Firm". You can then
+ # fetch this row again using Company.find_first "name = '37signals'" and it will return a Firm object.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
+ # == Connection to multiple databases in different models
+ #
+ # Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection.
+ # All classes inheriting from ActiveRecord::Base will use this connection. But you can also set a class-specific connection.
+ # For example, if Course is a ActiveRecord::Base, but resides in a different database you can just say Course.establish_connection
+ # and Course *and all its subclasses* will use this connection instead.
+ #
+ # This feature is implemented by keeping a connection pool in ActiveRecord::Base that is a Hash indexed by the class. If a connection is
+ # requested, the retrieve_connection method will go up the class-hierarchy until a connection is found in the connection pool.
+ #
+ # == Exceptions
+ #
+ # * +ActiveRecordError+ -- generic error class and superclass of all other errors raised by Active Record
+ # * +AdapterNotSpecified+ -- the configuration hash used in <tt>establish_connection</tt> didn't include a
+ # <tt>:adapter</tt> key.
+ # * +AdapterNotSpecified+ -- the <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified an unexisting adapter
+ # (or a bad spelling of an existing one).
+ # * +AssociationTypeMismatch+ -- the object assigned to the association wasn't of the type specified in the association definition.
+ # * +SerializationTypeMismatch+ -- the object serialized wasn't of the class specified in the <tt>:class_name</tt> option of
+ # the serialize definition.
+ # * +ConnectionNotEstablished+ -- no connection has been established. Use <tt>establish_connection</tt> before querying.
+ # * +RecordNotFound+ -- no record responded to the find* method.
+ # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
+ # * +StatementInvalid+ -- the database server rejected the SQL statement. The precise error is added in the message.
+ # Either the record with the given ID doesn't exist or the record didn't meet the additional restrictions.
+ #
+ # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
+ # So it's possible to assign a logger to the class through Base.logger= which will then be used by all
+ # instances in the current object space.
+ class Base
+ include ClassInheritableAttributes
+
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
+ # on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
+ cattr_accessor :logger
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work unrelated
+ # to any of the specific Active Records.
+ def self.connection
+ retrieve_connection
+ end
+
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work that isn't
+ # easily done without going straight to SQL.
+ def connection
+ self.class.connection
+ end
+
+ def self.inherited(child) #:nodoc:
+ @@subclasses[self] ||= []
+ @@subclasses[self] << child
+ super
+ end
+
+ @@subclasses = {}
+
+ cattr_accessor :configurations
+ @@primary_key_prefix_type = {}
+
+ # Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and
+ # :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as
+ # the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember
+ # that this is a global setting for all Active Records.
+ cattr_accessor :primary_key_prefix_type
+ @@primary_key_prefix_type = nil
+
+ # Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
+ # table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convinient way of creating a namespace
+ # for tables in a shared database. By default, the prefix is the empty string.
+ cattr_accessor :table_name_prefix
+ @@table_name_prefix = ""
+
+ # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
+ # "people_basecamp"). By default, the suffix is the empty string.
+ cattr_accessor :table_name_suffix
+ @@table_name_suffix = ""
+
+ # Indicate whether or not table names should be the pluralized versions of the corresponding class names.
+ # If true, this the default table name for a +Product+ class will be +products+. If false, it would just be +product+.
+ # See table_name for the full rules on table/class naming. This is true, by default.
+ cattr_accessor :pluralize_table_names
+ @@pluralize_table_names = true
+
+ # When turned on (which is default), all associations are included using "load". This mean that any change is instant in cached
+ # environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
+ # be effective.
+ @@reload_associations = true
+ cattr_accessor :reload_associations
+
+ @@associations_loaded = []
+ cattr_accessor :associations_loaded
+
+ class << self # Class methods
+ # Returns objects for the records responding to either a specific id (1), a list of ids (1, 5, 6) or an array of ids.
+ # If only one ID is specified, that object is returned directly. If more than one ID is specified, an array is returned.
+ # Examples:
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # +RecordNotFound+ is raised if no record can be found.
+ def find(*ids)
+ ids = ids.flatten.compact.uniq
+
+ if ids.length > 1
+ ids_list = ids.map{ |id| "'#{sanitize(id)}'" }.join(", ")
+ objects = find_all("#{primary_key} IN (#{ids_list})", primary_key)
+
+ if objects.length == ids.length
+ return objects
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID in (#{ids_list})"
+ end
+ elsif ids.length == 1
+ id = ids.first
+ sql = "SELECT * FROM #{table_name} WHERE #{primary_key} = '#{sanitize(id)}'"
+ sql << " AND #{type_condition}" unless descends_from_active_record?
+
+ if record = connection.select_one(sql, "#{name} Find")
+ instantiate(record)
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID = #{id}"
+ end
+ else
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
+ end
+ end
+
+ # Works like find, but the record matching +id+ must also meet the +conditions+.
+ # +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
+ # Example:
+ # Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
+ def find_on_conditions(id, conditions)
+ find_first("#{primary_key} = '#{sanitize(id)}' AND #{sanitize_conditions(conditions)}") ||
+ raise(RecordNotFound, "Couldn't find #{name} with #{primary_key} = #{id} on the condition of #{conditions}")
+ end
+
+ # Returns an array of all the objects that could be instantiated from the associated
+ # table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
+ # such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
+ # such as by "last_name, first_name DESC". A maximum of returned objects can be specified in +limit+. Example:
+ # Project.find_all "category = 'accounts'", "last_accessed DESC", 15
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
+ sql = "SELECT * FROM #{table_name} "
+ sql << "#{joins} " if joins
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+ sql << "LIMIT #{limit} " unless limit.nil?
+
+ find_by_sql(sql)
+ end
+
+ # Works like find_all, but requires a complete SQL string. Example:
+ # Post.find_by_sql "SELECT p.*, c.author FROM posts p, comments c WHERE p.id = c.post_id"
+ def find_by_sql(sql)
+ connection.select_all(sql, "#{name} Load").inject([]) { |objects, record| objects << instantiate(record) }
+ end
+
+ # Returns the object for the first record responding to the conditions in +conditions+,
+ # such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
+ # be used to create the object. In such cases, it might be beneficial to also specify
+ # +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
+ # Employee.find_first "income > 50000", "income DESC, name"
+ def find_first(conditions = nil, orderings = nil)
+ sql = "SELECT * FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+ sql << "LIMIT 1"
+
+ record = connection.select_one(sql, "#{name} Load First")
+ instantiate(record) unless record.nil?
+ end
+
+ # Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
+ # fail under validations, the unsaved object is still returned.
+ def create(attributes = nil)
+ object = new(attributes)
+ object.save
+ object
+ end
+
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
+ # and returns it. If the save fail under validations, the unsaved object is still returned.
+ def update(id, attributes)
+ object = find(id)
+ object.attributes = attributes
+ object.save
+ object
+ end
+
+ # Updates all records with the SET-part of an SQL update statement in +updates+. A subset of the records can be selected
+ # by specifying +conditions+. Example:
+ # Billing.update_all "category = 'authorized', approved = 1", "author = 'David'"
+ def update_all(updates, conditions = nil)
+ sql = "UPDATE #{table_name} SET #{updates} "
+ add_conditions!(sql, conditions)
+ connection.update(sql, "#{name} Update")
+ end
+
+ # Destroys the objects for all the records that matches the +condition+ by instantiating each object and calling
+ # the destroy method. Example:
+ # Person.destroy_all "last_login < '2004-04-04'"
+ def destroy_all(conditions = nil)
+ find_all(conditions).each { |object| object.destroy }
+ end
+
+ # Deletes all the records that matches the +condition+ without instantiating the objects first (and hence not
+ # calling the destroy method). Example:
+ # Post.destroy_all "person_id = 5 AND (category = 'Something' OR category = 'Else')"
+ def delete_all(conditions = nil)
+ sql = "DELETE FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ connection.delete(sql, "#{name} Delete all")
+ end
+
+ # Returns the number of records that meets the +conditions+. Zero is returned if no records match. Example:
+ # Product.count "sales > 1"
+ def count(conditions = nil)
+ sql = "SELECT COUNT(*) FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ count_by_sql(sql)
+ end
+
+ # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
+ # Product.count "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
+ def count_by_sql(sql)
+ count = connection.select_one(sql, "#{name} Count").values.first
+ return count ? count.to_i : 0
+ end
+
+ # Increments the specified counter by one. So <tt>DiscussionBoard.increment_counter("post_count",
+ # discussion_board_id)</tt> would increment the "post_count" counter on the board responding to discussion_board_id.
+ # This is used for caching aggregate values, so that they doesn't need to be computed every time. Especially important
+ # for looping over a collection where each element require a number of aggregate values. Like the DiscussionBoard
+ # that needs to list both the number of posts and comments.
+ def increment_counter(counter_name, id)
+ update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{id}"
+ end
+
+ # Works like increment_counter, but decrements instead.
+ def decrement_counter(counter_name, id)
+ update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{id}"
+ end
+
+ # Attributes named in this macro are protected from mass-assignment, such as <tt>new(attributes)</tt> and
+ # <tt>attributes=(attributes)</tt>. Their assignment will simply be ignored. Instead, you can use the direct writer
+ # methods to do assignment. This is meant to protect sensitive attributes to be overwritten by URL/form hackers. Example:
+ #
+ # class Customer < ActiveRecord::Base
+ # attr_protected :credit_rating
+ # end
+ #
+ # customer = Customer.new("name" => David, "credit_rating" => "Excellent")
+ # customer.credit_rating # => nil
+ # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
+ # customer.credit_rating # => nil
+ #
+ # customer.credit_rating = "Average"
+ # customer.credit_rating # => "Average"
+ def attr_protected(*attributes)
+ write_inheritable_array("attr_protected", attributes)
+ end
+
+ # Returns an array of all the attributes that have been protected from mass-assigment.
+ def protected_attributes # :nodoc:
+ read_inheritable_attribute("attr_protected")
+ end
+
+ # If this macro is used, only those attributed named in it will be accessible for mass-assignment, such as
+ # <tt>new(attributes)</tt> and <tt>attributes=(attributes)</tt>. This is the more conservative choice for mass-assignment
+ # protection. If you'd rather start from an all-open default and restrict attributes as needed, have a look at
+ # attr_protected.
+ def attr_accessible(*attributes)
+ write_inheritable_array("attr_accessible", attributes)
+ end
+
+ # Returns an array of all the attributes that have been made accessible to mass-assigment.
+ def accessible_attributes # :nodoc:
+ read_inheritable_attribute("attr_accessible")
+ end
+
+ # Specifies that the attribute by the name of +attr_name+ should be serialized before saving to the database and unserialized
+ # after loading from the database. The serialization is done through YAML. If +class_name+ is specified, the serialized
+ # object must be of that class on retrival or +SerializationTypeMismatch+ will be raised.
+ def serialize(attr_name, class_name = Object)
+ write_inheritable_attribute("attr_serialized", serialized_attributes.update(attr_name.to_s => class_name))
+ end
+
+ # Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
+ def serialized_attributes
+ read_inheritable_attribute("attr_serialized") || { }
+ end
+
+ # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
+ # directly from ActiveRecord. So if the hierarchy looks like: Reply < Message < ActiveRecord, then Message is used
+ # to guess the table name from even when called on Reply. The guessing rules are as follows:
+ #
+ # * Class name ends in "x", "ch" or "ss": "es" is appended, so a Search class becomes a searches table.
+ # * Class name ends in "y" preceded by a consonant or "qu": The "y" is replaced with "ies", so a Category class becomes a categories table.
+ # * Class name ends in "fe": The "fe" is replaced with "ves", so a Wife class becomes a wives table.
+ # * Class name ends in "lf" or "rf": The "f" is replaced with "ves", so a Half class becomes a halves table.
+ # * Class name ends in "person": The "person" is replaced with "people", so a Salesperson class becomes a salespeople table.
+ # * Class name ends in "man": The "man" is replaced with "men", so a Spokesman class becomes a spokesmen table.
+ # * Class name ends in "sis": The "i" is replaced with an "e", so a Basis class becomes a bases table.
+ # * Class name ends in "tum" or "ium": The "um" is replaced with an "a", so a Datum class becomes a data table.
+ # * Class name ends in "child": The "child" is replaced with "children", so a NodeChild class becomes a node_children table.
+ # * Class name ends in an "s": No additional characters are added or removed.
+ # * Class name doesn't end in "s": An "s" is appended, so a Comment class becomes a comments table.
+ # * Class name with word compositions: Compositions are underscored, so CreditCard class becomes a credit_cards table.
+ #
+ # Additionally, the class-level table_name_prefix is prepended to the table_name and the table_name_suffix is appended.
+ # So if you have "myapp_" as a prefix, the table name guess for an Account class becomes "myapp_accounts".
+ #
+ # You can also overwrite this class method to allow for unguessable links, such as a Mouse class with a link to a
+ # "mice" table. Example:
+ #
+ # class Mouse < ActiveRecord::Base
+ # def self.table_name() "mice" end
+ # end
+ def table_name(class_name = nil)
+ if class_name.nil?
+ class_name = class_name_of_active_record_descendant(self)
+ table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
+ else
+ table_name_prefix + undecorated_table_name(class_name) + table_name_suffix
+ end
+ end
+
+ # Defines the primary key field -- can be overridden in subclasses. Overwritting will negate any effect of the
+ # primary_key_prefix_type setting, though.
+ def primary_key
+ case primary_key_prefix_type
+ when :table_name
+ Inflector.foreign_key(class_name_of_active_record_descendant(self), false)
+ when :table_name_with_underscore
+ Inflector.foreign_key(class_name_of_active_record_descendant(self))
+ else
+ "id"
+ end
+ end
+
+ # Defines the column name for use with single table inheritance -- can be overridden in subclasses.
+ def inheritance_column
+ "type"
+ end
+
+ # Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
+ def class_name(table_name = table_name) # :nodoc:
+ # remove any prefix and/or suffix from the table name
+ class_name = Inflector.camelize(table_name[table_name_prefix.length..-(table_name_suffix.length + 1)])
+ class_name = Inflector.singularize(class_name) if pluralize_table_names
+ return class_name
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= connection.columns(table_name, "#{name} Columns")
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
+ end
+
+ # Returns an array of columns objects where the primary id, all columns ending in "_id" or "_count",
+ # and columns used for single table inheritance has been removed.
+ def content_columns
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
+ end
+
+ # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
+ # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
+ # is available.
+ def column_methods_hash
+ @dynamic_methods_hash ||= columns_hash.keys.inject(Hash.new(false)) do |methods, attr|
+ methods[attr.to_sym] = true
+ methods["#{attr}=".to_sym] = true
+ methods["#{attr}?".to_sym] = true
+ methods
+ end
+ end
+
+ # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
+ # Person.human_attribute_name("first_name") # => "First name"
+ def human_attribute_name(attribute_key_name)
+ attribute_key_name.gsub(/_/, " ").capitalize unless attribute_key_name.nil?
+ end
+
+ def descends_from_active_record? # :nodoc:
+ superclass == Base
+ end
+
+ # Used to sanitize objects before they're used in an SELECT SQL-statement.
+ def sanitize(object) # :nodoc:
+ return object if Fixnum === object
+ object.to_s.gsub(/([;:])/, "").gsub('##', '\#\#').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ # Used to aggregate logging and benchmark, so you can measure and represent multiple statements in a single block.
+ # Usage (hides all the SQL calls for the individual actions and calculates total runtime for them all):
+ #
+ # Project.benchmark("Creating project") do
+ # project = Project.create("name" => "stuff")
+ # project.create_manager("name" => "David")
+ # project.milestones << Milestone.find_all
+ # end
+ def benchmark(title)
+ result = nil
+ logger.level = Logger::ERROR
+ bm = Benchmark.measure { result = yield }
+ logger.level = Logger::DEBUG
+ logger.info "#{title} (#{sprintf("%f", bm.real)})"
+ return result
+ end
+
+ # Loads the <tt>file_name</tt> if reload_associations is true or requires if it's false.
+ def require_or_load(file_name)
+ if !associations_loaded.include?(file_name)
+ associations_loaded << file_name
+ reload_associations ? load("#{file_name}.rb") : require(file_name)
+ end
+ end
+
+ # Resets the list of dependencies loaded (typically to be called by the end of a request), so when require_or_load is
+ # called for that dependency it'll be loaded anew.
+ def reset_associations_loaded
+ associations_loaded = []
+ end
+
+ private
+ # Finder methods must instantiate through this method to work with the single-table inheritance model
+ # that makes it possible to create objects of different types from the same table.
+ def instantiate(record)
+ object = record_with_type?(record) ? compute_type(record[inheritance_column]).allocate : allocate
+ object.instance_variable_set("@attributes", record)
+ return object
+ end
+
+ # Returns true if the +record+ has a single table inheritance column and is using it.
+ def record_with_type?(record)
+ record.include?(inheritance_column) && !record[inheritance_column].nil? &&
+ !record[inheritance_column].empty?
+ end
+
+ # Returns the name of the type of the record using the current module as a prefix. So descendents of
+ # MyApp::Business::Account would be appear as "MyApp::Business::AccountSubclass".
+ def type_name_with_module(type_name)
+ self.name =~ /::/ ? self.name.scan(/(.*)::/).first.first + "::" + type_name : type_name
+ end
+
+ # Adds a sanitized version of +conditions+ to the +sql+ string. Note that it's the passed +sql+ string is changed.
+ def add_conditions!(sql, conditions)
+ sql << "WHERE #{sanitize_conditions(conditions)} " unless conditions.nil?
+ sql << (conditions.nil? ? "WHERE " : " AND ") + type_condition unless descends_from_active_record?
+ end
+
+ def type_condition
+ " (" + subclasses.inject("#{inheritance_column} = '#{Inflector.demodulize(name)}' ") do |condition, subclass|
+ condition << "OR #{inheritance_column} = '#{Inflector.demodulize(subclass.name)}'"
+ end + ") "
+ end
+
+ # Guesses the table name, but does not decorate it with prefix and suffix information.
+ def undecorated_table_name(class_name = class_name_of_active_record_descendant(self))
+ table_name = Inflector.underscore(Inflector.demodulize(class_name))
+ table_name = Inflector.pluralize(table_name) if pluralize_table_names
+ return table_name
+ end
+
+
+ protected
+ def subclasses
+ @@subclasses[self] ||= []
+ @@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
+ end
+
+ # Returns the class type of the record using the current module as a prefix. So descendents of
+ # MyApp::Business::Account would be appear as MyApp::Business::AccountSubclass.
+ def compute_type(type_name)
+ type_name_with_module(type_name).split("::").inject(Object) do |final_type, part|
+ final_type = final_type.const_get(part)
+ end
+ end
+
+ # Returns the name of the class descending directly from ActiveRecord in the inheritance hierarchy.
+ def class_name_of_active_record_descendant(klass)
+ if klass.superclass == Base
+ return klass.name
+ elsif klass.superclass.nil?
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
+ else
+ class_name_of_active_record_descendant(klass.superclass)
+ end
+ end
+
+ # Accepts either a condition array or string. The string is returned untouched, but the array has each of
+ # the condition values sanitized.
+ def sanitize_conditions(conditions)
+ if Array === conditions
+ statement, values = conditions[0], conditions[1..-1]
+ values.collect! { |value| sanitize(value) }
+ conditions = statement % values
+ end
+
+ return conditions
+ end
+ end
+
+ public
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
+ # attributes but not yet saved (pass a hash with key names matching the associated table column names).
+ # In both instances, valid attribute keys are determined by the column names of the associated table --
+ # hence you can't have attributes that aren't part of the table columns.
+ def initialize(attributes = nil)
+ @attributes = attributes_from_column_definition
+ @new_record = true
+ ensure_proper_type
+ self.attributes = attributes unless attributes.nil?
+ yield self if block_given?
+ end
+
+ # Every Active Record class must use "id" as their primary ID. This getter overwrites the native
+ # id method, which isn't being used in this context.
+ def id
+ read_attribute(self.class.primary_key)
+ end
+
+ # Sets the primary ID.
+ def id=(value)
+ write_attribute(self.class.primary_key, value)
+ end
+
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
+ def new_record?
+ @new_record
+ end
+
+ # * No record exists: Creates a new record with values matching those of the object attributes.
+ # * A record does exist: Updates the record with values matching those of the object attributes.
+ def save
+ create_or_update
+ return true
+ end
+
+ # Deletes the record in the database and freezes this instance to reflect that no changes should
+ # be made (since they can't be persisted).
+ def destroy
+ unless new_record?
+ connection.delete(
+ "DELETE FROM #{self.class.table_name} " +
+ "WHERE #{self.class.primary_key} = '#{id}'",
+ "#{self.class.name} Destroy"
+ )
+ end
+
+ freeze
+ end
+
+ # Returns a clone of the record that hasn't been assigned an id yet and is treated as a new record.
+ def clone
+ attr = Hash.new
+
+ self.attribute_names.each do |name|
+ begin
+ attr[name] = read_attribute(name).clone
+ rescue TypeError
+ attr[name] = read_attribute(name)
+ end
+ end
+
+ cloned_record = self.class.new(attr)
+ cloned_record.instance_variable_set "@new_record", true
+ cloned_record.id = nil
+ cloned_record
+ end
+
+ # Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
+ def update_attribute(name, value)
+ self[name] = value
+ save
+ end
+
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ # (Alias for the protected read_attribute method).
+ def [](attr_name)
+ read_attribute(attr_name)
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
+ # (Alias for the protected write_attribute method).
+ def []= (attr_name, value)
+ write_attribute(attr_name, value)
+ end
+
+ # Allows you to set all the attributes at once by passing in a hash with keys
+ # matching the attribute names (which again matches the column names). Sensitive attributes can be protected
+ # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
+ # specify which attributes *can* be accessed in with the +attr_accessible+ macro. Then all the
+ # attributes not included in that won't be allowed to be mass-assigned.
+ def attributes=(attributes)
+ return if attributes.nil?
+
+ multi_parameter_attributes = []
+ remove_attributes_protected_from_mass_assignment(attributes).each do |k, v|
+ k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
+ end
+ assign_multiparameter_attributes(multi_parameter_attributes)
+ end
+
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
+ # nil nor empty? (the latter only applies to objects that responds to empty?, most notably Strings).
+ def attribute_present?(attribute)
+ is_empty = read_attribute(attribute).respond_to?("empty?") ? read_attribute(attribute).empty? : false
+ @attributes.include?(attribute) && !@attributes[attribute].nil? && !is_empty
+ end
+
+ # Returns an array of names for the attributes available on this object sorted alphabetically.
+ def attribute_names
+ @attributes.keys.sort
+ end
+
+ # Returns the column object for the named attribute.
+ def column_for_attribute(name)
+ self.class.columns_hash[name]
+ end
+
+ # Returns true if the +comparison_object+ is of the same type and has the same id.
+ def ==(comparison_object)
+ comparison_object.instance_of?(self.class) && comparison_object.id == id
+ end
+
+ # Delegates to ==
+ def eql?(comparison_object)
+ self == (comparison_object)
+ end
+
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
+ def hash
+ id
+ end
+
+ # For checking respond_to? without searching the attributes (which is faster).
+ alias_method :respond_to_without_attributes?, :respond_to?
+
+ # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
+ # person.respond_to?("name?") which will all return true.
+ def respond_to?(method)
+ self.class.column_methods_hash[method.to_sym] || respond_to_without_attributes?(method)
+ end
+
+ def require_or_load(file_name)
+ self.class.require_or_load(file_name)
+ end
+
+ private
+ def create_or_update
+ if new_record? then create else update end
+ end
+
+ # Updates the associated record with values matching those of the instant attributes.
+ def update
+ connection.update(
+ "UPDATE #{self.class.table_name} " +
+ "SET #{quoted_comma_pair_list(connection, attributes_with_quotes)} " +
+ "WHERE #{self.class.primary_key} = '#{id}'",
+ "#{self.class.name} Update"
+ )
+ end
+
+ # Creates a new record with values matching those of the instant attributes.
+ def create
+ self.id = connection.insert(
+ "INSERT INTO #{self.class.table_name} " +
+ "(#{quoted_column_names.join(', ')}) " +
+ "VALUES(#{attributes_with_quotes.values.join(', ')})",
+ "#{self.class.name} Create",
+ self.class.primary_key, self.id
+ )
+
+ @new_record = false
+ end
+
+ # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendant.
+ # Considering the hierarchy Reply < Message < ActiveRecord, this makes it possible to do Reply.new without having to
+ # set Reply[Reply.inheritance_column] = "Reply" yourself. No such attribute would be set for objects of the
+ # Message class in that example.
+ def ensure_proper_type
+ unless self.class.descends_from_active_record?
+ write_attribute(self.class.inheritance_column, Inflector.demodulize(self.class.name))
+ end
+ end
+
+ # Allows access to the object attributes, which are held in the @attributes hash, as were
+ # they first-class methods. So a Person class with a name attribute can use Person#name and
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
+ # the completed attribute is not nil or 0.
+ #
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
+ # table with a master_id foreign key can instantiate master through Client#master.
+ def method_missing(method_id, *arguments)
+ method_name = method_id.id2name
+
+
+
+ if method_name =~ read_method? && @attributes.include?($1)
+ return read_attribute($1)
+ elsif method_name =~ write_method? && @attributes.include?($1)
+ write_attribute($1, arguments[0])
+ elsif method_name =~ query_method? && @attributes.include?($1)
+ return query_attribute($1)
+ else
+ super
+ end
+ end
+
+ def read_method?() /^([a-zA-Z][-_\w]*)[^=?]*$/ end
+ def write_method?() /^([a-zA-Z][-_\w]*)=.*$/ end
+ def query_method?() /^([a-zA-Z][-_\w]*)\?$/ end
+
+ # Returns the value of attribute identified by <tt>attr_name</tt> after it has been type cast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ def read_attribute(attr_name) #:doc:
+ if @attributes.keys.include? attr_name
+ if column = column_for_attribute(attr_name)
+ @attributes[attr_name] = unserializable_attribute?(attr_name, column) ?
+ unserialize_attribute(attr_name) : column.type_cast(@attributes[attr_name])
+ end
+
+ @attributes[attr_name]
+ else
+ nil
+ end
+ end
+
+ # Returns true if the attribute is of a text column and marked for serialization.
+ def unserializable_attribute?(attr_name, column)
+ @attributes[attr_name] && column.send(:type) == :text && @attributes[attr_name].is_a?(String) && self.class.serialized_attributes[attr_name]
+ end
+
+ # Returns the unserialized object of the attribute.
+ def unserialize_attribute(attr_name)
+ unserialized_object = object_from_yaml(@attributes[attr_name])
+
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name])
+ @attributes[attr_name] = unserialized_object
+ else
+ raise(
+ SerializationTypeMismatch,
+ "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, " +
+ "but was a #{unserialized_object.class.to_s}"
+ )
+ end
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
+ # columns are turned into nil.
+ def write_attribute(attr_name, value) #:doc:
+ @attributes[attr_name] = empty_string_for_number_column?(attr_name, value) ? nil : value
+ end
+
+ def empty_string_for_number_column?(attr_name, value)
+ column = column_for_attribute(attr_name)
+ column && (column.klass == Fixnum || column.klass == Float) && value == ""
+ end
+
+ def query_attribute(attr_name)
+ attribute = @attributes[attr_name]
+ if attribute.kind_of?(Fixnum) && attribute == 0
+ false
+ elsif attribute.kind_of?(String) && attribute == "0"
+ false
+ elsif attribute.kind_of?(String) && attribute.empty?
+ false
+ elsif attribute.nil?
+ false
+ elsif attribute == false
+ false
+ elsif attribute == "f"
+ false
+ elsif attribute == "false"
+ false
+ else
+ true
+ end
+ end
+
+ def remove_attributes_protected_from_mass_assignment(attributes)
+ if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
+ attributes.reject { |key, value| key == self.class.primary_key }
+ elsif self.class.protected_attributes.nil?
+ attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.intern) || key == self.class.primary_key }
+ elsif self.class.accessible_attributes.nil?
+ attributes.reject { |key, value| self.class.protected_attributes.include?(key.intern) || key == self.class.primary_key }
+ end
+ end
+
+ # Returns copy of the attributes hash where all the values have been safely quoted for use in
+ # an SQL statement.
+ def attributes_with_quotes
+ columns_hash = self.class.columns_hash
+ @attributes.inject({}) do |attrs_quoted, pair|
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
+ attrs_quoted
+ end
+ end
+
+ # Quote strings appropriately for SQL statements.
+ def quote(value, column = nil)
+ connection.quote(value, column)
+ end
+
+ # Interpolate custom sql string in instance context.
+ # Optional record argument is meant for custom insert_sql.
+ def interpolate_sql(sql, record = nil)
+ instance_eval("%(#{sql})")
+ end
+
+ # Initializes the attributes array with keys matching the columns from the linked table and
+ # the values matching the corresponding default value of that column, so
+ # that a new instance, or one populated from a passed-in Hash, still has all the attributes
+ # that instances loaded from the database would.
+ def attributes_from_column_definition
+ connection.columns(self.class.table_name, "#{self.class.name} Columns").inject({}) do |attributes, column|
+ attributes[column.name] = column.default unless column.name == self.class.primary_key
+ attributes
+ end
+ end
+
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
+ # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
+ # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
+ # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
+ # parenteses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
+ # s for String, and a for Array. If all the values for a given attribute is empty, the attribute will be set to nil.
+ def assign_multiparameter_attributes(pairs)
+ execute_callstack_for_multiparameter_attributes(
+ extract_callstack_for_multiparameter_attributes(pairs)
+ )
+ end
+
+ # Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ callstack.each do |name, values|
+ klass = (self.class.reflect_on_aggregation(name) || column_for_attribute(name)).klass
+ if values.empty?
+ send(name + "=", nil)
+ else
+ send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values))
+ end
+ end
+ end
+
+ def extract_callstack_for_multiparameter_attributes(pairs)
+ attributes = { }
+
+ for pair in pairs
+ multiparameter_name, value = pair
+ attribute_name = multiparameter_name.split("(").first
+ attributes[attribute_name] = [] unless attributes.include?(attribute_name)
+
+ unless value.empty?
+ attributes[attribute_name] <<
+ [find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value)]
+ end
+ end
+
+ attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
+ end
+
+ def type_cast_attribute_value(multiparameter_name, value)
+ multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value
+ end
+
+ def find_parameter_position(multiparameter_name)
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
+ end
+
+ # Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
+ def comma_pair_list(hash)
+ hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
+ end
+
+ def quoted_column_names(attributes = attributes_with_quotes)
+ attributes.keys.collect { |column_name| connection.quote_column_name(column_name) }
+ end
+
+ def quote_columns(column_quoter, hash)
+ hash.inject({}) {|list, pair|
+ list[column_quoter.quote_column_name(pair.first)] = pair.last
+ list
+ }
+ end
+
+ def quoted_comma_pair_list(column_quoter, hash)
+ comma_pair_list(quote_columns(column_quoter, hash))
+ end
+
+ def object_from_yaml(string)
+ return string unless String === string
+ if has_yaml_encoding_header?(string)
+ begin
+ YAML::load(string)
+ rescue Object
+ # Apparently wasn't YAML anyway
+ string
+ end
+ else
+ string
+ end
+ end
+
+ def has_yaml_encoding_header?(string)
+ string[0..3] == "--- "
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
new file mode 100755
index 0000000000..fc013ba743
--- /dev/null
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -0,0 +1,337 @@
+require 'observer'
+
+module ActiveRecord
+ # Callbacks are hooks into the lifecycle of an Active Record object that allows you to trigger logic
+ # before or after an alteration of the object state. This can be used to make sure that associated and
+ # dependent objects are deleted when destroy is called (by overwriting before_destroy) or to massage attributes
+ # before they're validated (by overwriting before_validation). As an example of the callbacks initiated, consider
+ # the Base#save call:
+ #
+ # * (-) save
+ # * (-) valid?
+ # * (1) before_validation
+ # * (2) before_validation_on_create
+ # * (-) validate
+ # * (-) validate_on_create
+ # * (4) after_validation
+ # * (5) after_validation_on_create
+ # * (6) before_save
+ # * (7) before_create
+ # * (-) create
+ # * (8) after_create
+ # * (9) after_save
+ #
+ # That's a total of nine callbacks, which gives you immense power to react and prepare for each state in the
+ # Active Record lifecyle.
+ #
+ # Examples:
+ # class CreditCard < ActiveRecord::Base
+ # # Strip everything but digits, so the user can specify "555 234 34" or
+ # # "5552-3434" or both will mean "55523434"
+ # def before_validation_on_create
+ # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
+ # end
+ # end
+ #
+ # class Subscription < ActiveRecord::Base
+ # # Automatically assign the signup date
+ # def before_create
+ # self.signed_up_on = Date.today
+ # end
+ # end
+ #
+ # class Firm < ActiveRecord::Base
+ # # Destroys the associated clients and people when the firm is destroyed
+ # def before_destroy
+ # Client.destroy_all "client_of = #{id}"
+ # Person.destroy_all "firm_id = #{id}"
+ # end
+ #
+ # == Inheritable callback queues
+ #
+ # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
+ # Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
+ # hierarchy. Example:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy :destroy_author
+ # end
+ #
+ # class Reply < Topic
+ # before_destroy :destroy_readers
+ # end
+ #
+ # Now, when Topic#destroy is run only +destroy_author+ is called. When Reply#destroy is run both +destroy_author+ and
+ # +destroy_readers+ is called. Contrast this to the situation where we've implemented the save behavior through overwriteable
+ # methods:
+ #
+ # class Topic < ActiveRecord::Base
+ # def before_destroy() destroy_author end
+ # end
+ #
+ # class Reply < Topic
+ # def before_destroy() destroy_readers end
+ # end
+ #
+ # In that case, Reply#destroy would only run +destroy_readers+ and _not_ +destroy_author+. So use the callback macros when
+ # you want to ensure that a certain callback is called for the entire hierarchy and the regular overwriteable methods when you
+ # want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
+ #
+ # == Types of callbacks
+ #
+ # There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
+ # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the
+ # recommended approaches, inline methods using a proc is some times appropriate (such as for creating mix-ins), and inline
+ # eval methods are deprecated.
+ #
+ # The method reference callbacks work by specifying a protected or private method available in the object, like this:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy :delete_parents
+ #
+ # private
+ # def delete_parents
+ # self.class.delete_all "parent_id = #{id}"
+ # end
+ # end
+ #
+ # The callback objects have methods named after the callback called with the record as the only parameter, such as:
+ #
+ # class BankAccount < ActiveRecord::Base
+ # before_save EncryptionWrapper.new("credit_card_number")
+ # after_save EncryptionWrapper.new("credit_card_number")
+ # after_initialize EncryptionWrapper.new("credit_card_number")
+ # end
+ #
+ # class EncryptionWrapper
+ # def initialize(attribute)
+ # @attribute = attribute
+ # end
+ #
+ # def before_save(record)
+ # record.credit_card_number = encrypt(record.credit_card_number)
+ # end
+ #
+ # def after_save(record)
+ # record.credit_card_number = decrypt(record.credit_card_number)
+ # end
+ #
+ # alias_method :after_initialize, :after_save
+ #
+ # private
+ # def encrypt(value)
+ # # Secrecy is committed
+ # end
+ #
+ # def decrypt(value)
+ # # Secrecy is unvieled
+ # end
+ # end
+ #
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # a method by the name of the callback messaged.
+ #
+ # The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
+ # which will then be evaluated within the binding of the callback. Example:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy 'self.class.delete_all "parent_id = #{id}"'
+ # end
+ #
+ # Notice that single plings (') are used so the #{id} part isn't evaluated until the callback is triggered. Also note that these
+ # inline callbacks can be stacked just like the regular ones:
+ #
+ # class Topic < ActiveRecord::Base
+ # before_destroy 'self.class.delete_all "parent_id = #{id}"',
+ # 'puts "Evaluated after parents are destroyed"'
+ # end
+ #
+ # == The after_find and after_initialize exceptions
+ #
+ # Because after_find and after_initialize is called for each object instantiated found by a finder, such as Base.find_all, we've had
+ # to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
+ # after_initialize can only be declared using an explicit implementation. So using the inheritable callback queue for after_find and
+ # after_initialize won't work.
+ module Callbacks
+ CALLBACKS = %w(
+ after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
+ after_validation before_validation_on_create after_validation_on_create before_validation_on_update
+ after_validation_on_update before_destroy after_destroy
+ )
+
+ def self.append_features(base) #:nodoc:
+ super
+
+ base.extend(ClassMethods)
+ base.class_eval do
+ class << self
+ include Observable
+ alias_method :instantiate_without_callbacks, :instantiate
+ alias_method :instantiate, :instantiate_with_callbacks
+ end
+ end
+
+ base.class_eval do
+ alias_method :initialize_without_callbacks, :initialize
+ alias_method :initialize, :initialize_with_callbacks
+
+ alias_method :create_or_update_without_callbacks, :create_or_update
+ alias_method :create_or_update, :create_or_update_with_callbacks
+
+ alias_method :valid_without_callbacks, :valid?
+ alias_method :valid?, :valid_with_callbacks
+
+ alias_method :create_without_callbacks, :create
+ alias_method :create, :create_with_callbacks
+
+ alias_method :update_without_callbacks, :update
+ alias_method :update, :update_with_callbacks
+
+ alias_method :destroy_without_callbacks, :destroy
+ alias_method :destroy, :destroy_with_callbacks
+ end
+
+ CALLBACKS.each { |cb| base.class_eval("def self.#{cb}(*methods) write_inheritable_array(\"#{cb}\", methods) end") }
+ end
+
+ module ClassMethods #:nodoc:
+ def instantiate_with_callbacks(record)
+ object = instantiate_without_callbacks(record)
+ object.callback(:after_find) if object.respond_to_without_attributes?(:after_find)
+ object.callback(:after_initialize) if object.respond_to_without_attributes?(:after_initialize)
+ object
+ end
+ end
+
+ # Is called when the object was instantiated by one of the finders, like Base.find.
+ # def after_find() end
+
+ # Is called after the object has been instantiated by a call to Base.new.
+ # def after_initialize() end
+ def initialize_with_callbacks(attributes = nil) #:nodoc:
+ initialize_without_callbacks(attributes)
+ yield self if block_given?
+ after_initialize if respond_to_without_attributes?(:after_initialize)
+ end
+
+ # Is called _before_ Base.save (regardless of whether it's a create or update save).
+ def before_save() end
+
+ # Is called _after_ Base.save (regardless of whether it's a create or update save).
+ def after_save() end
+ def create_or_update_with_callbacks #:nodoc:
+ callback(:before_save)
+ create_or_update_without_callbacks
+ callback(:after_save)
+ end
+
+ # Is called _before_ Base.save on new objects that haven't been saved yet (no record exists).
+ def before_create() end
+
+ # Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
+ def after_create() end
+ def create_with_callbacks #:nodoc:
+ callback(:before_create)
+ create_without_callbacks
+ callback(:after_create)
+ end
+
+ # Is called _before_ Base.save on existing objects that has a record.
+ def before_update() end
+
+ # Is called _after_ Base.save on existing objects that has a record.
+ def after_update() end
+
+ def update_with_callbacks #:nodoc:
+ callback(:before_update)
+ update_without_callbacks
+ callback(:after_update)
+ end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call).
+ def before_validation() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call).
+ def after_validation() end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call) on new objects
+ # that haven't been saved yet (no record exists).
+ def before_validation_on_create() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call) on new objects
+ # that haven't been saved yet (no record exists).
+ def after_validation_on_create() end
+
+ # Is called _before_ Validations.validate (which is part of the Base.save call) on
+ # existing objects that has a record.
+ def before_validation_on_update() end
+
+ # Is called _after_ Validations.validate (which is part of the Base.save call) on
+ # existing objects that has a record.
+ def after_validation_on_update() end
+
+ def valid_with_callbacks #:nodoc:
+ callback(:before_validation)
+ if new_record? then callback(:before_validation_on_create) else callback(:before_validation_on_update) end
+
+ result = valid_without_callbacks
+
+ callback(:after_validation)
+ if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end
+
+ return result
+ end
+
+ # Is called _before_ Base.destroy.
+ def before_destroy() end
+
+ # Is called _after_ Base.destroy (and all the attributes have been frozen).
+ def after_destroy() end
+ def destroy_with_callbacks #:nodoc:
+ callback(:before_destroy)
+ destroy_without_callbacks
+ callback(:after_destroy)
+ end
+
+ def callback(callback_method) #:nodoc:
+ run_callbacks(callback_method)
+ send(callback_method)
+ notify(callback_method)
+ end
+
+ def run_callbacks(callback_method)
+ filters = self.class.read_inheritable_attribute(callback_method.to_s)
+ if filters.nil? then return end
+ filters.each do |filter|
+ if Symbol === filter
+ self.send(filter)
+ elsif String === filter
+ eval(filter, binding)
+ elsif filter_block?(filter)
+ filter.call(self)
+ elsif filter_class?(filter, callback_method)
+ filter.send(callback_method, self)
+ else
+ raise(
+ ActiveRecordError,
+ "Filters need to be either a symbol, string (to be eval'ed), proc/method, or " +
+ "class implementing a static filter method"
+ )
+ end
+ end
+ end
+
+ def filter_block?(filter)
+ filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1)
+ end
+
+ def filter_class?(filter, callback_method)
+ filter.respond_to?(callback_method)
+ end
+
+ def notify(callback_method) #:nodoc:
+ self.class.changed
+ self.class.notify_observers(callback_method, self)
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
new file mode 100755
index 0000000000..54fdfd25cd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -0,0 +1,371 @@
+require 'benchmark'
+require 'date'
+
+# Method that requires a library, ensuring that rubygems is loaded
+# This is used in the database adaptors to require DB drivers. Reasons:
+# (1) database drivers are the only third-party library that Rails depend upon
+# (2) they are often installed as gems
+def require_library_or_gem(library_name)
+ begin
+ require library_name
+ rescue LoadError => cannot_require
+ # 1. Requiring the module is unsuccessful, maybe it's a gem and nobody required rubygems yet. Try.
+ begin
+ require 'rubygems'
+ rescue LoadError => rubygems_not_installed
+ raise cannot_require
+ end
+ # 2. Rubygems is installed and loaded. Try to load the library again
+ begin
+ require library_name
+ rescue LoadError => gem_not_installed
+ raise cannot_require
+ end
+ end
+end
+
+module ActiveRecord
+ class Base
+ class ConnectionSpecification #:nodoc:
+ attr_reader :config, :adapter_method
+ def initialize (config, adapter_method)
+ @config, @adapter_method = config, adapter_method
+ end
+ end
+
+ # The class -> [adapter_method, config] map
+ @@defined_connections = {}
+
+ # Establishes the connection to the database. Accepts a hash as input where
+ # the :adapter key must be specified with the name of a database adapter (in lower-case)
+ # example for regular databases (MySQL, Postgresql, etc):
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "mysql",
+ # :host => "localhost",
+ # :username => "myuser",
+ # :password => "mypass",
+ # :database => "somedatabase"
+ # )
+ #
+ # Example for SQLite database:
+ #
+ # ActiveRecord::Base.establish_connection(
+ # :adapter => "sqlite",
+ # :dbfile => "path/to/dbfile"
+ # )
+ #
+ # Also accepts keys as strings (for parsing from yaml for example):
+ # ActiveRecord::Base.establish_connection(
+ # "adapter" => "sqlite",
+ # "dbfile" => "path/to/dbfile"
+ # )
+ #
+ # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # may be returned on an error.
+ #
+ # == Connecting to another database for a single model
+ #
+ # To support different connections for different classes, you can
+ # simply call establish_connection with the classes you wish to have
+ # different connections for:
+ #
+ # class Courses < ActiveRecord::Base
+ # ...
+ # end
+ #
+ # Courses.establish_connection( ... )
+ def self.establish_connection(spec)
+ if spec.instance_of? ConnectionSpecification
+ @@defined_connections[self] = spec
+ elsif spec.is_a?(Symbol)
+ establish_connection(configurations[spec.to_s])
+ else
+ if spec.nil? then raise AdapterNotSpecified end
+ symbolize_strings_in_hash(spec)
+ unless spec.key?(:adapter) then raise AdapterNotSpecified end
+
+ adapter_method = "#{spec[:adapter]}_connection"
+ unless methods.include?(adapter_method) then raise AdapterNotFound end
+ remove_connection
+ @@defined_connections[self] = ConnectionSpecification.new(spec, adapter_method)
+ end
+ end
+
+ # Locate the connection of the nearest super class. This can be an
+ # active or defined connections: if it is the latter, it will be
+ # opened and set as the active connection for the class it was defined
+ # for (not necessarily the current class).
+ def self.retrieve_connection #:nodoc:
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ Thread.current['active_connections'] ||= {}
+ if Thread.current['active_connections'][klass]
+ return Thread.current['active_connections'][klass]
+ elsif @@defined_connections[klass]
+ klass.connection = @@defined_connections[klass]
+ return self.connection
+ end
+ klass = klass.superclass
+ end
+ raise ConnectionNotEstablished
+ end
+
+ # Returns true if a connection that's accessible to this class have already been opened.
+ def self.connected?
+ klass = self
+ until klass == ActiveRecord::Base.superclass
+ if Thread.current['active_connections'].is_a?(Hash) && Thread.current['active_connections'][klass]
+ return true
+ else
+ klass = klass.superclass
+ end
+ end
+ return false
+ end
+
+ # Remove the connection for this class. This will close the active
+ # connection and the defined connection (if they exist). The result
+ # can be used as argument for establish_connection, for easy
+ # re-establishing of the connection.
+ def self.remove_connection(klass=self)
+ conn = @@defined_connections[klass]
+ @@defined_connections.delete(klass)
+ Thread.current['active_connections'] ||= {}
+ Thread.current['active_connections'][klass] = nil
+ conn.config if conn
+ end
+
+ # Set the connection for the class.
+ def self.connection=(spec)
+ raise ConnectionNotEstablished unless spec
+ conn = self.send(spec.adapter_method, spec.config)
+ Thread.current['active_connections'] ||= {}
+ Thread.current['active_connections'][self] = conn
+ end
+
+ # Converts all strings in a hash to symbols.
+ def self.symbolize_strings_in_hash(hash)
+ hash.each do |key, value|
+ if key.class == String
+ hash.delete key
+ hash[key.intern] = value
+ end
+ end
+ end
+ end
+
+ module ConnectionAdapters # :nodoc:
+ class Column # :nodoc:
+ attr_reader :name, :default, :type, :limit
+ # The name should contain the name of the column, such as "name" in "name varchar(250)"
+ # The default should contain the type-casted default of the column, such as 1 in "count int(11) DEFAULT 1"
+ # The type parameter should either contain :integer, :float, :datetime, :date, :text, or :string
+ # The sql_type is just used for extracting the limit, such as 10 in "varchar(10)"
+ def initialize(name, default, sql_type = nil)
+ @name, @default, @type = name, default, simplified_type(sql_type)
+ @limit = extract_limit(sql_type) unless sql_type.nil?
+ end
+
+ def default
+ type_cast(@default)
+ end
+
+ def klass
+ case type
+ when :integer then Fixnum
+ when :float then Float
+ when :datetime then Time
+ when :date then Date
+ when :text, :string then String
+ when :boolean then Object
+ end
+ end
+
+ def type_cast(value)
+ if value.nil? then return nil end
+ case type
+ when :string then value
+ when :text then value
+ when :integer then value.to_i
+ when :float then value.to_f
+ when :datetime then string_to_time(value)
+ when :date then string_to_date(value)
+ when :boolean then (value == "t" or value == true ? true : false)
+ else value
+ end
+ end
+
+ def human_name
+ Base.human_attribute_name(@name)
+ end
+
+ private
+ def string_to_date(string)
+ return string if Date === string
+ date_array = ParseDate.parsedate(string)
+ # treat 0000-00-00 as nil
+ Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
+ end
+
+ def string_to_time(string)
+ return string if Time === string
+ time_array = ParseDate.parsedate(string).compact
+ # treat 0000-00-00 00:00:00 as nil
+ Time.local(*time_array) rescue nil
+ end
+
+ def extract_limit(sql_type)
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double|decimal|numeric/i
+ :float
+ when /time/i
+ :datetime
+ when /date/i
+ :date
+ when /(c|b)lob/i, /text/i
+ :text
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+ end
+
+ # All the concrete database adapters follow the interface laid down in this class.
+ # You can use this interface directly by borrowing the database connection from the Base with
+ # Base.connection.
+ class AbstractAdapter
+ @@row_even = true
+
+ include Benchmark
+
+ def initialize(connection, logger = nil) # :nodoc:
+ @connection, @logger = connection, logger
+ @runtime = 0
+ end
+
+ # Returns an array of record hashes with the column names as a keys and fields as values.
+ def select_all(sql, name = nil) end
+
+ # Returns a record hash with the column names as a keys and fields as values.
+ def select_one(sql, name = nil) end
+
+ # Returns an array of column objects for the table specified by +table_name+.
+ def columns(table_name, name = nil) end
+
+ # Returns the last auto-generated ID from the affected table.
+ def insert(sql, name = nil, pk = nil, id_value = nil) end
+
+ # Executes the update statement.
+ def update(sql, name = nil) end
+
+ # Executes the delete statement.
+ def delete(sql, name = nil) end
+
+ def reset_runtime # :nodoc:
+ rt = @runtime
+ @runtime = 0
+ return rt
+ end
+
+ # Wrap a block in a transaction. Returns result of block.
+ def transaction
+ begin
+ if block_given?
+ begin_db_transaction
+ result = yield
+ commit_db_transaction
+ result
+ end
+ rescue Exception => database_transaction_rollback
+ rollback_db_transaction
+ raise
+ end
+ end
+
+ # Begins the transaction (and turns off auto-committing).
+ def begin_db_transaction() end
+
+ # Commits the transaction (and turns on auto-committing).
+ def commit_db_transaction() end
+
+ # Rollsback the transaction (and turns on auto-committing). Must be done if the transaction block
+ # raises an exception or returns false.
+ def rollback_db_transaction() end
+
+ def quote(value, column = nil)
+ case value
+ when String then "'#{quote_string(value)}'" # ' (for ruby-mode)
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :boolean ? "'t'" : "1")
+ when FalseClass then (column && column.type == :boolean ? "'f'" : "0")
+ when Float, Fixnum, Bignum, Date then "'#{value.to_s}'"
+ when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
+ else "'#{quote_string(value.to_yaml)}'"
+ end
+ end
+
+ def quote_string(s)
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ def quote_column_name(name)
+ return name
+ end
+
+ # Returns a string of the CREATE TABLE SQL statements for recreating the entire structure of the database.
+ def structure_dump() end
+
+ protected
+ def log(sql, name, connection, &action)
+ begin
+ if @logger.nil?
+ action.call(connection)
+ else
+ result = nil
+ bm = measure { result = action.call(connection) }
+ @runtime += bm.real
+ log_info(sql, name, bm.real)
+ result
+ end
+ rescue => e
+ log_info("#{e.message}: #{sql}", name, 0)
+ raise ActiveRecord::StatementInvalid, "#{e.message}: #{sql}"
+ end
+ end
+
+ def log_info(sql, name, runtime)
+ if @logger.nil? then return end
+
+ @logger.info(
+ format_log_entry(
+ "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})",
+ sql.gsub(/ +/, " ")
+ )
+ )
+ end
+
+ def format_log_entry(message, dump = nil)
+ if @@row_even then
+ @@row_even = false; caller_color = "1;32"; message_color = "4;33"; dump_color = "1;37"
+ else
+ @@row_even = true; caller_color = "1;36"; message_color = "4;35"; dump_color = "0;37"
+ end
+
+ log_entry = " \e[#{message_color}m#{message}\e[m"
+ log_entry << " \e[#{dump_color}m%s\e[m" % dump if dump.kind_of?(String) && !dump.nil?
+ log_entry << " \e[#{dump_color}m%p\e[m" % dump if !dump.kind_of?(String) && !dump.nil?
+ log_entry
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
new file mode 100755
index 0000000000..5dcdded5bc
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -0,0 +1,131 @@
+require 'active_record/connection_adapters/abstract_adapter'
+require 'parsedate'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.mysql_connection(config) # :nodoc:
+ unless self.class.const_defined?(:Mysql)
+ begin
+ # Only include the MySQL driver if one hasn't already been loaded
+ require_library_or_gem 'mysql'
+ rescue LoadError => cannot_require_mysql
+ # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
+ begin
+ require 'active_record/vendor/mysql'
+ rescue LoadError
+ raise cannot_require_mysql
+ end
+ end
+ end
+ symbolize_strings_in_hash(config)
+ host = config[:host]
+ port = config[:port]
+ socket = config[:socket]
+ username = config[:username] ? config[:username].to_s : 'root'
+ password = config[:password].to_s
+
+ if config.has_key?(:database)
+ database = config[:database]
+ else
+ raise ArgumentError, "No database specified. Missing argument: database."
+ end
+
+ ConnectionAdapters::MysqlAdapter.new(
+ Mysql::real_connect(host, username, password, database, port, socket), logger
+ )
+ end
+ end
+
+ module ConnectionAdapters
+ class MysqlAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ sql = "SHOW FIELDS FROM #{table_name}"
+ result = nil
+ log(sql, name, @connection) { |connection| result = connection.query(sql) }
+
+ columns = []
+ result.each { |field| columns << Column.new(field[0], field[4], field[1]) }
+ columns
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ return id_value || @connection.insert_id
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) { |connection| connection.query(sql) }
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ begin
+ execute "BEGIN"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def commit_db_transaction
+ begin
+ execute "COMMIT"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def rollback_db_transaction
+ begin
+ execute "ROLLBACK"
+ rescue Exception
+ # Transactions aren't supported
+ end
+ end
+
+ def quote_column_name(name)
+ return "`#{name}`"
+ end
+
+ def structure_dump
+ select_all("SHOW TABLES").inject("") do |structure, table|
+ structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
+ end
+ end
+
+ def recreate_database(name)
+ drop_database(name)
+ create_database(name)
+ end
+
+ def drop_database(name)
+ execute "DROP DATABASE IF EXISTS #{name}"
+ end
+
+ def create_database(name)
+ execute "CREATE DATABASE #{name}"
+ end
+
+ private
+ def select(sql, name = nil)
+ result = nil
+ log(sql, name, @connection) { |connection| connection.query_with_result = true; result = connection.query(sql) }
+ rows = []
+ all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
+ result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
+ rows
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
new file mode 100644
index 0000000000..fb54642d3a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -0,0 +1,170 @@
+
+# postgresql_adaptor.rb
+# author: Luke Holden <lholden@cablelan.net>
+# notes: Currently this adaptor does not pass the test_zero_date_fields
+# and test_zero_datetime_fields unit tests in the BasicsTest test
+# group.
+#
+# This is due to the fact that, in postgresql you can not have a
+# totally zero timestamp. Instead null/nil should be used to
+# represent no value.
+#
+
+require 'active_record/connection_adapters/abstract_adapter'
+require 'parsedate'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.postgresql_connection(config) # :nodoc:
+ require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
+ symbolize_strings_in_hash(config)
+ host = config[:host]
+ port = config[:port] || 5432 unless host.nil?
+ username = config[:username].to_s
+ password = config[:password].to_s
+
+ if config.has_key?(:database)
+ database = config[:database]
+ else
+ raise ArgumentError, "No database specified. Missing argument: database."
+ end
+
+ ConnectionAdapters::PostgreSQLAdapter.new(
+ PGconn.connect(host, port, "", "", database, username, password), logger
+ )
+ end
+ end
+
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, field|
+ columns << Column.new(field[0], field[2], field[1])
+ columns
+ end
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ table = sql.split(" ", 4)[2]
+ return id_value || last_insert_id(table, pk)
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) { |connection| connection.query(sql) }
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN" end
+ def commit_db_transaction() execute "COMMIT" end
+ def rollback_db_transaction() execute "ROLLBACK" end
+
+ def quote_column_name(name)
+ return "\"#{name}\""
+ end
+
+ private
+ def last_insert_id(table, column = "id")
+ sequence_name = "#{table}_#{column || 'id'}_seq"
+ @connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
+ end
+
+ def select(sql, name = nil)
+ res = nil
+ log(sql, name, @connection) { |connection| res = connection.exec(sql) }
+
+ results = res.result
+ rows = []
+ if results.length > 0
+ fields = res.fields
+ results.each do |row|
+ hashed_row = {}
+ row.each_index { |cel_index| hashed_row[fields[cel_index]] = row[cel_index] }
+ rows << hashed_row
+ end
+ end
+ return rows
+ end
+
+ def split_table_schema(table_name)
+ schema_split = table_name.split('.')
+ schema_name = "public"
+ if schema_split.length > 1
+ schema_name = schema_split.first.strip
+ table_name = schema_split.last.strip
+ end
+ return [schema_name, table_name]
+ end
+
+ def table_structure(table_name)
+ database_name = @connection.db
+ schema_name, table_name = split_table_schema(table_name)
+
+ # Grab a list of all the default values for the columns.
+ sql = "SELECT column_name, column_default, character_maximum_length, data_type "
+ sql << " FROM information_schema.columns "
+ sql << " WHERE table_catalog = '#{database_name}' "
+ sql << " AND table_schema = '#{schema_name}' "
+ sql << " AND table_name = '#{table_name}';"
+
+ column_defaults = nil
+ log(sql, nil, @connection) { |connection| column_defaults = connection.query(sql) }
+ column_defaults.collect do |row|
+ field = row[0]
+ type = type_as_string(row[3], row[2])
+ default = default_value(row[1])
+ length = row[2]
+
+ [field, type, default, length]
+ end
+ end
+
+ def type_as_string(field_type, field_length)
+ type = case field_type
+ when 'numeric', 'real', 'money' then 'float'
+ when 'character varying', 'interval' then 'string'
+ when 'timestamp without time zone' then 'datetime'
+ else field_type
+ end
+
+ size = field_length.nil? ? "" : "(#{field_length})"
+
+ return type + size
+ end
+
+ def default_value(value)
+ # Boolean types
+ return "t" if value =~ /true/i
+ return "f" if value =~ /false/i
+
+ # Char/String type values
+ return $1 if value =~ /^'(.*)'::(bpchar|text|character varying)$/
+
+ # Numeric values
+ return value if value =~ /^[0-9]+(\.[0-9]*)?/
+
+ # Date / Time magic values
+ return Time.now.to_s if value =~ /^\('now'::text\)::(date|timestamp)/
+
+ # Fixed dates / times
+ return $1 if value =~ /^'(.+)'::(date|timestamp)/
+
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ return nil
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
new file mode 100644
index 0000000000..1f3845e6a8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -0,0 +1,105 @@
+# sqlite_adapter.rb
+# author: Luke Holden <lholden@cablelan.net>
+
+require 'active_record/connection_adapters/abstract_adapter'
+
+module ActiveRecord
+ class Base
+ # Establishes a connection to the database that's used by all Active Record objects
+ def self.sqlite_connection(config) # :nodoc:
+ require_library_or_gem('sqlite') unless self.class.const_defined?(:SQLite)
+ symbolize_strings_in_hash(config)
+ unless config.has_key?(:dbfile)
+ raise ArgumentError, "No database file specified. Missing argument: dbfile"
+ end
+
+ db = SQLite::Database.new(config[:dbfile], 0)
+
+ db.show_datatypes = "ON" if !defined? SQLite::Version
+ db.results_as_hash = true if defined? SQLite::Version
+ db.type_translation = false
+
+ ConnectionAdapters::SQLiteAdapter.new(db, logger)
+ end
+ end
+
+ module ConnectionAdapters
+ class SQLiteAdapter < AbstractAdapter # :nodoc:
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ table_structure(table_name).inject([]) do |columns, field|
+ columns << Column.new(field['name'], field['dflt_value'], field['type'])
+ columns
+ end
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ execute(sql, name = nil)
+ id_value || @connection.send( defined?( SQLite::Version ) ? :last_insert_row_id : :last_insert_rowid )
+ end
+
+ def execute(sql, name = nil)
+ log(sql, name, @connection) do |connection|
+ if defined?( SQLite::Version )
+ case sql
+ when "BEGIN" then connection.transaction
+ when "COMMIT" then connection.commit
+ when "ROLLBACK" then connection.rollback
+ else connection.execute(sql)
+ end
+ else
+ connection.execute( sql )
+ end
+ end
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction() execute "BEGIN" end
+ def commit_db_transaction() execute "COMMIT" end
+ def rollback_db_transaction() execute "ROLLBACK" end
+
+ def quote_string(s)
+ SQLite::Database.quote(s)
+ end
+
+ def quote_column_name(name)
+ return "'#{name}'"
+ end
+
+ private
+ def select(sql, name = nil)
+ results = nil
+ log(sql, name, @connection) { |connection| results = connection.execute(sql) }
+
+ rows = []
+
+ results.each do |row|
+ hash_only_row = {}
+ row.each_key do |key|
+ hash_only_row[key.sub(/\w+\./, "")] = row[key] unless key.class == Fixnum
+ end
+ rows << hash_only_row
+ end
+
+ return rows
+ end
+
+ def table_structure(table_name)
+ sql = "PRAGMA table_info(#{table_name});"
+ results = nil
+ log(sql, nil, @connection) { |connection| results = connection.execute(sql) }
+ return results
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
new file mode 100644
index 0000000000..5cd5f5a0be
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlserver_adapter.rb
@@ -0,0 +1,298 @@
+require 'active_record/connection_adapters/abstract_adapter'
+
+# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server
+#
+# Author: Joey Gibson <joey@joeygibson.com>
+# Date: 10/14/2004
+#
+# REQUIREMENTS:
+#
+# This adapter will ONLY work on Windows systems, since it relies on Win32OLE, which,
+# to my knowledge, is only available on Window.
+#
+# It relies on the ADO support in the DBI module. If you are using the
+# one-click installer of Ruby, then you already have DBI installed, but
+# the ADO module is *NOT* installed. You will need to get the latest
+# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
+# unzip it, and copy the file src/lib/dbd_ado/ADO.rb to
+# X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb (you will need to create
+# the ADO directory). Once you've installed that file, you are ready to go.
+#
+# This module uses the ADO-style DSNs for connection. For example:
+# "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;"
+# with User Id replaced with your proper login, and Password with your
+# password.
+#
+# I have tested this code on a WindowsXP Pro SP1 system,
+# ruby 1.8.2 (2004-07-29) [i386-mswin32], SQL Server 2000.
+#
+module ActiveRecord
+ class Base
+ def self.sqlserver_connection(config)
+ require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI)
+ class_eval { include ActiveRecord::SQLServerBaseExtensions }
+
+ symbolize_strings_in_hash(config)
+
+ if config.has_key? :dsn
+ dsn = config[:dsn]
+ else
+ raise ArgumentError, "No DSN specified"
+ end
+
+ conn = DBI.connect(dsn)
+ conn["AutoCommit"] = true
+
+ ConnectionAdapters::SQLServerAdapter.new(conn, logger)
+ end
+ end
+
+ module SQLServerBaseExtensions #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def find_first(conditions = nil, orderings = nil)
+ sql = "SELECT TOP 1 * FROM #{table_name} "
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+
+ record = connection.select_one(sql, "#{name} Load First")
+ instantiate(record) unless record.nil?
+ end
+
+ def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
+ sql = "SELECT "
+ sql << "TOP #{limit} " unless limit.nil?
+ sql << " * FROM #{table_name} "
+ sql << "#{joins} " if joins
+ add_conditions!(sql, conditions)
+ sql << "ORDER BY #{orderings} " unless orderings.nil?
+
+ find_by_sql(sql)
+ end
+ end
+
+ def attributes_with_quotes
+ columns_hash = self.class.columns_hash
+
+ attrs = @attributes.dup
+
+ attrs = attrs.reject do |name, value|
+ columns_hash[name].identity
+ end
+
+ attrs.inject({}) do |attrs_quoted, pair|
+ attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
+ attrs_quoted
+ end
+ end
+ end
+
+ module ConnectionAdapters
+ class ColumnWithIdentity < Column
+ attr_reader :identity
+
+ def initialize(name, default, sql_type = nil, is_identity = false)
+ super(name, default, sql_type)
+
+ @identity = is_identity
+ end
+ end
+
+ class SQLServerAdapter < AbstractAdapter # :nodoc:
+ def quote_column_name(name)
+ " [#{name}] "
+ end
+
+ def select_all(sql, name = nil)
+ select(sql, name)
+ end
+
+ def select_one(sql, name = nil)
+ result = select(sql, name)
+ result.nil? ? nil : result.first
+ end
+
+ def columns(table_name, name = nil)
+ sql = <<EOL
+SELECT s.name AS TableName, c.id AS ColId, c.name AS ColName, t.name AS ColType, c.length AS Length,
+c.AutoVal AS IsIdentity,
+c.cdefault AS DefaultId, com.text AS DefaultValue
+FROM syscolumns AS c
+JOIN systypes AS t ON (c.xtype = t.xtype AND c.usertype = t.usertype)
+JOIN sysobjects AS s ON (c.id = s.id)
+LEFT OUTER JOIN syscomments AS com ON (c.cdefault = com.id)
+WHERE s.name = '#{table_name}'
+EOL
+
+ columns = []
+
+ log(sql, name, @connection) do |conn|
+ conn.select_all(sql) do |row|
+ default_value = row[:DefaultValue]
+
+ if default_value =~ /null/i
+ default_value = nil
+ else
+ default_value =~ /\(([^)]+)\)/
+ default_value = $1
+ end
+
+ col = ColumnWithIdentity.new(row[:ColName], default_value, "#{row[:ColType]}(#{row[:Length]})", row[:IsIdentity] != nil)
+
+ columns << col
+ end
+ end
+
+ columns
+ end
+
+ def insert(sql, name = nil, pk = nil, id_value = nil)
+ begin
+ table_name = get_table_name(sql)
+
+ col = get_identity_column(table_name)
+
+ ii_enabled = false
+
+ if col != nil
+ if query_contains_identity_column(sql, col)
+ begin
+ execute enable_identity_insert(table_name, true)
+ ii_enabled = true
+ rescue Exception => e
+ # Coulnd't turn on IDENTITY_INSERT
+ end
+ end
+ end
+
+ log(sql, name, @connection) do |conn|
+ conn.execute(sql)
+
+ select_one("SELECT @@IDENTITY AS Ident")["Ident"]
+ end
+ ensure
+ if ii_enabled
+ begin
+ execute enable_identity_insert(table_name, false)
+
+ rescue Exception => e
+ # Couldn't turn off IDENTITY_INSERT
+ end
+ end
+ end
+ end
+
+ def execute(sql, name = nil)
+ if sql =~ /^INSERT/i
+ insert(sql, name)
+ else
+ log(sql, name, @connection) do |conn|
+ conn.execute(sql)
+ end
+ end
+ end
+
+ alias_method :update, :execute
+ alias_method :delete, :execute
+
+ def begin_db_transaction
+ begin
+ @connection["AutoCommit"] = false
+ rescue Exception => e
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def commit_db_transaction
+ begin
+ @connection.commit
+ ensure
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def rollback_db_transaction
+ begin
+ @connection.rollback
+ ensure
+ @connection["AutoCommit"] = true
+ end
+ end
+
+ def recreate_database(name)
+ drop_database(name)
+ create_database(name)
+ end
+
+ def drop_database(name)
+ execute "DROP DATABASE #{name}"
+ end
+
+ def create_database(name)
+ execute "CREATE DATABASE #{name}"
+ end
+
+ private
+ def select(sql, name = nil)
+ rows = []
+
+ log(sql, name, @connection) do |conn|
+ conn.select_all(sql) do |row|
+ record = {}
+
+ row.column_names.each do |col|
+ record[col] = row[col]
+ end
+
+ rows << record
+ end
+ end
+
+ rows
+ end
+
+ def enable_identity_insert(table_name, enable = true)
+ if has_identity_column(table_name)
+ "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
+ end
+ end
+
+ def get_table_name(sql)
+ if sql =~ /into\s*([^\s]+)\s*/i or
+ sql =~ /update\s*([^\s]+)\s*/i
+ $1
+ else
+ nil
+ end
+ end
+
+ def has_identity_column(table_name)
+ return get_identity_column(table_name) != nil
+ end
+
+ def get_identity_column(table_name)
+ if not @table_columns
+ @table_columns = {}
+ end
+
+ if @table_columns[table_name] == nil
+ @table_columns[table_name] = columns(table_name)
+ end
+
+ @table_columns[table_name].each do |col|
+ return col.name if col.identity
+ end
+
+ return nil
+ end
+
+ def query_contains_identity_column(sql, col)
+ return sql =~ /[\(\.\,]\s*#{col}/
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/deprecated_associations.rb b/activerecord/lib/active_record/deprecated_associations.rb
new file mode 100644
index 0000000000..481b66bf0a
--- /dev/null
+++ b/activerecord/lib/active_record/deprecated_associations.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ module Associations # :nodoc:
+ module ClassMethods
+ def deprecated_collection_count_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{collection_name}_count(force_reload = false)
+ #{collection_name}.reload if force_reload
+ #{collection_name}.size
+ end
+ end_eval
+ end
+
+ def deprecated_add_association_relation(association_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def add_#{association_name}(*items)
+ #{association_name}.concat(items)
+ end
+ end_eval
+ end
+
+ def deprecated_remove_association_relation(association_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def remove_#{association_name}(*items)
+ #{association_name}.delete(items)
+ end
+ end_eval
+ end
+
+ def deprecated_has_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{collection_name}?(force_reload = false)
+ !#{collection_name}(force_reload).empty?
+ end
+ end_eval
+ end
+
+ def deprecated_find_in_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def find_in_#{collection_name}(association_id)
+ #{collection_name}.find(association_id)
+ end
+ end_eval
+ end
+
+ def deprecated_find_all_in_collection_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def find_all_in_#{collection_name}(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
+ #{collection_name}.find_all(runtime_conditions, orderings, limit, joins)
+ end
+ end_eval
+ end
+
+ def deprecated_create_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def create_in_#{collection_name}(attributes = {})
+ #{collection_name}.create(attributes)
+ end
+ end_eval
+ end
+
+ def deprecated_build_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def build_to_#{collection_name}(attributes = {})
+ #{collection_name}.build(attributes)
+ end
+ end_eval
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
new file mode 100755
index 0000000000..f17768e1f2
--- /dev/null
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -0,0 +1,208 @@
+require 'erb'
+require 'yaml'
+require 'active_record/support/class_inheritable_attributes'
+require 'active_record/support/inflector'
+
+# Fixtures are a way of organizing data that you want to test against. You normally have one YAML file with fixture
+# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep
+# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash
+# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml:
+#
+# david:
+# id: 1
+# name: David Heinemeier Hansson
+# birthday: 1979-10-15
+# profession: Systems development
+#
+# steve:
+# id: 2
+# name: Steve Ross Kellock
+# birthday: 1974-09-27
+# profession: guy with keyboard
+#
+# So this YAML file includes two fixtures. T
+#
+# Now when we call <tt>@developers = Fixtures.create_fixtures(".", "developers")</tt> both developers will get inserted into
+# the "developers" table through the active Active Record connection (that must be setup before-hand). And we can now query
+# the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt> will return
+# <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
+#
+# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then
+# be used for comparison in a unit test. Something like:
+#
+# def test_find
+# assert_equal @developers["david"]["name"], @developers["david"].find.name
+# end
+#
+# Comparing that the data we have on the name is also what the object returns when we ask for it.
+#
+# == Automatic fixture setup and instance variable availability
+#
+# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
+#
+# class FixturesTest < Test::Unit::TestCase
+# fixtures :developers # you can add more with comma separation
+#
+# def test_developers
+# assert_equal 3, @developers.size # the container for all the fixtures is automatically set
+# assert_kind_of Developer, @david # works like @developers["david"].find
+# assert_equal "David Heinemeier Hansson", @david.name
+# end
+# end
+class Fixtures < Hash
+ def self.instantiate_fixtures(object, fixtures_directory, *table_names)
+ [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
+ object.instance_variable_set "@#{table_names[idx]}", fixtures
+ fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, *table_names)
+ connection = block_given? ? yield : ActiveRecord::Base.connection
+ old_logger_level = ActiveRecord::Base.logger.level
+
+ begin
+ ActiveRecord::Base.logger.level = Logger::ERROR
+ fixtures = connection.transaction do
+ table_names.flatten.map do |table_name|
+ Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
+ end
+ end
+ return fixtures.size > 1 ? fixtures : fixtures.first
+ ensure
+ ActiveRecord::Base.logger.level = old_logger_level
+ end
+ end
+
+ def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/)
+ @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
+ @class_name = Inflector.classify(@table_name)
+
+ read_fixture_files
+ delete_existing_fixtures
+ insert_fixtures
+ end
+
+ private
+ def read_fixture_files
+ if File.exists?(yaml_file_path)
+ YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data|
+ self[name] = Fixture.new(data, @class_name)
+ end
+ else
+ Dir.entries(@fixture_path).each do |file|
+ self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter
+ end
+ end
+ end
+
+ def delete_existing_fixtures
+ @connection.delete "DELETE FROM #{@table_name}"
+ end
+
+ def insert_fixtures
+ values.each do |fixture|
+ @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
+ end
+ end
+
+ def yaml_file_path
+ @fixture_path + ".yml"
+ end
+
+ def yaml_fixtures_key(path)
+ File.basename(@fixture_path).split(".").first
+ end
+
+ def erb_render(fixture_content)
+ ERB.new(fixture_content).result
+ end
+end
+
+class Fixture #:nodoc:
+ include Enumerable
+ class FixtureError < StandardError; end
+ class FormatError < FixtureError; end
+
+ def initialize(fixture, class_name)
+ @fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
+ @class_name = class_name
+ end
+
+ def each
+ @fixture.each { |item| yield item }
+ end
+
+ def [](key)
+ @fixture[key]
+ end
+
+ def to_hash
+ @fixture
+ end
+
+ def key_list
+ @fixture.keys.join(", ")
+ end
+
+ def value_list
+ @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
+ end
+
+ def find
+ Object.const_get(@class_name).find(self["id"])
+ end
+
+ private
+ def read_fixture_file(fixture_file_path)
+ IO.readlines(fixture_file_path).inject({}) do |fixture, line|
+ # Mercifully skip empty lines.
+ next if line.empty?
+
+ # Use the same regular expression for attributes as Active Record.
+ unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
+ raise FormatError, "#{path}: fixture format error at '#{line}'. Expecting 'key => value'."
+ end
+ key, value = md.captures
+
+ # Disallow duplicate keys to catch typos.
+ raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
+ fixture[key] = value.strip
+ fixture
+ end
+ end
+end
+
+class Test::Unit::TestCase #:nodoc:
+ include ClassInheritableAttributes
+
+ cattr_accessor :fixture_path
+ cattr_accessor :fixture_table_names
+
+ def self.fixtures(*table_names)
+ write_inheritable_attribute("fixture_table_names", table_names)
+ end
+
+ def setup
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ end
+
+ def self.method_added(method_symbol)
+ if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
+ alias_method :setup_without_fixtures, :setup
+ define_method(:setup) do
+ instantiate_fixtures(*fixture_table_names) if fixture_table_names
+ setup_without_fixtures
+ end
+ end
+ end
+
+ private
+ def instantiate_fixtures(*table_names)
+ Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
+ end
+
+ def fixture_table_names
+ self.class.read_inheritable_attribute("fixture_table_names")
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
new file mode 100644
index 0000000000..ceba78b043
--- /dev/null
+++ b/activerecord/lib/active_record/observer.rb
@@ -0,0 +1,71 @@
+require 'singleton'
+
+module ActiveRecord
+ # Observers can be programmed to react to lifecycle callbacks in another class to implement
+ # trigger-like behavior outside the original class. This is a great way to reduce the clutter that
+ # normally comes when the model class is burdened with excess responsibility that doesn't pertain to
+ # the core and nature of the class. Example:
+ #
+ # class CommentObserver < ActiveRecord::Observer
+ # def after_save(comment)
+ # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
+ # end
+ # end
+ #
+ # This Observer is triggered when a Comment#save is finished and sends a notification about it to the administrator.
+ #
+ # == Observing a class that can't be infered
+ #
+ # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
+ # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
+ # something else than the class you're interested in observing, you can implement the observed_class class method. Like this:
+ #
+ # class AuditObserver < ActiveRecord::Observer
+ # def self.observed_class() Account end
+ # def after_update(account)
+ # AuditTrail.new(account, "UPDATED")
+ # end
+ # end
+ #
+ # == Observing multiple classes at once
+ #
+ # If the audit observer needs to watch more than one kind of object, this can be specified in an array, like this:
+ #
+ # class AuditObserver < ActiveRecord::Observer
+ # def self.observed_class() [ Account, Balance ] end
+ # def after_update(record)
+ # AuditTrail.new(record, "UPDATED")
+ # end
+ # end
+ #
+ # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
+ #
+ # The observer can implement callback methods for each of the methods described in the Callbacks module.
+ class Observer
+ include Singleton
+
+ def initialize
+ [ observed_class ].flatten.each do |klass|
+ klass.add_observer(self)
+ klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find)
+ end
+ end
+
+ def update(callback_method, object)
+ send(callback_method, object) if respond_to?(callback_method)
+ end
+
+ private
+ def observed_class
+ if self.class.respond_to? "observed_class"
+ self.class.observed_class
+ else
+ Object.const_get(infer_observed_class_name)
+ end
+ end
+
+ def infer_observed_class_name
+ self.class.name.scan(/(.*)Observer/)[0][0]
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
new file mode 100644
index 0000000000..036200a200
--- /dev/null
+++ b/activerecord/lib/active_record/reflection.rb
@@ -0,0 +1,126 @@
+module ActiveRecord
+ module Reflection # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+
+ base.class_eval do
+ class << self
+ alias_method :composed_of_without_reflection, :composed_of
+
+ def composed_of_with_reflection(part_id, options = {})
+ composed_of_without_reflection(part_id, options)
+ write_inheritable_array "aggregations", [ AggregateReflection.new(part_id, options, self) ]
+ end
+
+ alias_method :composed_of, :composed_of_with_reflection
+ end
+ end
+
+ for association_type in %w( belongs_to has_one has_many has_and_belongs_to_many )
+ base.module_eval <<-"end_eval"
+ class << self
+ alias_method :#{association_type}_without_reflection, :#{association_type}
+
+ def #{association_type}_with_reflection(association_id, options = {})
+ #{association_type}_without_reflection(association_id, options)
+ write_inheritable_array "associations", [ AssociationReflection.new(association_id, options, self) ]
+ end
+
+ alias_method :#{association_type}, :#{association_type}_with_reflection
+ end
+ end_eval
+ end
+ end
+
+ # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
+ # This information can for example be used in a form builder that took an Active Record object and created input
+ # fields for all of the attributes depending on their type and displayed the associations to other objects.
+ #
+ # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
+ module ClassMethods
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
+ def reflect_on_all_aggregations
+ read_inheritable_attribute "aggregations"
+ end
+
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
+ # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
+ def reflect_on_aggregation(aggregation)
+ reflect_on_all_aggregations.find { |reflection| reflection.name == aggregation } unless reflect_on_all_aggregations.nil?
+ end
+
+ # Returns an array of AssociationReflection objects for all the aggregations in the class.
+ def reflect_on_all_associations
+ read_inheritable_attribute "associations"
+ end
+
+ # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
+ def reflect_on_association(association)
+ reflect_on_all_associations.find { |reflection| reflection.name == association } unless reflect_on_all_associations.nil?
+ end
+ end
+
+
+ # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
+ # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
+ class MacroReflection
+ attr_reader :active_record
+ def initialize(name, options, active_record)
+ @name, @options, @active_record = name, options, active_record
+ end
+
+ # Returns the name of the macro, so it would return :balance for "composed_of :balance, :class_name => 'Money'" or
+ # :clients for "has_many :clients".
+ def name
+ @name
+ end
+
+ # Returns the hash of options used for the macro, so it would return { :class_name => "Money" } for
+ # "composed_of :balance, :class_name => 'Money'" or {} for "has_many :clients".
+ def options
+ @options
+ end
+
+ # Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
+ # "has_many :clients" would return the Client class.
+ def klass() end
+
+ def ==(other_aggregation)
+ name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ end
+ end
+
+
+ # Holds all the meta-data about an aggregation as it was specified in the Active Record class.
+ class AggregateReflection < MacroReflection #:nodoc:
+ def klass
+ Object.const_get(options[:class_name] || name_to_class_name(name.id2name))
+ end
+
+ private
+ def name_to_class_name(name)
+ name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
+ end
+ end
+
+ # Holds all the meta-data about an association as it was specified in the Active Record class.
+ class AssociationReflection < MacroReflection #:nodoc:
+ def klass
+ active_record.send(:compute_type, (name_to_class_name(name.id2name)))
+ end
+
+ private
+ def name_to_class_name(name)
+ if name !~ /::/
+ class_name = active_record.send(
+ :type_name_with_module,
+ (options[:class_name] || active_record.class_name(active_record.table_name_prefix + name + active_record.table_name_suffix))
+ )
+ end
+ return class_name || name
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/class_attribute_accessors.rb b/activerecord/lib/active_record/support/class_attribute_accessors.rb
new file mode 100644
index 0000000000..0e269165a6
--- /dev/null
+++ b/activerecord/lib/active_record/support/class_attribute_accessors.rb
@@ -0,0 +1,43 @@
+# attr_* style accessors for class-variables that can accessed both on an instance and class level.
+class Class #:nodoc:
+ def cattr_reader(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}
+ @@#{sym}
+ end
+
+ def #{sym.id2name}
+ self.class.#{sym.id2name}
+ end
+ EOS
+ end
+ end
+
+ def cattr_writer(*syms)
+ syms.each do |sym|
+ class_eval <<-EOS
+ if ! defined? @@#{sym.id2name}
+ @@#{sym.id2name} = nil
+ end
+
+ def self.#{sym.id2name}=(obj)
+ @@#{sym.id2name} = obj
+ end
+
+ def #{sym.id2name}=(obj)
+ self.class.#{sym.id2name}=(obj)
+ end
+ EOS
+ end
+ end
+
+ def cattr_accessor(*syms)
+ cattr_reader(*syms)
+ cattr_writer(*syms)
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/class_inheritable_attributes.rb b/activerecord/lib/active_record/support/class_inheritable_attributes.rb
new file mode 100644
index 0000000000..ee69646da0
--- /dev/null
+++ b/activerecord/lib/active_record/support/class_inheritable_attributes.rb
@@ -0,0 +1,37 @@
+# Allows attributes to be shared within an inheritance hierarchy, but where each descentent gets a copy of
+# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
+# to, for example, an array without those additions being shared with either their parent, siblings, or
+# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
+module ClassInheritableAttributes # :nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods # :nodoc:
+ @@classes ||= {}
+
+ def inheritable_attributes
+ @@classes[self] ||= {}
+ end
+
+ def write_inheritable_attribute(key, value)
+ inheritable_attributes[key] = value
+ end
+
+ def write_inheritable_array(key, elements)
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
+ end
+
+ def read_inheritable_attribute(key)
+ inheritable_attributes[key]
+ end
+
+ private
+ def inherited(child)
+ @@classes[child] = inheritable_attributes.dup
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/support/clean_logger.rb b/activerecord/lib/active_record/support/clean_logger.rb
new file mode 100644
index 0000000000..1a36562892
--- /dev/null
+++ b/activerecord/lib/active_record/support/clean_logger.rb
@@ -0,0 +1,10 @@
+require 'logger'
+
+class Logger #:nodoc:
+ private
+ remove_const "Format"
+ Format = "%s\n"
+ def format_message(severity, timestamp, msg, progname)
+ Format % [msg]
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/support/inflector.rb b/activerecord/lib/active_record/support/inflector.rb
new file mode 100644
index 0000000000..05ff4fede9
--- /dev/null
+++ b/activerecord/lib/active_record/support/inflector.rb
@@ -0,0 +1,78 @@
+# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without,
+# and class names to foreign keys.
+module Inflector
+ extend self
+
+ def pluralize(word)
+ result = word.dup
+ plural_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def singularize(word)
+ result = word.dup
+ singular_rules.each do |(rule, replacement)|
+ break if result.gsub!(rule, replacement)
+ end
+ return result
+ end
+
+ def camelize(lower_case_and_underscored_word)
+ lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase}
+ end
+
+ def underscore(camel_cased_word)
+ camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
+ end
+
+ def demodulize(class_name_in_module)
+ class_name_in_module.gsub(/^.*::/, '')
+ end
+
+ def tableize(class_name)
+ pluralize(underscore(class_name))
+ end
+
+ def classify(table_name)
+ camelize(singularize(table_name))
+ end
+
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
+ Inflector.underscore(Inflector.demodulize(class_name)) +
+ (separate_class_name_and_id_with_underscore ? "_id" : "id")
+ end
+
+ private
+ def plural_rules #:doc:
+ [
+ [/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
+ [/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
+ [/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
+ [/sis$/, 'ses'], # basis, diagnosis
+ [/([ti])um$/, '\1a'], # datum, medium
+ [/person$/, 'people'], # person, salesperson
+ [/man$/, 'men'], # man, woman, spokesman
+ [/child$/, 'children'], # child
+ [/s$/, 's'], # no change (compatibility)
+ [/$/, 's']
+ ]
+ end
+
+ def singular_rules #:doc:
+ [
+ [/(x|ch|ss)es$/, '\1'],
+ [/([^aeiouy]|qu)ies$/, '\1y'],
+ [/([lr])ves$/, '\1f'],
+ [/([^f])ves$/, '\1fe'],
+ [/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'],
+ [/([ti])a$/, '\1um'],
+ [/people$/, 'person'],
+ [/men$/, 'man'],
+ [/status$/, 'status'],
+ [/children$/, 'child'],
+ [/s$/, '']
+ ]
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
new file mode 100644
index 0000000000..d440e74346
--- /dev/null
+++ b/activerecord/lib/active_record/transactions.rb
@@ -0,0 +1,119 @@
+require 'active_record/vendor/simple.rb'
+require 'thread'
+
+module ActiveRecord
+ module Transactions # :nodoc:
+ TRANSACTION_MUTEX = Mutex.new
+
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+
+ base.class_eval do
+ alias_method :destroy_without_transactions, :destroy
+ alias_method :destroy, :destroy_with_transactions
+
+ alias_method :save_without_transactions, :save
+ alias_method :save, :save_with_transactions
+ end
+ end
+
+ # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
+ # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and
+ # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs.
+ # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
+ # not at all. Example:
+ #
+ # transaction do
+ # david.withdrawal(100)
+ # mary.deposit(100)
+ # end
+ #
+ # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
+ # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
+ # that the objects by default will _not_ have their instance data returned to their pre-transactional state.
+ #
+ # == Transactions are not distributed across database connections
+ #
+ # A transaction acts on a single database connection. If you have
+ # multiple class-specific databases, the transaction will not protect
+ # interaction among them. One workaround is to begin a transaction
+ # on each class whose models you alter:
+ #
+ # Student.transaction do
+ # Course.transaction do
+ # course.enroll(student)
+ # student.units += course.units
+ # end
+ # end
+ #
+ # This is a poor solution, but full distributed transactions are beyond
+ # the scope of Active Record.
+ #
+ # == Save and destroy are automatically wrapped in a transaction
+ #
+ # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
+ # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
+ # depend on or you can raise exceptions in the callbacks to rollback.
+ #
+ # == Object-level transactions
+ #
+ # You can enable object-level transactions for Active Record objects, though. You do this by naming the each of the Active Records
+ # that you want to enable object-level transactions for, like this:
+ #
+ # Account.transaction(david, mary) do
+ # david.withdrawal(100)
+ # mary.deposit(100)
+ # end
+ #
+ # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in
+ # neither object nor database.
+ #
+ # == Exception handling
+ #
+ # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
+ # should be ready to catch those in your application code.
+ #
+ # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
+ module ClassMethods
+ def transaction(*objects, &block)
+ TRANSACTION_MUTEX.lock
+
+ begin
+ objects.each { |o| o.extend(Transaction::Simple) }
+ objects.each { |o| o.start_transaction }
+
+ result = connection.transaction(&block)
+
+ objects.each { |o| o.commit_transaction }
+ return result
+ rescue Exception => object_transaction_rollback
+ objects.each { |o| o.abort_transaction }
+ raise
+ ensure
+ TRANSACTION_MUTEX.unlock
+ end
+ end
+ end
+
+ def transaction(*objects, &block)
+ self.class.transaction(*objects, &block)
+ end
+
+ def destroy_with_transactions #:nodoc:
+ if TRANSACTION_MUTEX.locked?
+ destroy_without_transactions
+ else
+ transaction { destroy_without_transactions }
+ end
+ end
+
+ def save_with_transactions(perform_validation = true) #:nodoc:
+ if TRANSACTION_MUTEX.locked?
+ save_without_transactions(perform_validation)
+ else
+ transaction { save_without_transactions(perform_validation) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
new file mode 100755
index 0000000000..07bc7b99b5
--- /dev/null
+++ b/activerecord/lib/active_record/validations.rb
@@ -0,0 +1,205 @@
+module ActiveRecord
+ # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
+ # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
+ # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
+ #
+ # Example:
+ #
+ # class Person < ActiveRecord::Base
+ # protected
+ # def validate
+ # errors.add_on_empty %w( first_name last_name )
+ # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
+ # end
+ #
+ # def validate_on_create # is only run the first time a new object is saved
+ # unless valid_discount?(membership_discount)
+ # errors.add("membership_discount", "has expired")
+ # end
+ # end
+ #
+ # def validate_on_update
+ # errors.add_to_base("No changes have occured") if unchanged_attributes?
+ # end
+ # end
+ #
+ # person = Person.new("first_name" => "David", "phone_number" => "what?")
+ # person.save # => false (and doesn't do the save)
+ # person.errors.empty? # => false
+ # person.count # => 2
+ # person.errors.on "last_name" # => "can't be empty"
+ # person.errors.on "phone_number" # => "has invalid format"
+ # person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
+ # "Phone number has invalid format"
+ #
+ # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
+ # person.save # => true (and person is now saved in the database)
+ #
+ # An +Errors+ object is automatically created for every Active Record.
+ module Validations
+ def self.append_features(base) # :nodoc:
+ super
+
+ base.class_eval do
+ alias_method :save_without_validation, :save
+ alias_method :save, :save_with_validation
+
+ alias_method :update_attribute_without_validation_skipping, :update_attribute
+ alias_method :update_attribute, :update_attribute_with_validation_skipping
+ end
+ end
+
+ # The validation process on save can be skipped by passing false. The regular Base#save method is
+ # replaced with this when the validations module is mixed in, which it is by default.
+ def save_with_validation(perform_validation = true)
+ if perform_validation && valid? || !perform_validation then save_without_validation else false end
+ end
+
+ # Updates a single attribute and saves the record without going through the normal validation procedure.
+ # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
+ # in Base is replaced with this when the validations module is mixed in, which it is by default.
+ def update_attribute_with_validation_skipping(name, value)
+ @attributes[name] = value
+ save(false)
+ end
+
+ # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
+ def valid?
+ errors.clear
+ validate
+ if new_record? then validate_on_create else validate_on_update end
+ errors.empty?
+ end
+
+ # Returns the Errors object that holds all information about attribute error messages.
+ def errors
+ @errors = Errors.new(self) if @errors.nil?
+ @errors
+ end
+
+ protected
+ # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
+ def validate #:doc:
+ end
+
+ # Overwrite this method for validation checks used only on creation.
+ def validate_on_create #:doc:
+ end
+
+ # Overwrite this method for validation checks used only on updates.
+ def validate_on_update # :doc:
+ end
+ end
+
+ # Active Record validation is reported to and from this object, which is used by Base#save to
+ # determine whether the object in a valid state to be saved. See usage example in Validations.
+ class Errors
+ def initialize(base) # :nodoc:
+ @base, @errors = base, {}
+ end
+
+ # Adds an error to the base object instead of any particular attribute. This is used
+ # to report errors that doesn't tie to any specific attribute, but rather to the object
+ # as a whole. These error messages doesn't get prepended with any field name when iterating
+ # with each_full, so they should be complete sentences.
+ def add_to_base(msg)
+ add(:base, msg)
+ end
+
+ # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
+ # for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
+ # If no +msg+ is supplied, "invalid" is assumed.
+ def add(attribute, msg = "invalid")
+ @errors[attribute] = [] if @errors[attribute].nil?
+ @errors[attribute] << msg
+ end
+
+ # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
+ def add_on_empty(attributes, msg = "can't be empty")
+ [attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr) }
+ end
+
+ # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
+ # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
+ def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
+ for attr in [attributes].flatten
+ add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr) && @base.send(attr).length < range.begin
+ add(attr, too_long_msg % range.end) if @base.attribute_present?(attr) && @base.send(attr).length > range.end
+ end
+ end
+
+ alias :add_on_boundry_breaking :add_on_boundary_breaking
+
+ # Returns true if the specified +attribute+ has errors associated with it.
+ def invalid?(attribute)
+ !@errors[attribute].nil?
+ end
+
+ # * Returns nil, if no errors are associated with the specified +attribute+.
+ # * Returns the error message, if one error is associated with the specified +attribute+.
+ # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
+ def on(attribute)
+ if @errors[attribute].nil?
+ nil
+ elsif @errors[attribute].length == 1
+ @errors[attribute].first
+ else
+ @errors[attribute]
+ end
+ end
+
+ alias :[] :on
+
+ # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
+ def on_base
+ on(:base)
+ end
+
+ # Yields each attribute and associated message per error added.
+ def each
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
+ end
+
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
+ # through iteration as "First name can't be empty".
+ def each_full
+ full_messages.each { |msg| yield msg }
+ end
+
+ # Returns all the full error messages in an array.
+ def full_messages
+ full_messages = []
+
+ @errors.each_key do |attr|
+ @errors[attr].each do |msg|
+ if attr == :base
+ full_messages << msg
+ else
+ full_messages << @base.class.human_attribute_name(attr) + " " + msg
+ end
+ end
+ end
+
+ return full_messages
+ end
+
+ # Returns true if no errors have been added.
+ def empty?
+ return @errors.empty?
+ end
+
+ # Removes all the errors that have been added.
+ def clear
+ @errors = {}
+ end
+
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
+ # with this as well.
+ def count
+ error_count = 0
+ @errors.each_value { |attribute| error_count += attribute.length }
+ error_count
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/vendor/mysql.rb b/activerecord/lib/active_record/vendor/mysql.rb
new file mode 100644
index 0000000000..4970f77bd3
--- /dev/null
+++ b/activerecord/lib/active_record/vendor/mysql.rb
@@ -0,0 +1,1117 @@
+# $Id: mysql.rb,v 1.1 2004/02/24 15:42:29 webster132 Exp $
+#
+# Copyright (C) 2003 TOMITA Masahiro
+# tommy@tmtm.org
+#
+
+class Mysql
+
+ VERSION = "4.0-ruby-0.2.4"
+
+ require "socket"
+
+ MAX_PACKET_LENGTH = 256*256*256-1
+ MAX_ALLOWED_PACKET = 1024*1024*1024
+
+ MYSQL_UNIX_ADDR = "/tmp/mysql.sock"
+ MYSQL_PORT = 3306
+ PROTOCOL_VERSION = 10
+
+ # Command
+ COM_SLEEP = 0
+ COM_QUIT = 1
+ COM_INIT_DB = 2
+ COM_QUERY = 3
+ COM_FIELD_LIST = 4
+ COM_CREATE_DB = 5
+ COM_DROP_DB = 6
+ COM_REFRESH = 7
+ COM_SHUTDOWN = 8
+ COM_STATISTICS = 9
+ COM_PROCESS_INFO = 10
+ COM_CONNECT = 11
+ COM_PROCESS_KILL = 12
+ COM_DEBUG = 13
+ COM_PING = 14
+ COM_TIME = 15
+ COM_DELAYED_INSERT = 16
+ COM_CHANGE_USER = 17
+ COM_BINLOG_DUMP = 18
+ COM_TABLE_DUMP = 19
+ COM_CONNECT_OUT = 20
+ COM_REGISTER_SLAVE = 21
+
+ # Client flag
+ CLIENT_LONG_PASSWORD = 1
+ CLIENT_FOUND_ROWS = 1 << 1
+ CLIENT_LONG_FLAG = 1 << 2
+ CLIENT_CONNECT_WITH_DB= 1 << 3
+ CLIENT_NO_SCHEMA = 1 << 4
+ CLIENT_COMPRESS = 1 << 5
+ CLIENT_ODBC = 1 << 6
+ CLIENT_LOCAL_FILES = 1 << 7
+ CLIENT_IGNORE_SPACE = 1 << 8
+ CLIENT_INTERACTIVE = 1 << 10
+ CLIENT_SSL = 1 << 11
+ CLIENT_IGNORE_SIGPIPE = 1 << 12
+ CLIENT_TRANSACTIONS = 1 << 13
+ CLIENT_CAPABILITIES = CLIENT_LONG_PASSWORD|CLIENT_LONG_FLAG|CLIENT_TRANSACTIONS
+
+ # Connection Option
+ OPT_CONNECT_TIMEOUT = 0
+ OPT_COMPRESS = 1
+ OPT_NAMED_PIPE = 2
+ INIT_COMMAND = 3
+ READ_DEFAULT_FILE = 4
+ READ_DEFAULT_GROUP = 5
+ SET_CHARSET_DIR = 6
+ SET_CHARSET_NAME = 7
+ OPT_LOCAL_INFILE = 8
+
+ # Server Status
+ SERVER_STATUS_IN_TRANS = 1
+ SERVER_STATUS_AUTOCOMMIT = 2
+
+ # Refresh parameter
+ REFRESH_GRANT = 1
+ REFRESH_LOG = 2
+ REFRESH_TABLES = 4
+ REFRESH_HOSTS = 8
+ REFRESH_STATUS = 16
+ REFRESH_THREADS = 32
+ REFRESH_SLAVE = 64
+ REFRESH_MASTER = 128
+
+ def initialize(*args)
+ @client_flag = 0
+ @max_allowed_packet = MAX_ALLOWED_PACKET
+ @query_with_result = true
+ @status = :STATUS_READY
+ if args[0] != :INIT then
+ real_connect(*args)
+ end
+ end
+
+ def real_connect(host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
+ @server_status = SERVER_STATUS_AUTOCOMMIT
+ if (host == nil or host == "localhost") and defined? UNIXSocket then
+ unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
+ sock = UNIXSocket::new(unix_socket)
+ @host_info = Error::err(Error::CR_LOCALHOST_CONNECTION)
+ @unix_socket = unix_socket
+ else
+ sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
+ @host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
+ end
+ @host = host ? host.dup : nil
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
+ @net = Net::new sock
+
+ a = read
+ @protocol_version = a.slice!(0)
+ @server_version, a = a.split(/\0/,2)
+ @thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
+ if a.size >= 2 then
+ @server_capabilities, = a.slice!(0,2).unpack("v")
+ end
+ if a.size >= 16 then
+ @server_language, @server_status = a.unpack("cv")
+ end
+
+ flag = 0 if flag == nil
+ flag |= @client_flag | CLIENT_CAPABILITIES
+ flag |= CLIENT_CONNECT_WITH_DB if db
+ data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)
+ if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
+ data << "\0"+db
+ @db = db.dup
+ end
+ write data
+ read
+ self
+ end
+ alias :connect :real_connect
+
+ def escape_string(str)
+ Mysql::escape_string str
+ end
+ alias :quote :escape_string
+
+ def get_client_info()
+ VERSION
+ end
+ alias :client_info :get_client_info
+
+ def options(option, arg=nil)
+ if option == OPT_LOCAL_INFILE then
+ if arg == false or arg == 0 then
+ @client_flag &= ~CLIENT_LOCAL_FILES
+ else
+ @client_flag |= CLIENT_LOCAL_FILES
+ end
+ else
+ raise "not implemented"
+ end
+ end
+
+ def real_query(query)
+ command COM_QUERY, query, true
+ read_query_result
+ self
+ end
+
+ def use_result()
+ if @status != :STATUS_GET_RESULT then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ res = Result::new self, @fields, @field_count
+ @status = :STATUS_USE_RESULT
+ res
+ end
+
+ def store_result()
+ if @status != :STATUS_GET_RESULT then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ @status = :STATUS_READY
+ data = read_rows @field_count
+ res = Result::new self, @fields, @field_count, data
+ @fields = nil
+ @affected_rows = data.length
+ res
+ end
+
+ def change_user(user="", passwd="", db="")
+ data = user+"\0"+scramble(passwd, @scramble_buff, @protocol_version==9)+"\0"+db
+ command COM_CHANGE_USER, data
+ @user = user
+ @passwd = passwd
+ @db = db
+ end
+
+ def character_set_name()
+ raise "not implemented"
+ end
+
+ def close()
+ @status = :STATUS_READY
+ command COM_QUIT, nil, true
+ @net.close
+ self
+ end
+
+ def create_db(db)
+ command COM_CREATE_DB, db
+ self
+ end
+
+ def drop_db(db)
+ command COM_DROP_DB, db
+ self
+ end
+
+ def dump_debug_info()
+ command COM_DEBUG
+ self
+ end
+
+ def get_host_info()
+ @host_info
+ end
+ alias :host_info :get_host_info
+
+ def get_proto_info()
+ @protocol_version
+ end
+ alias :proto_info :get_proto_info
+
+ def get_server_info()
+ @server_version
+ end
+ alias :server_info :get_server_info
+
+ def kill(id)
+ command COM_PROCESS_KILL, Net::int4str(id)
+ self
+ end
+
+ def list_dbs(db=nil)
+ real_query "show databases #{db}"
+ @status = :STATUS_READY
+ read_rows(1).flatten
+ end
+
+ def list_fields(table, field=nil)
+ command COM_FIELD_LIST, "#{table}\0#{field}", true
+ f = read_rows 6
+ fields = unpack_fields(f, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ res = Result::new self, fields, f.length
+ res.eof = true
+ res
+ end
+
+ def list_processes()
+ data = command COM_PROCESS_INFO
+ @field_count = get_length data
+ fields = read_rows 5
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ @status = :STATUS_GET_RESULT
+ store_result
+ end
+
+ def list_tables(table=nil)
+ real_query "show tables #{table}"
+ @status = :STATUS_READY
+ read_rows(1).flatten
+ end
+
+ def ping()
+ command COM_PING
+ self
+ end
+
+ def query(query)
+ real_query query
+ if not @query_with_result then
+ return self
+ end
+ if @field_count == 0 then
+ return nil
+ end
+ store_result
+ end
+
+ def refresh(r)
+ command COM_REFRESH, r.chr
+ self
+ end
+
+ def reload()
+ refresh REFRESH_GRANT
+ self
+ end
+
+ def select_db(db)
+ command COM_INIT_DB, db
+ @db = db
+ self
+ end
+
+ def shutdown()
+ command COM_SHUTDOWN
+ self
+ end
+
+ def stat()
+ command COM_STATISTICS
+ end
+
+ attr_reader :info, :insert_id, :affected_rows, :field_count, :thread_id
+ attr_accessor :query_with_result, :status
+
+ def read_one_row(field_count)
+ data = read
+ return if data[0] == 254 and data.length == 1
+ rec = []
+ field_count.times do
+ len = get_length data
+ if len == nil then
+ rec << len
+ else
+ rec << data.slice!(0,len)
+ end
+ end
+ rec
+ end
+
+ def skip_result()
+ if @status == :STATUS_USE_RESULT then
+ loop do
+ data = read
+ break if data[0] == 254 and data.length == 1
+ end
+ @status = :STATUS_READY
+ end
+ end
+
+ def inspect()
+ "#<#{self.class}>"
+ end
+
+ private
+
+ def read_query_result()
+ data = read
+ @field_count = get_length(data)
+ if @field_count == nil then # LOAD DATA LOCAL INFILE
+ File::open(data) do |f|
+ write f.read
+ end
+ write "" # mark EOF
+ data = read
+ @field_count = get_length(data)
+ end
+ if @field_count == 0 then
+ @affected_rows = get_length(data, true)
+ @insert_id = get_length(data, true)
+ if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
+ a = data.slice!(0,2)
+ @server_status = a[0]+a[1]*256
+ end
+ if data.size > 0 and get_length(data) then
+ @info = data
+ end
+ else
+ @extra_info = get_length(data, true)
+ fields = read_rows 5
+ @fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
+ @status = :STATUS_GET_RESULT
+ end
+ self
+ end
+
+ def unpack_fields(data, long_flag_protocol)
+ ret = []
+ data.each do |f|
+ table = org_table = f[0]
+ name = f[1]
+ length = f[2][0]+f[2][1]*256+f[2][2]*256*256
+ type = f[3][0]
+ if long_flag_protocol then
+ flags = f[4][0]+f[4][1]*256
+ decimals = f[4][2]
+ else
+ flags = f[4][0]
+ decimals = f[4][1]
+ end
+ def_value = f[5]
+ max_length = 0
+ ret << Field::new(table, org_table, name, length, type, flags, decimals, def_value, max_length)
+ end
+ ret
+ end
+
+ def read_rows(field_count)
+ ret = []
+ while rec = read_one_row(field_count) do
+ ret << rec
+ end
+ ret
+ end
+
+ def get_length(data, longlong=nil)
+ return if data.length == 0
+ c = data.slice!(0)
+ case c
+ when 251
+ return nil
+ when 252
+ a = data.slice!(0,2)
+ return a[0]+a[1]*256
+ when 253
+ a = data.slice!(0,3)
+ return a[0]+a[1]*256+a[2]*256**2
+ when 254
+ a = data.slice!(0,8)
+ if longlong then
+ return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3+
+ a[4]*256**4+a[5]*256**5+a[6]*256**6+a[7]*256**7
+ else
+ return a[0]+a[1]*256+a[2]*256**2+a[3]*256**3
+ end
+ else
+ c
+ end
+ end
+
+ def command(cmd, arg=nil, skip_check=nil)
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ if @status != :STATUS_READY then
+ error Error::CR_COMMANDS_OUT_OF_SYNC
+ end
+ @net.clear
+ write cmd.chr+(arg||"")
+ read unless skip_check
+ end
+
+ def read()
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ a = @net.read
+ if a[0] == 255 then
+ if a.length > 3 then
+ @errno = a[1]+a[2]*256
+ @error = a[3 .. -1]
+ else
+ @errno = Error::CR_UNKNOWN_ERROR
+ @error = Error::err @errno
+ end
+ raise Error::new(@errno, @error)
+ end
+ a
+ end
+
+ def write(arg)
+ unless @net then
+ error Error::CR_SERVER_GONE_ERROR
+ end
+ @net.write arg
+ end
+
+ def hash_password(password)
+ nr = 1345345333
+ add = 7
+ nr2 = 0x12345671
+ password.each_byte do |i|
+ next if i == 0x20 or i == 9
+ nr ^= (((nr & 63) + add) * i) + (nr << 8)
+ nr2 += (nr2 << 8) ^ nr
+ add += i
+ end
+ [nr & ((1 << 31) - 1), nr2 & ((1 << 31) - 1)]
+ end
+
+ def scramble(password, message, old_ver)
+ return "" if password == nil or password == ""
+ raise "old version password is not implemented" if old_ver
+ hash_pass = hash_password password
+ hash_message = hash_password message
+ rnd = Random::new hash_pass[0] ^ hash_message[0], hash_pass[1] ^ hash_message[1]
+ to = []
+ 1.upto(message.length) do
+ to << ((rnd.rnd*31)+64).floor
+ end
+ extra = (rnd.rnd*31).floor
+ to.map! do |t| (t ^ extra).chr end
+ to.join
+ end
+
+ def error(errno)
+ @errno = errno
+ @error = Error::err errno
+ raise Error::new(@errno, @error)
+ end
+
+ class Result
+ def initialize(mysql, fields, field_count, data=nil)
+ @handle = mysql
+ @fields = fields
+ @field_count = field_count
+ @data = data
+ @current_field = 0
+ @current_row = 0
+ @eof = false
+ @row_count = 0
+ end
+ attr_accessor :eof
+
+ def data_seek(n)
+ @current_row = n
+ end
+
+ def fetch_field()
+ return if @current_field >= @field_count
+ f = @fields[@current_field]
+ @current_field += 1
+ f
+ end
+
+ def fetch_fields()
+ @fields
+ end
+
+ def fetch_field_direct(n)
+ @fields[n]
+ end
+
+ def fetch_lengths()
+ @data ? @data[@current_row].map{|i| i ? i.length : 0} : @lengths
+ end
+
+ def fetch_row()
+ if @data then
+ if @current_row >= @data.length then
+ @handle.status = :STATUS_READY
+ return
+ end
+ ret = @data[@current_row]
+ @current_row += 1
+ else
+ return if @eof
+ ret = @handle.read_one_row @field_count
+ if ret == nil then
+ @eof = true
+ return
+ end
+ @lengths = ret.map{|i| i ? i.length : 0}
+ @row_count += 1
+ end
+ ret
+ end
+
+ def fetch_hash(with_table=nil)
+ row = fetch_row
+ return if row == nil
+ hash = {}
+ @fields.each_index do |i|
+ f = with_table ? @fields[i].table+"."+@fields[i].name : @fields[i].name
+ hash[f] = row[i]
+ end
+ hash
+ end
+
+ def field_seek(n)
+ @current_field = n
+ end
+
+ def field_tell()
+ @current_field
+ end
+
+ def free()
+ @handle.skip_result
+ @handle = @fields = @data = nil
+ GC::start
+ end
+
+ def num_fields()
+ @field_count
+ end
+
+ def num_rows()
+ @data ? @data.length : @row_count
+ end
+
+ def row_seek(n)
+ @current_row = n
+ end
+
+ def row_tell()
+ @current_row
+ end
+
+ def each()
+ while row = fetch_row do
+ yield row
+ end
+ end
+
+ def each_hash(with_table=nil)
+ while hash = fetch_hash(with_table) do
+ yield hash
+ end
+ end
+
+ def inspect()
+ "#<#{self.class}>"
+ end
+
+ end
+
+ class Field
+ # Field type
+ TYPE_DECIMAL = 0
+ TYPE_TINY = 1
+ TYPE_SHORT = 2
+ TYPE_LONG = 3
+ TYPE_FLOAT = 4
+ TYPE_DOUBLE = 5
+ TYPE_NULL = 6
+ TYPE_TIMESTAMP = 7
+ TYPE_LONGLONG = 8
+ TYPE_INT24 = 9
+ TYPE_DATE = 10
+ TYPE_TIME = 11
+ TYPE_DATETIME = 12
+ TYPE_YEAR = 13
+ TYPE_NEWDATE = 14
+ TYPE_ENUM = 247
+ TYPE_SET = 248
+ TYPE_TINY_BLOB = 249
+ TYPE_MEDIUM_BLOB = 250
+ TYPE_LONG_BLOB = 251
+ TYPE_BLOB = 252
+ TYPE_VAR_STRING = 253
+ TYPE_STRING = 254
+ TYPE_GEOMETRY = 255
+ TYPE_CHAR = TYPE_TINY
+ TYPE_INTERVAL = TYPE_ENUM
+
+ # Flag
+ NOT_NULL_FLAG = 1
+ PRI_KEY_FLAG = 2
+ UNIQUE_KEY_FLAG = 4
+ MULTIPLE_KEY_FLAG = 8
+ BLOB_FLAG = 16
+ UNSIGNED_FLAG = 32
+ ZEROFILL_FLAG = 64
+ BINARY_FLAG = 128
+ ENUM_FLAG = 256
+ AUTO_INCREMENT_FLAG = 512
+ TIMESTAMP_FLAG = 1024
+ SET_FLAG = 2048
+ NUM_FLAG = 32768
+ PART_KEY_FLAG = 16384
+ GROUP_FLAG = 32768
+ UNIQUE_FLAG = 65536
+
+ def initialize(table, org_table, name, length, type, flags, decimals, def_value, max_length)
+ @table = table
+ @org_table = org_table
+ @name = name
+ @length = length
+ @type = type
+ @flags = flags
+ @decimals = decimals
+ @def = def_value
+ @max_length = max_length
+ if (type <= TYPE_INT24 and (type != TYPE_TIMESTAMP or length == 14 or length == 8)) or type == TYPE_YEAR then
+ @flags |= NUM_FLAG
+ end
+ end
+ attr_reader :table, :org_table, :name, :length, :type, :flags, :decimals, :def, :max_length
+
+ def inspect()
+ "#<#{self.class}:#{@name}>"
+ end
+ end
+
+ class Error < StandardError
+ # Server Error
+ ER_HASHCHK = 1000
+ ER_NISAMCHK = 1001
+ ER_NO = 1002
+ ER_YES = 1003
+ ER_CANT_CREATE_FILE = 1004
+ ER_CANT_CREATE_TABLE = 1005
+ ER_CANT_CREATE_DB = 1006
+ ER_DB_CREATE_EXISTS = 1007
+ ER_DB_DROP_EXISTS = 1008
+ ER_DB_DROP_DELETE = 1009
+ ER_DB_DROP_RMDIR = 1010
+ ER_CANT_DELETE_FILE = 1011
+ ER_CANT_FIND_SYSTEM_REC = 1012
+ ER_CANT_GET_STAT = 1013
+ ER_CANT_GET_WD = 1014
+ ER_CANT_LOCK = 1015
+ ER_CANT_OPEN_FILE = 1016
+ ER_FILE_NOT_FOUND = 1017
+ ER_CANT_READ_DIR = 1018
+ ER_CANT_SET_WD = 1019
+ ER_CHECKREAD = 1020
+ ER_DISK_FULL = 1021
+ ER_DUP_KEY = 1022
+ ER_ERROR_ON_CLOSE = 1023
+ ER_ERROR_ON_READ = 1024
+ ER_ERROR_ON_RENAME = 1025
+ ER_ERROR_ON_WRITE = 1026
+ ER_FILE_USED = 1027
+ ER_FILSORT_ABORT = 1028
+ ER_FORM_NOT_FOUND = 1029
+ ER_GET_ERRNO = 1030
+ ER_ILLEGAL_HA = 1031
+ ER_KEY_NOT_FOUND = 1032
+ ER_NOT_FORM_FILE = 1033
+ ER_NOT_KEYFILE = 1034
+ ER_OLD_KEYFILE = 1035
+ ER_OPEN_AS_READONLY = 1036
+ ER_OUTOFMEMORY = 1037
+ ER_OUT_OF_SORTMEMORY = 1038
+ ER_UNEXPECTED_EOF = 1039
+ ER_CON_COUNT_ERROR = 1040
+ ER_OUT_OF_RESOURCES = 1041
+ ER_BAD_HOST_ERROR = 1042
+ ER_HANDSHAKE_ERROR = 1043
+ ER_DBACCESS_DENIED_ERROR = 1044
+ ER_ACCESS_DENIED_ERROR = 1045
+ ER_NO_DB_ERROR = 1046
+ ER_UNKNOWN_COM_ERROR = 1047
+ ER_BAD_NULL_ERROR = 1048
+ ER_BAD_DB_ERROR = 1049
+ ER_TABLE_EXISTS_ERROR = 1050
+ ER_BAD_TABLE_ERROR = 1051
+ ER_NON_UNIQ_ERROR = 1052
+ ER_SERVER_SHUTDOWN = 1053
+ ER_BAD_FIELD_ERROR = 1054
+ ER_WRONG_FIELD_WITH_GROUP = 1055
+ ER_WRONG_GROUP_FIELD = 1056
+ ER_WRONG_SUM_SELECT = 1057
+ ER_WRONG_VALUE_COUNT = 1058
+ ER_TOO_LONG_IDENT = 1059
+ ER_DUP_FIELDNAME = 1060
+ ER_DUP_KEYNAME = 1061
+ ER_DUP_ENTRY = 1062
+ ER_WRONG_FIELD_SPEC = 1063
+ ER_PARSE_ERROR = 1064
+ ER_EMPTY_QUERY = 1065
+ ER_NONUNIQ_TABLE = 1066
+ ER_INVALID_DEFAULT = 1067
+ ER_MULTIPLE_PRI_KEY = 1068
+ ER_TOO_MANY_KEYS = 1069
+ ER_TOO_MANY_KEY_PARTS = 1070
+ ER_TOO_LONG_KEY = 1071
+ ER_KEY_COLUMN_DOES_NOT_EXITS = 1072
+ ER_BLOB_USED_AS_KEY = 1073
+ ER_TOO_BIG_FIELDLENGTH = 1074
+ ER_WRONG_AUTO_KEY = 1075
+ ER_READY = 1076
+ ER_NORMAL_SHUTDOWN = 1077
+ ER_GOT_SIGNAL = 1078
+ ER_SHUTDOWN_COMPLETE = 1079
+ ER_FORCING_CLOSE = 1080
+ ER_IPSOCK_ERROR = 1081
+ ER_NO_SUCH_INDEX = 1082
+ ER_WRONG_FIELD_TERMINATORS = 1083
+ ER_BLOBS_AND_NO_TERMINATED = 1084
+ ER_TEXTFILE_NOT_READABLE = 1085
+ ER_FILE_EXISTS_ERROR = 1086
+ ER_LOAD_INFO = 1087
+ ER_ALTER_INFO = 1088
+ ER_WRONG_SUB_KEY = 1089
+ ER_CANT_REMOVE_ALL_FIELDS = 1090
+ ER_CANT_DROP_FIELD_OR_KEY = 1091
+ ER_INSERT_INFO = 1092
+ ER_INSERT_TABLE_USED = 1093
+ ER_NO_SUCH_THREAD = 1094
+ ER_KILL_DENIED_ERROR = 1095
+ ER_NO_TABLES_USED = 1096
+ ER_TOO_BIG_SET = 1097
+ ER_NO_UNIQUE_LOGFILE = 1098
+ ER_TABLE_NOT_LOCKED_FOR_WRITE = 1099
+ ER_TABLE_NOT_LOCKED = 1100
+ ER_BLOB_CANT_HAVE_DEFAULT = 1101
+ ER_WRONG_DB_NAME = 1102
+ ER_WRONG_TABLE_NAME = 1103
+ ER_TOO_BIG_SELECT = 1104
+ ER_UNKNOWN_ERROR = 1105
+ ER_UNKNOWN_PROCEDURE = 1106
+ ER_WRONG_PARAMCOUNT_TO_PROCEDURE = 1107
+ ER_WRONG_PARAMETERS_TO_PROCEDURE = 1108
+ ER_UNKNOWN_TABLE = 1109
+ ER_FIELD_SPECIFIED_TWICE = 1110
+ ER_INVALID_GROUP_FUNC_USE = 1111
+ ER_UNSUPPORTED_EXTENSION = 1112
+ ER_TABLE_MUST_HAVE_COLUMNS = 1113
+ ER_RECORD_FILE_FULL = 1114
+ ER_UNKNOWN_CHARACTER_SET = 1115
+ ER_TOO_MANY_TABLES = 1116
+ ER_TOO_MANY_FIELDS = 1117
+ ER_TOO_BIG_ROWSIZE = 1118
+ ER_STACK_OVERRUN = 1119
+ ER_WRONG_OUTER_JOIN = 1120
+ ER_NULL_COLUMN_IN_INDEX = 1121
+ ER_CANT_FIND_UDF = 1122
+ ER_CANT_INITIALIZE_UDF = 1123
+ ER_UDF_NO_PATHS = 1124
+ ER_UDF_EXISTS = 1125
+ ER_CANT_OPEN_LIBRARY = 1126
+ ER_CANT_FIND_DL_ENTRY = 1127
+ ER_FUNCTION_NOT_DEFINED = 1128
+ ER_HOST_IS_BLOCKED = 1129
+ ER_HOST_NOT_PRIVILEGED = 1130
+ ER_PASSWORD_ANONYMOUS_USER = 1131
+ ER_PASSWORD_NOT_ALLOWED = 1132
+ ER_PASSWORD_NO_MATCH = 1133
+ ER_UPDATE_INFO = 1134
+ ER_CANT_CREATE_THREAD = 1135
+ ER_WRONG_VALUE_COUNT_ON_ROW = 1136
+ ER_CANT_REOPEN_TABLE = 1137
+ ER_INVALID_USE_OF_NULL = 1138
+ ER_REGEXP_ERROR = 1139
+ ER_MIX_OF_GROUP_FUNC_AND_FIELDS = 1140
+ ER_NONEXISTING_GRANT = 1141
+ ER_TABLEACCESS_DENIED_ERROR = 1142
+ ER_COLUMNACCESS_DENIED_ERROR = 1143
+ ER_ILLEGAL_GRANT_FOR_TABLE = 1144
+ ER_GRANT_WRONG_HOST_OR_USER = 1145
+ ER_NO_SUCH_TABLE = 1146
+ ER_NONEXISTING_TABLE_GRANT = 1147
+ ER_NOT_ALLOWED_COMMAND = 1148
+ ER_SYNTAX_ERROR = 1149
+ ER_DELAYED_CANT_CHANGE_LOCK = 1150
+ ER_TOO_MANY_DELAYED_THREADS = 1151
+ ER_ABORTING_CONNECTION = 1152
+ ER_NET_PACKET_TOO_LARGE = 1153
+ ER_NET_READ_ERROR_FROM_PIPE = 1154
+ ER_NET_FCNTL_ERROR = 1155
+ ER_NET_PACKETS_OUT_OF_ORDER = 1156
+ ER_NET_UNCOMPRESS_ERROR = 1157
+ ER_NET_READ_ERROR = 1158
+ ER_NET_READ_INTERRUPTED = 1159
+ ER_NET_ERROR_ON_WRITE = 1160
+ ER_NET_WRITE_INTERRUPTED = 1161
+ ER_TOO_LONG_STRING = 1162
+ ER_TABLE_CANT_HANDLE_BLOB = 1163
+ ER_TABLE_CANT_HANDLE_AUTO_INCREMENT = 1164
+ ER_DELAYED_INSERT_TABLE_LOCKED = 1165
+ ER_WRONG_COLUMN_NAME = 1166
+ ER_WRONG_KEY_COLUMN = 1167
+ ER_WRONG_MRG_TABLE = 1168
+ ER_DUP_UNIQUE = 1169
+ ER_BLOB_KEY_WITHOUT_LENGTH = 1170
+ ER_PRIMARY_CANT_HAVE_NULL = 1171
+ ER_TOO_MANY_ROWS = 1172
+ ER_REQUIRES_PRIMARY_KEY = 1173
+ ER_NO_RAID_COMPILED = 1174
+ ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE = 1175
+ ER_KEY_DOES_NOT_EXITS = 1176
+ ER_CHECK_NO_SUCH_TABLE = 1177
+ ER_CHECK_NOT_IMPLEMENTED = 1178
+ ER_CANT_DO_THIS_DURING_AN_TRANSACTION = 1179
+ ER_ERROR_DURING_COMMIT = 1180
+ ER_ERROR_DURING_ROLLBACK = 1181
+ ER_ERROR_DURING_FLUSH_LOGS = 1182
+ ER_ERROR_DURING_CHECKPOINT = 1183
+ ER_NEW_ABORTING_CONNECTION = 1184
+ ER_DUMP_NOT_IMPLEMENTED = 1185
+ ER_FLUSH_MASTER_BINLOG_CLOSED = 1186
+ ER_INDEX_REBUILD = 1187
+ ER_MASTER = 1188
+ ER_MASTER_NET_READ = 1189
+ ER_MASTER_NET_WRITE = 1190
+ ER_FT_MATCHING_KEY_NOT_FOUND = 1191
+ ER_LOCK_OR_ACTIVE_TRANSACTION = 1192
+ ER_UNKNOWN_SYSTEM_VARIABLE = 1193
+ ER_CRASHED_ON_USAGE = 1194
+ ER_CRASHED_ON_REPAIR = 1195
+ ER_WARNING_NOT_COMPLETE_ROLLBACK = 1196
+ ER_TRANS_CACHE_FULL = 1197
+ ER_SLAVE_MUST_STOP = 1198
+ ER_SLAVE_NOT_RUNNING = 1199
+ ER_BAD_SLAVE = 1200
+ ER_MASTER_INFO = 1201
+ ER_SLAVE_THREAD = 1202
+ ER_TOO_MANY_USER_CONNECTIONS = 1203
+ ER_SET_CONSTANTS_ONLY = 1204
+ ER_LOCK_WAIT_TIMEOUT = 1205
+ ER_LOCK_TABLE_FULL = 1206
+ ER_READ_ONLY_TRANSACTION = 1207
+ ER_DROP_DB_WITH_READ_LOCK = 1208
+ ER_CREATE_DB_WITH_READ_LOCK = 1209
+ ER_WRONG_ARGUMENTS = 1210
+ ER_NO_PERMISSION_TO_CREATE_USER = 1211
+ ER_UNION_TABLES_IN_DIFFERENT_DIR = 1212
+ ER_LOCK_DEADLOCK = 1213
+ ER_TABLE_CANT_HANDLE_FULLTEXT = 1214
+ ER_CANNOT_ADD_FOREIGN = 1215
+ ER_NO_REFERENCED_ROW = 1216
+ ER_ROW_IS_REFERENCED = 1217
+ ER_CONNECT_TO_MASTER = 1218
+ ER_QUERY_ON_MASTER = 1219
+ ER_ERROR_WHEN_EXECUTING_COMMAND = 1220
+ ER_WRONG_USAGE = 1221
+ ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT = 1222
+ ER_CANT_UPDATE_WITH_READLOCK = 1223
+ ER_MIXING_NOT_ALLOWED = 1224
+ ER_DUP_ARGUMENT = 1225
+ ER_USER_LIMIT_REACHED = 1226
+ ER_SPECIFIC_ACCESS_DENIED_ERROR = 1227
+ ER_LOCAL_VARIABLE = 1228
+ ER_GLOBAL_VARIABLE = 1229
+ ER_NO_DEFAULT = 1230
+ ER_WRONG_VALUE_FOR_VAR = 1231
+ ER_WRONG_TYPE_FOR_VAR = 1232
+ ER_VAR_CANT_BE_READ = 1233
+ ER_CANT_USE_OPTION_HERE = 1234
+ ER_NOT_SUPPORTED_YET = 1235
+ ER_MASTER_FATAL_ERROR_READING_BINLOG = 1236
+ ER_SLAVE_IGNORED_TABLE = 1237
+ ER_ERROR_MESSAGES = 238
+
+ # Client Error
+ CR_MIN_ERROR = 2000
+ CR_MAX_ERROR = 2999
+ CR_UNKNOWN_ERROR = 2000
+ CR_SOCKET_CREATE_ERROR = 2001
+ CR_CONNECTION_ERROR = 2002
+ CR_CONN_HOST_ERROR = 2003
+ CR_IPSOCK_ERROR = 2004
+ CR_UNKNOWN_HOST = 2005
+ CR_SERVER_GONE_ERROR = 2006
+ CR_VERSION_ERROR = 2007
+ CR_OUT_OF_MEMORY = 2008
+ CR_WRONG_HOST_INFO = 2009
+ CR_LOCALHOST_CONNECTION = 2010
+ CR_TCP_CONNECTION = 2011
+ CR_SERVER_HANDSHAKE_ERR = 2012
+ CR_SERVER_LOST = 2013
+ CR_COMMANDS_OUT_OF_SYNC = 2014
+ CR_NAMEDPIPE_CONNECTION = 2015
+ CR_NAMEDPIPEWAIT_ERROR = 2016
+ CR_NAMEDPIPEOPEN_ERROR = 2017
+ CR_NAMEDPIPESETSTATE_ERROR = 2018
+ CR_CANT_READ_CHARSET = 2019
+ CR_NET_PACKET_TOO_LARGE = 2020
+ CR_EMBEDDED_CONNECTION = 2021
+ CR_PROBE_SLAVE_STATUS = 2022
+ CR_PROBE_SLAVE_HOSTS = 2023
+ CR_PROBE_SLAVE_CONNECT = 2024
+ CR_PROBE_MASTER_CONNECT = 2025
+ CR_SSL_CONNECTION_ERROR = 2026
+ CR_MALFORMED_PACKET = 2027
+
+ CLIENT_ERRORS = [
+ "Unknown MySQL error",
+ "Can't create UNIX socket (%d)",
+ "Can't connect to local MySQL server through socket '%-.64s' (%d)",
+ "Can't connect to MySQL server on '%-.64s' (%d)",
+ "Can't create TCP/IP socket (%d)",
+ "Unknown MySQL Server Host '%-.64s' (%d)",
+ "MySQL server has gone away",
+ "Protocol mismatch. Server Version = %d Client Version = %d",
+ "MySQL client run out of memory",
+ "Wrong host info",
+ "Localhost via UNIX socket",
+ "%-.64s via TCP/IP",
+ "Error in server handshake",
+ "Lost connection to MySQL server during query",
+ "Commands out of sync; You can't run this command now",
+ "%-.64s via named pipe",
+ "Can't wait for named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't open named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't set state of named pipe to host: %-.64s pipe: %-.32s (%lu)",
+ "Can't initialize character set %-.64s (path: %-.64s)",
+ "Got packet bigger than 'max_allowed_packet'",
+ "Embedded server",
+ "Error on SHOW SLAVE STATUS:",
+ "Error on SHOW SLAVE HOSTS:",
+ "Error connecting to slave:",
+ "Error connecting to master:",
+ "SSL connection error",
+ "Malformed packet"
+ ]
+
+ def initialize(errno, error)
+ @errno = errno
+ @error = error
+ super error
+ end
+ attr_reader :errno, :error
+
+ def Error::err(errno)
+ CLIENT_ERRORS[errno - Error::CR_MIN_ERROR]
+ end
+ end
+
+ class Net
+ def initialize(sock)
+ @sock = sock
+ @pkt_nr = 0
+ end
+
+ def clear()
+ @pkt_nr = 0
+ end
+
+ def read()
+ buf = []
+ len = nil
+ @sock.sync = false
+ while len == nil or len == MAX_PACKET_LENGTH do
+ a = @sock.read(4)
+ len = a[0]+a[1]*256+a[2]*256*256
+ pkt_nr = a[3]
+ if @pkt_nr != pkt_nr then
+ raise "Packets out of order: #{@pkt_nr}<>#{pkt_nr}"
+ end
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ buf << @sock.read(len)
+ end
+ @sock.sync = true
+ buf.join
+ end
+
+ def write(data)
+ if data.is_a? Array then
+ data = data.join
+ end
+ @sock.sync = false
+ ptr = 0
+ while data.length >= MAX_PACKET_LENGTH do
+ @sock.write Net::int3str(MAX_PACKET_LENGTH)+@pkt_nr.chr+data[ptr, MAX_PACKET_LENGTH]
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ ptr += MAX_PACKET_LENGTH
+ end
+ @sock.write Net::int3str(data.length-ptr)+@pkt_nr.chr+data[ptr .. -1]
+ @pkt_nr = @pkt_nr + 1 & 0xff
+ @sock.sync = true
+ @sock.flush
+ end
+
+ def close()
+ @sock.close
+ end
+
+ def Net::int2str(n)
+ [n].pack("v")
+ end
+
+ def Net::int3str(n)
+ [n%256, n>>8].pack("cv")
+ end
+
+ def Net::int4str(n)
+ [n].pack("V")
+ end
+
+ end
+
+ class Random
+ def initialize(seed1, seed2)
+ @max_value = 0x3FFFFFFF
+ @seed1 = seed1 % @max_value
+ @seed2 = seed2 % @max_value
+ end
+
+ def rnd()
+ @seed1 = (@seed1*3+@seed2) % @max_value
+ @seed2 = (@seed1+@seed2+33) % @max_value
+ @seed1.to_f / @max_value
+ end
+ end
+
+end
+
+class << Mysql
+ def init()
+ Mysql::new :INIT
+ end
+
+ def real_connect(*args)
+ Mysql::new(*args)
+ end
+ alias :connect :real_connect
+
+ def escape_string(str)
+ str.gsub(/([\0\n\r\032\'\"\\])/) do
+ case $1
+ when "\0" then "\\0"
+ when "\n" then "\\n"
+ when "\r" then "\\r"
+ when "\032" then "\Z"
+ else "\\"+$1
+ end
+ end
+ end
+ alias :quote :escape_string
+
+ def get_client_info()
+ Mysql::VERSION
+ end
+ alias :client_info :get_client_info
+
+ def debug(str)
+ raise "not implemented"
+ end
+end
+
+#
+# for compatibility
+#
+
+MysqlRes = Mysql::Result
+MysqlField = Mysql::Field
+MysqlError = Mysql::Error
diff --git a/activerecord/lib/active_record/vendor/simple.rb b/activerecord/lib/active_record/vendor/simple.rb
new file mode 100644
index 0000000000..1bd332c882
--- /dev/null
+++ b/activerecord/lib/active_record/vendor/simple.rb
@@ -0,0 +1,702 @@
+# :title: Transaction::Simple
+#
+# == Licence
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#--
+# Transaction::Simple
+# Simple object transaction support for Ruby
+# Version 1.11
+#
+# Copyright (c) 2003 Austin Ziegler
+#
+# $Id: simple.rb,v 1.2 2004/08/20 13:56:37 webster132 Exp $
+#
+# ==========================================================================
+# Revision History ::
+# YYYY.MM.DD Change ID Developer
+# Description
+# --------------------------------------------------------------------------
+# 2003.07.29 Austin Ziegler
+# Added debugging capabilities and VERSION string.
+# 2003.08.21 Austin Ziegler
+# Added named transactions.
+#
+# ==========================================================================
+#++
+require 'thread'
+
+ # The "Transaction" namespace can be used for additional transactional
+ # support objects and modules.
+module Transaction
+
+ # A standard exception for transactional errors.
+ class TransactionError < StandardError; end
+ # A standard exception for transactional errors involving the acquisition
+ # of locks for Transaction::Simple::ThreadSafe.
+ class TransactionThreadError < StandardError; end
+
+ # = Transaction::Simple for Ruby
+ # Simple object transaction support for Ruby
+ #
+ # == Introduction
+ #
+ # Transaction::Simple provides a generic way to add active transactional
+ # support to objects. The transaction methods added by this module will
+ # work with most objects, excluding those that cannot be <i>Marshal</i>ed
+ # (bindings, procedure objects, IO instances, or singleton objects).
+ #
+ # The transactions supported by Transaction::Simple are not backed
+ # transactions; that is, they have nothing to do with any sort of data
+ # store. They are "live" transactions occurring in memory and in the
+ # object itself. This is to allow "test" changes to be made to an object
+ # before making the changes permanent.
+ #
+ # Transaction::Simple can handle an "infinite" number of transactional
+ # levels (limited only by memory). If I open two transactions, commit the
+ # first, but abort the second, the object will revert to the original
+ # version.
+ #
+ # Transaction::Simple supports "named" transactions, so that multiple
+ # levels of transactions can be committed, aborted, or rewound by
+ # referring to the appropriate name of the transaction. Names may be any
+ # object *except* +nil+.
+ #
+ # Copyright:: Copyright © 2003 by Austin Ziegler
+ # Version:: 1.1
+ # Licence:: MIT-Style
+ #
+ # Thanks to David Black for help with the initial concept that led to this
+ # library.
+ #
+ # == Usage
+ # include 'transaction/simple'
+ #
+ # v = "Hello, you." # => "Hello, you."
+ # v.extend(Transaction::Simple) # => "Hello, you."
+ #
+ # v.start_transaction # => ... (a Marshal string)
+ # v.transaction_open? # => true
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ #
+ # v.rewind_transaction # => "Hello, you."
+ # v.transaction_open? # => true
+ #
+ # v.gsub!(/you/, "HAL") # => "Hello, HAL."
+ # v.abort_transaction # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # v.start_transaction # => ... (a Marshal string)
+ # v.start_transaction # => ... (a Marshal string)
+ #
+ # v.transaction_open? # => true
+ # v.gsub!(/you/, "HAL") # => "Hello, HAL."
+ #
+ # v.commit_transaction # => "Hello, HAL."
+ # v.transaction_open? # => true
+ # v.abort_transaction # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # == Named Transaction Usage
+ # v = "Hello, you." # => "Hello, you."
+ # v.extend(Transaction::Simple) # => "Hello, you."
+ #
+ # v.start_transaction(:first) # => ... (a Marshal string)
+ # v.transaction_open? # => true
+ # v.transaction_open?(:first) # => true
+ # v.transaction_open?(:second) # => false
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ #
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ # v.rewind_transaction(:first) # => "Hello, you."
+ # v.transaction_open? # => true
+ # v.transaction_open?(:first) # => true
+ # v.transaction_open?(:second) # => false
+ #
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ # v.transaction_name # => :second
+ # v.abort_transaction(:first) # => "Hello, you."
+ # v.transaction_open? # => false
+ #
+ # v.start_transaction(:first) # => ... (a Marshal string)
+ # v.gsub!(/you/, "world") # => "Hello, world."
+ # v.start_transaction(:second) # => ... (a Marshal string)
+ # v.gsub!(/world/, "HAL") # => "Hello, HAL."
+ #
+ # v.commit_transaction(:first) # => "Hello, HAL."
+ # v.transaction_open? # => false
+ #
+ # == Contraindications
+ #
+ # While Transaction::Simple is very useful, it has some severe limitations
+ # that must be understood. Transaction::Simple:
+ #
+ # * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed cannot
+ # use Transaction::Simple.
+ # * does not manage resources. Resources external to the object and its
+ # instance variables are not managed at all. However, all instance
+ # variables and objects "belonging" to those instance variables are
+ # managed. If there are object reference counts to be handled,
+ # Transaction::Simple will probably cause problems.
+ # * is not inherently thread-safe. In the ACID ("atomic, consistent,
+ # isolated, durable") test, Transaction::Simple provides CD, but it is
+ # up to the user of Transaction::Simple to provide isolation and
+ # atomicity. Transactions should be considered "critical sections" in
+ # multi-threaded applications. If thread safety and atomicity is
+ # absolutely required, use Transaction::Simple::ThreadSafe, which uses a
+ # Mutex object to synchronize the accesses on the object during the
+ # transactional operations.
+ # * does not necessarily maintain Object#__id__ values on rewind or abort.
+ # This may change for future versions that will be Ruby 1.8 or better
+ # *only*. Certain objects that support #replace will maintain
+ # Object#__id__.
+ # * Can be a memory hog if you use many levels of transactions on many
+ # objects.
+ #
+ module Simple
+ VERSION = '1.1.1.0';
+
+ # Sets the Transaction::Simple debug object. It must respond to #<<.
+ # Sets the transaction debug object. Debugging will be performed
+ # automatically if there's a debug object. The generic transaction error
+ # class.
+ def self.debug_io=(io)
+ raise TransactionError, "Transaction Error: the transaction debug object must respond to #<<" unless io.respond_to?(:<<)
+ @tdi = io
+ end
+
+ # Returns the Transaction::Simple debug object. It must respond to #<<.
+ def self.debug_io
+ @tdi
+ end
+
+ # If +name+ is +nil+ (default), then returns +true+ if there is
+ # currently a transaction open.
+ #
+ # If +name+ is specified, then returns +true+ if there is currently a
+ # transaction that responds to +name+ open.
+ def transaction_open?(name = nil)
+ if name.nil?
+ Transaction::Simple.debug_io << "Transaction [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
+ return (not @__transaction_checkpoint__.nil?)
+ else
+ Transaction::Simple.debug_io << "Transaction(#{name.inspect}) [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
+ return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
+ end
+ end
+
+ # Returns the current name of the transaction. Transactions not
+ # explicitly named are named +nil+.
+ def transaction_name
+ raise TransactionError, "Transaction Error: No transaction open." if @__transaction_checkpoint__.nil?
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Transaction Name: #{@__transaction_names__[-1].inspect}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_names__[-1]
+ end
+
+ # Starts a transaction. Stores the current object state. If a
+ # transaction name is specified, the transaction will be named.
+ # Transaction names must be unique. Transaction names of +nil+ will be
+ # treated as unnamed transactions.
+ def start_transaction(name = nil)
+ @__transaction_level__ ||= 0
+ @__transaction_names__ ||= []
+
+ if name.nil?
+ @__transaction_names__ << nil
+ s = ""
+ else
+ raise TransactionError, "Transaction Error: Named transactions must be unique." if @__transaction_names__.include?(name)
+ @__transaction_names__ << name
+ s = "(#{name.inspect})"
+ end
+
+ @__transaction_level__ += 1
+
+ Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} Start Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+
+ @__transaction_checkpoint__ = Marshal.dump(self)
+ end
+
+ # Rewinds the transaction. If +name+ is specified, then the intervening
+ # transactions will be aborted and the named transaction will be
+ # rewound. Otherwise, only the current transaction is rewound.
+ def rewind_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot rewind. There is no current transaction." if @__transaction_checkpoint__.nil?
+ if name.nil?
+ __rewind_this_transaction
+ s = ""
+ else
+ raise TransactionError, "Transaction Error: Cannot rewind to transaction #{name.inspect} because it does not exist." unless @__transaction_names__.include?(name)
+ s = "(#{name})"
+
+ while @__transaction_names__[-1] != name
+ @__transaction_checkpoint__ = __rewind_this_transaction
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ end
+ __rewind_this_transaction
+ end
+ Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ self
+ end
+
+ # Aborts the transaction. Resets the object state to what it was before
+ # the transaction was started and closes the transaction. If +name+ is
+ # specified, then the intervening transactions and the named transaction
+ # will be aborted. Otherwise, only the current transaction is aborted.
+ def abort_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot abort. There is no current transaction." if @__transaction_checkpoint__.nil?
+ if name.nil?
+ __abort_transaction(name)
+ else
+ raise TransactionError, "Transaction Error: Cannot abort nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
+
+ __abort_transaction(name) while @__transaction_names__.include?(name)
+ end
+ self
+ end
+
+ # If +name+ is +nil+ (default), the current transaction level is closed
+ # out and the changes are committed.
+ #
+ # If +name+ is specified and +name+ is in the list of named
+ # transactions, then all transactions are closed and committed until the
+ # named transaction is reached.
+ def commit_transaction(name = nil)
+ raise TransactionError, "Transaction Error: Cannot commit. There is no current transaction." if @__transaction_checkpoint__.nil?
+
+ if name.nil?
+ s = ""
+ __commit_transaction
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ else
+ raise TransactionError, "Transaction Error: Cannot commit nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
+ s = "(#{name})"
+
+ while @__transaction_names__[-1] != name
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ __commit_transaction
+ end
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ __commit_transaction
+ end
+ self
+ end
+
+ # Alternative method for calling the transaction methods. An optional
+ # name can be specified for named transaction support.
+ #
+ # #transaction(:start):: #start_transaction
+ # #transaction(:rewind):: #rewind_transaction
+ # #transaction(:abort):: #abort_transaction
+ # #transaction(:commit):: #commit_transaction
+ # #transaction(:name):: #transaction_name
+ # #transaction:: #transaction_open?
+ def transaction(action = nil, name = nil)
+ case action
+ when :start
+ start_transaction(name)
+ when :rewind
+ rewind_transaction(name)
+ when :abort
+ abort_transaction(name)
+ when :commit
+ commit_transaction(name)
+ when :name
+ transaction_name
+ when nil
+ transaction_open?(name)
+ end
+ end
+
+ def __abort_transaction(name = nil) #:nodoc:
+ @__transaction_checkpoint__ = __rewind_this_transaction
+
+ if name.nil?
+ s = ""
+ else
+ s = "(#{name.inspect})"
+ end
+
+ Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Abort Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ if @__transaction_level__ < 1
+ @__transaction_level__ = 0
+ @__transaction_names__ = []
+ end
+ end
+
+ TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
+ SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
+
+ def __rewind_this_transaction #:nodoc:
+ r = Marshal.restore(@__transaction_checkpoint__)
+
+ begin
+ self.replace(r) if respond_to?(:replace)
+ rescue
+ nil
+ end
+
+ r.instance_variables.each do |i|
+ next if SKIP_TRANSACTION_VARS.include?(i)
+ if respond_to?(:instance_variable_get)
+ instance_variable_set(i, r.instance_variable_get(i))
+ else
+ instance_eval(%q|#{i} = r.instance_eval("#{i}")|)
+ end
+ end
+
+ if respond_to?(:instance_variable_get)
+ return r.instance_variable_get(TRANSACTION_CHECKPOINT)
+ else
+ return r.instance_eval(TRANSACTION_CHECKPOINT)
+ end
+ end
+
+ def __commit_transaction #:nodoc:
+ if respond_to?(:instance_variable_get)
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
+ else
+ @__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
+ end
+
+ @__transaction_level__ -= 1
+ @__transaction_names__.pop
+ if @__transaction_level__ < 1
+ @__transaction_level__ = 0
+ @__transaction_names__ = []
+ end
+ end
+
+ private :__abort_transaction, :__rewind_this_transaction, :__commit_transaction
+
+ # = Transaction::Simple::ThreadSafe
+ # Thread-safe simple object transaction support for Ruby.
+ # Transaction::Simple::ThreadSafe is used in the same way as
+ # Transaction::Simple. Transaction::Simple::ThreadSafe uses a Mutex
+ # object to ensure atomicity at the cost of performance in threaded
+ # applications.
+ #
+ # Transaction::Simple::ThreadSafe will not wait to obtain a lock; if the
+ # lock cannot be obtained immediately, a
+ # Transaction::TransactionThreadError will be raised.
+ #
+ # Thanks to Mauricio Fernández for help with getting this part working.
+ module ThreadSafe
+ VERSION = '1.1.1.0';
+
+ include Transaction::Simple
+
+ SKIP_TRANSACTION_VARS = Transaction::Simple::SKIP_TRANSACTION_VARS.dup #:nodoc:
+ SKIP_TRANSACTION_VARS << "@__transaction_mutex__"
+
+ Transaction::Simple.instance_methods(false) do |meth|
+ next if meth == "transaction"
+ arg = "(name = nil)" unless meth == "transaction_name"
+ module_eval <<-EOS
+ def #{meth}#{arg}
+ if (@__transaction_mutex__ ||= Mutex.new).try_lock
+ result = super
+ @__transaction_mutex__.unlock
+ return result
+ else
+ raise TransactionThreadError, "Transaction Error: Cannot obtain lock for ##{meth}"
+ end
+ ensure
+ @__transaction_mutex__.unlock
+ end
+ EOS
+ end
+ end
+ end
+end
+
+if $0 == __FILE__
+ require 'test/unit'
+
+ class Test__Transaction_Simple < Test::Unit::TestCase #:nodoc:
+ VALUE = "Now is the time for all good men to come to the aid of their country."
+
+ def setup
+ @value = VALUE.dup
+ @value.extend(Transaction::Simple)
+ end
+
+ def test_extended
+ assert_respond_to(@value, :start_transaction)
+ end
+
+ def test_started
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ end
+
+ def test_rewind
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.rewind_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_abort
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_commit
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_not_equal(VALUE, @value)
+ end
+
+ def test_multilevel
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(VALUE, @value)
+ end
+
+ def test_multilevel_named
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.transaction_name }
+ assert_nothing_raised { @value.start_transaction(:first) } # 1
+ assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(:first, @value.transaction_name)
+ assert_nothing_raised { @value.start_transaction } # 2
+ assert_not_equal(:first, @value.transaction_name)
+ assert_equal(nil, @value.transaction_name)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
+ assert_nothing_raised { @value.abort_transaction(:first) }
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised do
+ @value.start_transaction(:first)
+ @value.gsub!(/men/, 'women')
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_nothing_raised { @value.abort_transaction(:second) }
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
+ assert_nothing_raised { @value.rewind_transaction(:second) }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
+ assert_nothing_raised { @value.commit_transaction(:first) }
+ assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
+ assert_equal(false, @value.transaction_open?)
+ end
+
+ def test_array
+ assert_nothing_raised do
+ @orig = ["first", "second", "third"]
+ @value = ["first", "second", "third"]
+ @value.extend(Transaction::Simple)
+ end
+ assert_equal(@orig, @value)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
+ assert_not_equal(@orig, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(@orig, @value)
+ end
+ end
+
+ class Test__Transaction_Simple_ThreadSafe < Test::Unit::TestCase #:nodoc:
+ VALUE = "Now is the time for all good men to come to the aid of their country."
+
+ def setup
+ @value = VALUE.dup
+ @value.extend(Transaction::Simple::ThreadSafe)
+ end
+
+ def test_extended
+ assert_respond_to(@value, :start_transaction)
+ end
+
+ def test_started
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ end
+
+ def test_rewind
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.rewind_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_abort
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_equal(VALUE, @value)
+ end
+
+ def test_commit
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction }
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_not_equal(VALUE, @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(false, @value.transaction_open?)
+ assert_not_equal(VALUE, @value)
+ end
+
+ def test_multilevel
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.gsub!(/men/, 'women') }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.start_transaction }
+ assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
+ assert_nothing_raised { @value.commit_transaction }
+ assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(VALUE, @value)
+ end
+
+ def test_multilevel_named
+ assert_equal(false, @value.transaction_open?)
+ assert_raises(Transaction::TransactionError) { @value.transaction_name }
+ assert_nothing_raised { @value.start_transaction(:first) } # 1
+ assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
+ assert_equal(true, @value.transaction_open?)
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(:first, @value.transaction_name)
+ assert_nothing_raised { @value.start_transaction } # 2
+ assert_not_equal(:first, @value.transaction_name)
+ assert_equal(nil, @value.transaction_name)
+ assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
+ assert_nothing_raised { @value.abort_transaction(:first) }
+ assert_equal(false, @value.transaction_open?)
+ assert_nothing_raised do
+ @value.start_transaction(:first)
+ @value.gsub!(/men/, 'women')
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_nothing_raised { @value.abort_transaction(:second) }
+ assert_equal(true, @value.transaction_open?(:first))
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.start_transaction(:second)
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
+ assert_nothing_raised { @value.rewind_transaction(:second) }
+ assert_equal(VALUE.gsub(/men/, 'women'), @value)
+ assert_nothing_raised do
+ @value.gsub!(/women/, 'people')
+ @value.start_transaction
+ @value.gsub!(/people/, 'sentients')
+ end
+ assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
+ assert_nothing_raised { @value.commit_transaction(:first) }
+ assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
+ assert_equal(false, @value.transaction_open?)
+ end
+
+ def test_array
+ assert_nothing_raised do
+ @orig = ["first", "second", "third"]
+ @value = ["first", "second", "third"]
+ @value.extend(Transaction::Simple::ThreadSafe)
+ end
+ assert_equal(@orig, @value)
+ assert_nothing_raised { @value.start_transaction }
+ assert_equal(true, @value.transaction_open?)
+ assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
+ assert_not_equal(@orig, @value)
+ assert_nothing_raised { @value.abort_transaction }
+ assert_equal(@orig, @value)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/wrappers/yaml_wrapper.rb b/activerecord/lib/active_record/wrappers/yaml_wrapper.rb
new file mode 100644
index 0000000000..74f40a507c
--- /dev/null
+++ b/activerecord/lib/active_record/wrappers/yaml_wrapper.rb
@@ -0,0 +1,15 @@
+require 'yaml'
+
+module ActiveRecord
+ module Wrappings #:nodoc:
+ class YamlWrapper < AbstractWrapper #:nodoc:
+ def wrap(attribute) attribute.to_yaml end
+ def unwrap(attribute) YAML::load(attribute) end
+ end
+
+ module ClassMethods #:nodoc:
+ # Wraps the attribute in Yaml encoding
+ def wrap_in_yaml(*attributes) wrap_with(YamlWrapper, attributes) end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/wrappings.rb b/activerecord/lib/active_record/wrappings.rb
new file mode 100644
index 0000000000..43e5e3151d
--- /dev/null
+++ b/activerecord/lib/active_record/wrappings.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ # A plugin framework for wrapping attribute values before they go in and unwrapping them after they go out of the database.
+ # This was intended primarily for YAML wrapping of arrays and hashes, but this behavior is now native in the Base class.
+ # So for now this framework is laying dorment until a need pops up.
+ module Wrappings #:nodoc:
+ module ClassMethods #:nodoc:
+ def wrap_with(wrapper, *attributes)
+ [ attributes ].flat.each { |attribute| wrapper.wrap(attribute) }
+ end
+ end
+
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ class AbstractWrapper #:nodoc:
+ def self.wrap(attribute, record_binding) #:nodoc:
+ %w( before_save after_save after_initialize ).each do |callback|
+ eval "#{callback} #{name}.new('#{attribute}')", record_binding
+ end
+ end
+
+ def initialize(attribute) #:nodoc:
+ @attribute = attribute
+ end
+
+ def save_wrapped_attribute(record) #:nodoc:
+ if record.attribute_present?(@attribute)
+ record.send(
+ "write_attribute",
+ @attribute,
+ wrap(record.send("read_attribute", @attribute))
+ )
+ end
+ end
+
+ def load_wrapped_attribute(record) #:nodoc:
+ if record.attribute_present?(@attribute)
+ record.send(
+ "write_attribute",
+ @attribute,
+ unwrap(record.send("read_attribute", @attribute))
+ )
+ end
+ end
+
+ alias_method :before_save, :save_wrapped_attribute #:nodoc:
+ alias_method :after_save, :load_wrapped_attribute #:nodoc:
+ alias_method :after_initialize, :after_save #:nodoc:
+
+ # Overwrite to implement the logic that'll take the regular attribute and wrap it.
+ def wrap(attribute) end
+
+ # Overwrite to implement the logic that'll take the wrapped attribute and unwrap it.
+ def unwrap(attribute) end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/abstract_unit.rb b/activerecord/test/abstract_unit.rb
new file mode 100755
index 0000000000..1b33579206
--- /dev/null
+++ b/activerecord/test/abstract_unit.rb
@@ -0,0 +1,22 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')#.unshift(File.dirname(__FILE__))
+
+# Make rubygems available for testing if possible
+begin require('rubygems'); rescue LoadError; end
+begin require('dev-utils/debug'); rescue LoadError; end
+
+require 'test/unit'
+require 'active_record'
+require 'active_record/fixtures'
+require 'connection'
+
+class Test::Unit::TestCase #:nodoc:
+ def create_fixtures(*table_names)
+ if block_given?
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names) { yield }
+ else
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names)
+ end
+ end
+end
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" \ No newline at end of file
diff --git a/activerecord/test/aggregations_test.rb b/activerecord/test/aggregations_test.rb
new file mode 100644
index 0000000000..2eff36dc73
--- /dev/null
+++ b/activerecord/test/aggregations_test.rb
@@ -0,0 +1,34 @@
+require 'abstract_unit'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/customer'
+
+class AggregationsTest < Test::Unit::TestCase
+ def setup
+ @customers = create_fixtures "customers"
+ @david = Customer.find(1)
+ end
+
+ def test_find_single_value_object
+ assert_equal 50, @david.balance.amount
+ assert_kind_of Money, @david.balance
+ assert_equal 300, @david.balance.exchange_to("DKK").amount
+ end
+
+ def test_find_multiple_value_object
+ assert_equal @customers["david"]["address_street"], @david.address.street
+ assert(
+ @david.address.close_to?(Address.new("Different Street", @customers["david"]["address_city"], @customers["david"]["address_country"]))
+ )
+ end
+
+ def test_change_single_value_object
+ @david.balance = Money.new(100)
+ @david.save
+ assert_equal 100, Customer.find(1).balance.amount
+ end
+
+ def test_immutable_value_objects
+ @david.balance = Money.new(100)
+ assert_raises(TypeError) { @david.balance.instance_eval { @amount = 20 } }
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/all.sh b/activerecord/test/all.sh
new file mode 100755
index 0000000000..a6712cc48e
--- /dev/null
+++ b/activerecord/test/all.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 connections/<db_library>" 1>&2
+ exit 1
+fi
+
+ruby -I $1 -e 'Dir.foreach(".") { |file| require file if file =~ /_test.rb$/ }'
diff --git a/activerecord/test/associations_test.rb b/activerecord/test/associations_test.rb
new file mode 100755
index 0000000000..2eb6ba267e
--- /dev/null
+++ b/activerecord/test/associations_test.rb
@@ -0,0 +1,549 @@
+require 'abstract_unit'
+require 'fixtures/developer'
+require 'fixtures/project'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/company'
+require 'fixtures/topic'
+require 'fixtures/reply'
+
+# Can't declare new classes in test case methods, so tests before that
+bad_collection_keys = false
+begin
+ class Car < ActiveRecord::Base; has_many :wheels, :name => "wheels"; end
+rescue ActiveRecord::ActiveRecordError
+ bad_collection_keys = true
+end
+raise "ActiveRecord should have barked on bad collection keys" unless bad_collection_keys
+
+
+class AssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_force_reload
+ firm = Firm.new
+ firm.save
+ firm.clients.each {|c|} # forcing to load all clients
+ assert firm.clients.empty?, "New firm shouldn't have client objects"
+ assert !firm.has_clients?, "New firm shouldn't have clients"
+ assert_equal 0, firm.clients.size, "New firm should have 0 clients"
+
+ client = Client.new("firm_id" => firm.id)
+ client.save
+
+ assert firm.clients.empty?, "New firm should have cached no client objects"
+ assert !firm.has_clients?, "New firm should have cached a no-clients response"
+ assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
+
+ assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
+ assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
+ end
+
+ def test_storing_in_pstore
+ require "tmpdir"
+ store_filename = File.join(Dir.tmpdir, "ar-pstore-association-test")
+ File.delete(store_filename) if File.exists?(store_filename)
+ require "pstore"
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+
+ db = PStore.new(store_filename)
+ db.transaction do
+ db["apple"] = apple
+ end
+
+ db = PStore.new(store_filename)
+ db.transaction do
+ assert_equal "Natural Company", db["apple"].clients.first.name
+ end
+ end
+end
+
+class HasOneAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_has_one
+ assert_equal @signals37.account, Account.find(1)
+ assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
+ assert @signals37.has_account?, "37signals should have an account"
+ assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
+ assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
+
+ assert !Account.find(2).has_firm?, "Unknown isn't linked"
+ assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
+ end
+
+ def test_type_mismatch
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.account = 1 }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.account = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ old_account_id = @signals37.account.id
+ @signals37.account = nil
+ @signals37.save
+ assert_nil @signals37.account
+ assert_nil Account.find(old_account_id).firm_id
+ end
+
+ def test_build
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account
+ assert !account.save
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_create
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ assert_equal firm.create_account("credit_limit" => 1000), firm.account
+ end
+
+ def test_dependence
+ firm = Firm.find(1)
+ assert !firm.account.nil?
+ firm.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_dependence_with_missing_association
+ Account.destroy_all
+ firm = Firm.find(1)
+ assert !firm.has_account?
+ firm.destroy
+ end
+end
+
+
+class HasManyAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects", "topics"
+ @signals37 = Firm.find(1)
+ end
+
+ def force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.each {|f| }
+ end
+
+ def test_finding
+ assert_equal 2, Firm.find_first.clients.length
+ end
+
+ def test_finding_default_orders
+ assert_equal "Summit", Firm.find_first.clients.first.name
+ end
+
+ def test_finding_with_different_class_name_and_order
+ assert_equal "Microsoft", Firm.find_first.clients_sorted_desc.first.name
+ end
+
+ def test_finding_with_foreign_key
+ assert_equal "Microsoft", Firm.find_first.clients_of_firm.first.name
+ end
+
+ def test_finding_with_condition
+ assert_equal "Microsoft", Firm.find_first.clients_like_ms.first.name
+ end
+
+ def test_finding_using_sql
+ firm = Firm.find_first
+ first_client = firm.clients_using_sql.first
+ assert_not_nil first_client
+ assert_equal "Microsoft", first_client.name
+ assert_equal 1, firm.clients_using_sql.size
+ assert_equal 1, Firm.find_first.clients_using_sql.size
+ end
+
+ def test_find_all
+ assert_equal 2, Firm.find_first.clients.find_all("type = 'Client'").length
+ assert_equal 1, Firm.find_first.clients.find_all("name = 'Summit'").length
+ end
+
+ def test_find_all_sanitized
+ firm = Firm.find_first
+ assert_equal firm.clients.find_all("name = 'Summit'"), firm.clients.find_all(["name = '%s'", "Summit"])
+ end
+
+ def test_find_in_collection
+ assert_equal Client.find(2).name, @signals37.clients.find(2).name
+ assert_equal Client.find(2).name, @signals37.clients.find {|c| c.name == @signals37.clients.find(2).name }.name
+ assert_raises(ActiveRecord::RecordNotFound) { @signals37.clients.find(6) }
+ end
+
+ def test_adding
+ force_signal37_to_load_all_clients_of_firm
+ natural = Client.new("name" => "Natural Company")
+ @signals37.clients_of_firm << natural
+ assert_equal 2, @signals37.clients_of_firm.size # checking via the collection
+ assert_equal 2, @signals37.clients_of_firm(true).size # checking using the db
+ assert_equal natural, @signals37.clients_of_firm.last
+ end
+
+ def test_adding_a_mismatch_class
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.clients_of_firm << nil }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.clients_of_firm << 1 }
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { @signals37.clients_of_firm << Topic.find(1) }
+ end
+
+ def test_adding_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+ assert_equal 3, @signals37.clients_of_firm.size
+ assert_equal 3, @signals37.clients_of_firm(true).size
+ end
+
+ def test_build
+ new_client = @signals37.clients_of_firm.build("name" => "Another Client")
+ assert_equal "Another Client", new_client.name
+ assert new_client.save
+ assert_equal 2, @signals37.clients_of_firm(true).size
+ end
+
+ def test_create
+ force_signal37_to_load_all_clients_of_firm
+ new_client = @signals37.clients_of_firm.create("name" => "Another Client")
+ assert_equal new_client, @signals37.clients_of_firm.last
+ assert_equal new_client, @signals37.clients_of_firm(true).last
+ end
+
+ def test_deleting
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.delete(@signals37.clients_of_firm.first)
+ assert_equal 0, @signals37.clients_of_firm.size
+ assert_equal 0, @signals37.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_collection
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, @signals37.clients_of_firm.size
+ #@signals37.clients_of_firm.clear
+ @signals37.clients_of_firm.delete([@signals37.clients_of_firm[0], @signals37.clients_of_firm[1]])
+ assert_equal 0, @signals37.clients_of_firm.size
+ assert_equal 0, @signals37.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_association_collection
+ force_signal37_to_load_all_clients_of_firm
+ @signals37.clients_of_firm.create("name" => "Another Client")
+ assert_equal 2, @signals37.clients_of_firm.size
+ @signals37.clients_of_firm.clear
+ assert_equal 0, @signals37.clients_of_firm.size
+ assert_equal 0, @signals37.clients_of_firm(true).size
+ end
+
+ def test_deleting_a_item_which_is_not_in_the_collection
+ force_signal37_to_load_all_clients_of_firm
+ summit = Client.find_first("name = 'Summit'")
+ @signals37.clients_of_firm.delete(summit)
+ assert_equal 1, @signals37.clients_of_firm.size
+ assert_equal 1, @signals37.clients_of_firm(true).size
+ assert_equal 2, summit.client_of
+ end
+
+ def test_deleting_type_mismatch
+ david = Developer.find(1)
+ david.projects.id
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
+ end
+
+ def test_deleting_self_type_mismatch
+ david = Developer.find(1)
+ david.projects.id
+ assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ end
+
+ def test_destroy_all
+ force_signal37_to_load_all_clients_of_firm
+ assert !@signals37.clients_of_firm.empty?, "37signals has clients after load"
+ @signals37.clients_of_firm.destroy_all
+ assert @signals37.clients_of_firm.empty?, "37signals has no clients after destroy all"
+ assert @signals37.clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
+ end
+
+ def test_dependence
+ assert_equal 2, Client.find_all.length
+ Firm.find_first.destroy
+ assert_equal 0, Client.find_all.length
+ end
+
+ def test_dependence_with_transaction_support_on_failure
+ assert_equal 2, Client.find_all.length
+ firm = Firm.find_first
+ clients = firm.clients
+ clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end }
+
+ firm.destroy rescue "do nothing"
+
+ assert_equal 2, Client.find_all.length
+ end
+
+ def test_dependence_on_account
+ assert_equal 2, Account.find_all.length
+ @signals37.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_included_in_collection
+ assert @signals37.clients.include?(Client.find(2))
+ end
+
+ def test_adding_array_and_collection
+ assert_nothing_raised { Firm.find_first.clients + Firm.find_all.last.clients }
+ end
+end
+
+class BelongsToAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects", "topics"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_belongs_to
+ Client.find(3).firm.name
+ assert_equal @signals37.name, Client.find(3).firm.name
+ assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
+ end
+
+ def test_type_mismatch
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
+ end
+
+ def test_natural_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = apple
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_to_nil
+ client = Client.find(3)
+ client.firm = nil
+ client.save
+ assert_nil client.firm(true)
+ assert_nil client.client_of
+ end
+
+ def test_with_different_class_name
+ assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
+ assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
+ end
+
+ def test_with_condition
+ assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
+ assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm"
+ end
+
+ def test_belongs_to_counter
+ debate = Topic.create("title" => "debate")
+ assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+
+ trash = debate.replies.create("title" => "blah!", "content" => "world around!")
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ trash.destroy
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
+ def xtest_counter_cache
+ apple = Firm.create("name" => "Apple")
+ final_cut = apple.clients.create("name" => "Final Cut")
+
+ apple.clients.to_s
+ assert_equal 1, apple.clients.size, "Created one client"
+
+ apple.companies_count = 2
+ apple.save
+
+ apple = Firm.find_first("name = 'Apple'")
+ assert_equal 2, apple.clients.size, "Should use the new cached number"
+
+ apple.clients.to_s
+ assert_equal 1, apple.clients.size, "Should not use the cached number, but go to the database"
+ end
+end
+
+
+class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase
+ def setup
+ @accounts, @companies, @developers, @projects, @developers_projects =
+ create_fixtures "accounts", "companies", "developers", "projects", "developers_projects"
+
+ @signals37 = Firm.find(1)
+ end
+
+ def test_has_and_belongs_to_many
+ david = Developer.find(1)
+
+ assert !david.projects.empty?
+ assert_equal 2, david.projects.size
+
+ active_record = Project.find(1)
+ assert !active_record.developers.empty?
+ assert_equal 2, active_record.developers.size
+ assert_equal david.name, active_record.developers.first.name
+ end
+
+ def test_adding_single
+ jamis = Developer.find(2)
+ jamis.projects.id # causing the collection to load
+ action_controller = Project.find(2)
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ jamis.projects << action_controller
+
+ assert_equal 2, jamis.projects.size
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_type_mismatch
+ jamis = Developer.find(2)
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { jamis.projects << 1 }
+ end
+
+ def test_adding_from_the_project
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+ action_controller.developers.id
+ assert_equal 1, jamis.projects.size
+ assert_equal 1, action_controller.developers.size
+
+ action_controller.developers << jamis
+
+ assert_equal 2, jamis.projects(true).size
+ assert_equal 2, action_controller.developers.size
+ assert_equal 2, action_controller.developers(true).size
+ end
+
+ def test_adding_multiple
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.save
+ aridridel.projects.id
+ aridridel.projects.push(Project.find(1), Project.find(2))
+ assert_equal 2, aridridel.projects.size
+ assert_equal 2, aridridel.projects(true).size
+ end
+
+ def test_adding_a_collection
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.save
+ aridridel.projects.id
+ aridridel.projects.concat([Project.find(1), Project.find(2)])
+ assert_equal 2, aridridel.projects.size
+ assert_equal 2, aridridel.projects(true).size
+ end
+
+ def test_uniq_after_the_fact
+ @developers["jamis"].find.projects << @projects["active_record"].find
+ @developers["jamis"].find.projects << @projects["active_record"].find
+ assert_equal 3, @developers["jamis"].find.projects.size
+ assert_equal 1, @developers["jamis"].find.projects.uniq.size
+ end
+
+ def test_uniq_before_the_fact
+ @projects["active_record"].find.developers << @developers["jamis"].find
+ @projects["active_record"].find.developers << @developers["david"].find
+ assert_equal 2, @projects["active_record"].find.developers.size
+ end
+
+ def test_deleting
+ david = Developer.find(1)
+ active_record = Project.find(1)
+ david.projects.id
+ assert_equal 2, david.projects.size
+ assert_equal 2, active_record.developers.size
+
+ david.projects.delete(active_record)
+
+ assert_equal 1, david.projects.size
+ assert_equal 1, david.projects(true).size
+ assert_equal 1, active_record.developers(true).size
+ end
+
+ def test_deleting_array
+ david = Developer.find(1)
+ david.projects.id
+ david.projects.delete(Project.find_all)
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
+ def test_deleting_all
+ david = Developer.find(1)
+ david.projects.id
+ david.projects.clear
+ assert_equal 0, david.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
+ def test_removing_associations_on_destroy
+ Developer.find(1).destroy
+ assert Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = '1'").empty?
+ end
+
+ def test_additional_columns_from_join_table
+ assert_equal Date.new(2004, 10, 10).to_s, Developer.find(1).projects.first.joined_on.to_s
+ end
+
+ def test_destroy_all
+ david = Developer.find(1)
+ david.projects.id
+ assert !david.projects.empty?
+ david.projects.destroy_all
+ assert david.projects.empty?
+ assert david.projects(true).empty?
+ end
+
+ def test_rich_association
+ @jamis = @developers["jamis"].find
+ @jamis.projects.push_with_attributes(@projects["action_controller"].find, :joined_on => Date.today)
+ assert_equal Date.today.to_s, @jamis.projects.select { |p| p.name == @projects["action_controller"]["name"] }.first.joined_on.to_s
+ assert_equal Date.today.to_s, @developers["jamis"].find.projects.select { |p| p.name == @projects["action_controller"]["name"] }.first.joined_on.to_s
+ end
+
+ def test_associations_with_conditions
+ assert_equal 2, @projects["active_record"].find.developers.size
+ assert_equal 1, @projects["active_record"].find.developers_named_david.size
+
+ @projects["active_record"].find.developers_named_david.clear
+ assert_equal 1, @projects["active_record"].find.developers.size
+ end
+
+ def test_find_in_association
+ # Using sql
+ assert_equal @developers["david"].find, @projects["active_record"].find.developers.find(@developers["david"]["id"]), "SQL find"
+
+ # Using ruby
+ @active_record = @projects["active_record"].find
+ @active_record.developers.reload
+ assert_equal @developers["david"].find, @active_record.developers.find(@developers["david"]["id"]), "Ruby find"
+ end
+end
diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb
new file mode 100755
index 0000000000..0d2278eb58
--- /dev/null
+++ b/activerecord/test/base_test.rb
@@ -0,0 +1,544 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/company'
+require 'fixtures/default'
+require 'fixtures/auto_id'
+require 'fixtures/column_name'
+
+class Category < ActiveRecord::Base; end
+class Smarts < ActiveRecord::Base; end
+class CreditCard < ActiveRecord::Base; end
+class MasterCreditCard < ActiveRecord::Base; end
+
+class LoosePerson < ActiveRecord::Base
+ attr_protected :credit_rating, :administrator
+end
+
+class TightPerson < ActiveRecord::Base
+ attr_accessible :name, :address
+end
+
+class TightDescendent < TightPerson
+ attr_accessible :phone_number
+end
+
+class Booleantest < ActiveRecord::Base; end
+
+class BasicsTest < Test::Unit::TestCase
+ def setup
+ @topic_fixtures, @companies = create_fixtures "topics", "companies"
+ end
+
+ def test_set_attributes
+ topic = Topic.find(1)
+ topic.attributes = { "title" => "Budget", "author_name" => "Jason" }
+ topic.save
+ assert_equal("Budget", topic.title)
+ assert_equal("Jason", topic.author_name)
+ assert_equal(@topic_fixtures["first"]["author_email_address"], Topic.find(1).author_email_address)
+ end
+
+ def test_integers_as_nil
+ Topic.update(1, "approved" => "")
+ assert_nil Topic.find(1).approved
+ end
+
+ def test_set_attributes_with_block
+ topic = Topic.new do |t|
+ t.title = "Budget"
+ t.author_name = "Jason"
+ end
+
+ assert_equal("Budget", topic.title)
+ assert_equal("Jason", topic.author_name)
+ end
+
+ def test_respond_to?
+ topic = Topic.find(1)
+ assert topic.respond_to?("title")
+ assert topic.respond_to?("title?")
+ assert topic.respond_to?("title=")
+ assert topic.respond_to?(:title)
+ assert topic.respond_to?(:title?)
+ assert topic.respond_to?(:title=)
+ assert topic.respond_to?("author_name")
+ assert topic.respond_to?("attribute_names")
+ assert !topic.respond_to?("nothingness")
+ assert !topic.respond_to?(:nothingness)
+ end
+
+ def test_array_content
+ topic = Topic.new
+ topic.content = %w( one two three )
+ topic.save
+
+ assert_equal(%w( one two three ), Topic.find(topic.id).content)
+ end
+
+ def test_hash_content
+ topic = Topic.new
+ topic.content = { "one" => 1, "two" => 2 }
+ topic.save
+
+ assert_equal 2, Topic.find(topic.id).content["two"]
+
+ topic.content["three"] = 3
+ topic.save
+
+ assert_equal 3, Topic.find(topic.id).content["three"]
+ end
+
+ def test_update_array_content
+ topic = Topic.new
+ topic.content = %w( one two three )
+
+ topic.content.push "four"
+ assert_equal(%w( one two three four ), topic.content)
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ topic.content << "five"
+ assert_equal(%w( one two three four five ), topic.content)
+ end
+
+ def test_create
+ topic = Topic.new
+ topic.title = "New Topic"
+ topic.save
+ id = topic.id
+ topicReloaded = Topic.find(id)
+ assert_equal("New Topic", topicReloaded.title)
+ end
+
+ def test_create_through_factory
+ topic = Topic.create("title" => "New Topic")
+ topicReloaded = Topic.find(topic.id)
+ assert_equal(topic, topicReloaded)
+ end
+
+ def test_update
+ topic = Topic.new
+ topic.title = "Another New Topic"
+ topic.written_on = "2003-12-12 23:23"
+ topic.save
+ id = topic.id
+ assert_equal(id, topic.id)
+
+ topicReloaded = Topic.find(id)
+ assert_equal("Another New Topic", topicReloaded.title)
+
+ topicReloaded.title = "Updated topic"
+ topicReloaded.save
+
+ topicReloadedAgain = Topic.find(id)
+
+ assert_equal("Updated topic", topicReloadedAgain.title)
+ end
+
+ def test_preserving_date_objects
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
+ end
+
+ assert_kind_of(
+ Date, Topic.find(1).last_read,
+ "The last_read attribute should be of the Date class"
+ )
+ end
+
+ def test_preserving_time_objects
+ assert_kind_of(
+ Time, Topic.find(1).written_on,
+ "The written_on attribute should be of the Time class"
+ )
+ end
+
+ def test_destroy
+ topic = Topic.new
+ topic.title = "Yet Another New Topic"
+ topic.written_on = "2003-12-12 23:23"
+ topic.save
+ id = topic.id
+ topic.destroy
+
+ assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(id) }
+ end
+
+ def test_record_not_found_exception
+ assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(id) }
+ end
+
+ def test_initialize_with_attributes
+ topic = Topic.new({
+ "title" => "initialized from attributes", "written_on" => "2003-12-12 23:23"
+ })
+
+ assert_equal("initialized from attributes", topic.title)
+ end
+
+ def test_load
+ topics = Topic.find_all nil, "id"
+ assert_equal(2, topics.size)
+ assert_equal(@topic_fixtures["first"]["title"], topics.first.title)
+ end
+
+ def test_load_with_condition
+ topics = Topic.find_all "author_name = 'Mary'"
+
+ assert_equal(1, topics.size)
+ assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
+ end
+
+ def test_table_name_guesses
+ assert_equal "topics", Topic.table_name
+
+ assert_equal "categories", Category.table_name
+ assert_equal "smarts", Smarts.table_name
+ assert_equal "credit_cards", CreditCard.table_name
+ assert_equal "master_credit_cards", MasterCreditCard.table_name
+
+ ActiveRecord::Base.pluralize_table_names = false
+ assert_equal "category", Category.table_name
+ assert_equal "smarts", Smarts.table_name
+ assert_equal "credit_card", CreditCard.table_name
+ assert_equal "master_credit_card", MasterCreditCard.table_name
+ ActiveRecord::Base.pluralize_table_names = true
+
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "test_categories", Category.table_name
+ ActiveRecord::Base.table_name_suffix = "_test"
+ assert_equal "test_categories_test", Category.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "categories_test", Category.table_name
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "categories", Category.table_name
+
+ ActiveRecord::Base.pluralize_table_names = false
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "test_category", Category.table_name
+ ActiveRecord::Base.table_name_suffix = "_test"
+ assert_equal "test_category_test", Category.table_name
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "category_test", Category.table_name
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "category", Category.table_name
+ ActiveRecord::Base.pluralize_table_names = true
+ end
+
+ def test_destroy_all
+ assert_equal(2, Topic.find_all.size)
+
+ Topic.destroy_all "author_name = 'Mary'"
+ assert_equal(1, Topic.find_all.size)
+ end
+
+ def test_boolean_attributes
+ assert ! Topic.find(1).approved?
+ assert Topic.find(2).approved?
+ end
+
+ def test_increment_counter
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 1, Topic.find(1).replies_count
+
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 2, Topic.find(1).replies_count
+ end
+
+ def test_decrement_counter
+ Topic.decrement_counter("replies_count", 2)
+ assert_equal 1, Topic.find(2).replies_count
+
+ Topic.decrement_counter("replies_count", 2)
+ assert_equal 0, Topic.find(1).replies_count
+ end
+
+ def test_update_all
+ Topic.update_all "content = 'bulk updated!'"
+ assert_equal "bulk updated!", Topic.find(1).content
+ assert_equal "bulk updated!", Topic.find(2).content
+ end
+
+ def test_update_by_condition
+ Topic.update_all "content = 'bulk updated!'", "approved = 1"
+ assert_equal "Have a nice day", Topic.find(1).content
+ assert_equal "bulk updated!", Topic.find(2).content
+ end
+
+ def test_attribute_present
+ t = Topic.new
+ t.title = "hello there!"
+ t.written_on = Time.now
+ assert t.attribute_present?("title")
+ assert t.attribute_present?("written_on")
+ assert !t.attribute_present?("content")
+ end
+
+ def test_attribute_keys_on_new_instance
+ t = Topic.new
+ assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
+ assert_raises(NoMethodError) { t.title2 }
+ end
+
+ def test_class_name
+ assert_equal "Firm", ActiveRecord::Base.class_name("firms")
+ assert_equal "Category", ActiveRecord::Base.class_name("categories")
+ assert_equal "AccountHolder", ActiveRecord::Base.class_name("account_holder")
+
+ ActiveRecord::Base.pluralize_table_names = false
+ assert_equal "Firms", ActiveRecord::Base.class_name( "firms" )
+ ActiveRecord::Base.pluralize_table_names = true
+
+ ActiveRecord::Base.table_name_prefix = "test_"
+ assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms" )
+ ActiveRecord::Base.table_name_suffix = "_tests"
+ assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms_tests" )
+ ActiveRecord::Base.table_name_prefix = ""
+ assert_equal "Firm", ActiveRecord::Base.class_name( "firms_tests" )
+ ActiveRecord::Base.table_name_suffix = ""
+ assert_equal "Firm", ActiveRecord::Base.class_name( "firms" )
+ end
+
+ def test_null_fields
+ assert_nil Topic.find(1).parent_id
+ assert_nil Topic.create("title" => "Hey you").parent_id
+ end
+
+ def test_default_values
+ topic = Topic.new
+ assert_equal 1, topic.approved
+ assert_nil topic.written_on
+ assert_nil topic.last_read
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ assert_equal 1, topic.approved
+ assert_nil topic.last_read
+ end
+
+ def test_default_values_on_empty_strings
+ topic = Topic.new
+ topic.approved = nil
+ topic.last_read = nil
+
+ topic.save
+
+ topic = Topic.find(topic.id)
+ assert_nil topic.last_read
+ assert_nil topic.approved
+ end
+
+ def test_equality
+ assert_equal Topic.find(1), Topic.find(2).parent
+ end
+
+ def test_hashing
+ assert_equal [ Topic.find(1) ], [ Topic.find(2).parent ] & [ Topic.find(1) ]
+ end
+
+ def test_destroy_new_record
+ client = Client.new
+ client.destroy
+ assert client.frozen?
+ end
+
+ def test_update_attribute
+ assert !Topic.find(1).approved?
+ Topic.find(1).update_attribute("approved", true)
+ assert Topic.find(1).approved?
+ end
+
+ def test_mass_assignment_protection
+ firm = Firm.new
+ firm.attributes = { "name" => "Next Angle", "rating" => 5 }
+ assert_equal 1, firm.rating
+ end
+
+ def test_mass_assignment_accessible
+ reply = Reply.new("title" => "hello", "content" => "world", "approved" => 0)
+ reply.save
+
+ assert_equal 1, reply.approved
+
+ reply.approved = 0
+ reply.save
+
+ assert_equal 0, reply.approved
+ end
+
+ def test_mass_assignment_protection_inheritance
+ assert_equal [ :credit_rating, :administrator ], LoosePerson.protected_attributes
+ assert_nil TightPerson.protected_attributes
+ end
+
+ def test_multiparameter_attributes_on_date
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
+ end
+
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Date.new(2004, 6, 24).to_s, topic.last_read.to_s
+ end
+
+ def test_multiparameter_attributes_on_date_with_empty_date
+ # SQL Server doesn't have a separate column type just for dates, so all are returned as time
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
+ end
+
+ attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Date.new(2004, 6, 1).to_s, topic.last_read.to_s
+ end
+
+ def test_multiparameter_attributes_on_date_with_all_empty
+ attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_nil topic.last_read
+ end
+
+ def test_multiparameter_attributes_on_time
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
+
+ def test_boolean
+ b_false = Booleantest.create({ "value" => false })
+ false_id = b_false.id
+ b_true = Booleantest.create({ "value" => true })
+ true_id = b_true.id
+
+ b_false = Booleantest.find(false_id)
+ assert !b_false.value?
+ b_true = Booleantest.find(true_id)
+ assert b_true.value?
+ end
+
+ def test_clone
+ topic = Topic.find(1)
+ cloned_topic = topic.clone
+ assert_equal topic.title, cloned_topic.title
+ assert cloned_topic.new_record?
+
+ # test if the attributes have been cloned
+ topic.title = "a"
+ cloned_topic.title = "b"
+ assert_equal "a", topic.title
+ assert_equal "b", cloned_topic.title
+
+ # test if the attribute values have been cloned
+ topic.title = {"a" => "b"}
+ cloned_topic = topic.clone
+ cloned_topic.title["a"] = "c"
+ assert_equal "b", topic.title["a"]
+ end
+
+ def test_bignum
+ company = Company.find(1)
+ company.rating = 2147483647
+ company.save
+ company = Company.find(1)
+ assert_equal 2147483647, company.rating
+ end
+
+ def test_default
+ if Default.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
+ default = Default.new
+
+ # dates / timestampts
+ time_format = "%m/%d/%Y %H:%M"
+ assert_equal Time.now.strftime(time_format), default.modified_time.strftime(time_format)
+ assert_equal Date.today, default.modified_date
+
+ # fixed dates / times
+ assert_equal Date.new(2004, 1, 1), default.fixed_date
+ assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
+ end
+
+ def test_auto_id
+ auto = AutoId.new
+ auto.save
+ assert (auto.id > 0)
+ end
+
+ def quote_column_name(name)
+ "<#{name}>"
+ end
+
+ def test_quote_keys
+ ar = AutoId.new
+ source = {"foo" => "bar", "baz" => "quux"}
+ actual = ar.send(:quote_columns, self, source)
+ inverted = actual.invert
+ assert_equal("<foo>", inverted["bar"])
+ assert_equal("<baz>", inverted["quux"])
+ end
+
+ def test_column_name_properly_quoted
+ col_record = ColumnName.new
+ col_record.references = 40
+ col_record.save
+ col_record.references = 41
+ col_record.save
+ c2 = ColumnName.find(col_record.id)
+ assert_equal(41, c2.references)
+ end
+
+ MyObject = Struct.new :attribute1, :attribute2
+
+ def test_serialized_attribute
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create("content" => myobj)
+ Topic.serialize("content", MyObject)
+ assert_equal(myobj, topic.content)
+ end
+
+ def test_serialized_attribute_with_class_constraint
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create("content" => myobj)
+ Topic.serialize(:content, Hash)
+
+ assert_raises(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
+
+ settings = { "color" => "blue" }
+ Topic.find(topic.id).update_attribute("content", settings)
+ assert_equal(settings, Topic.find(topic.id).content)
+ Topic.serialize(:content)
+ end
+
+ def test_quote
+ content = "\\ \001 ' \n \\n \""
+ topic = Topic.create('content' => content)
+ assert_equal content, Topic.find(topic.id).content
+ end
+end
diff --git a/activerecord/test/class_inheritable_attributes_test.rb b/activerecord/test/class_inheritable_attributes_test.rb
new file mode 100644
index 0000000000..00a6945a66
--- /dev/null
+++ b/activerecord/test/class_inheritable_attributes_test.rb
@@ -0,0 +1,33 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+
+require 'test/unit'
+require 'active_record/support/class_inheritable_attributes'
+
+class A
+ include ClassInheritableAttributes
+end
+
+class B < A
+ write_inheritable_array "first", [ :one, :two ]
+end
+
+class C < A
+ write_inheritable_array "first", [ :three ]
+end
+
+class D < B
+ write_inheritable_array "first", [ :four ]
+end
+
+
+class ClassInheritableAttributesTest < Test::Unit::TestCase
+ def test_first_level
+ assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
+ assert_equal [ :three ], C.read_inheritable_attribute("first")
+ end
+
+ def test_second_level
+ assert_equal [ :one, :two, :four ], D.read_inheritable_attribute("first")
+ assert_equal [ :one, :two ], B.read_inheritable_attribute("first")
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb
new file mode 100644
index 0000000000..b663106d1f
--- /dev/null
+++ b/activerecord/test/connections/native_mysql/connection.rb
@@ -0,0 +1,24 @@
+print "Using native MySQL\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'activerecord_unittest'
+db2 = 'activerecord_unittest2'
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "root",
+ :password => "",
+ :database => db1
+)
+
+Course.establish_connection(
+ :adapter => "mysql",
+ :host => "localhost",
+ :username => "root",
+ :password => "",
+ :database => db2
+)
diff --git a/activerecord/test/connections/native_postgresql/connection.rb b/activerecord/test/connections/native_postgresql/connection.rb
new file mode 100644
index 0000000000..c9b00447f9
--- /dev/null
+++ b/activerecord/test/connections/native_postgresql/connection.rb
@@ -0,0 +1,24 @@
+print "Using native PostgreSQL\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'activerecord_unittest'
+db2 = 'activerecord_unittest2'
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "postgresql",
+ :host => nil,
+ :username => "postgres",
+ :password => "postgres",
+ :database => db1
+)
+
+Course.establish_connection(
+ :adapter => "postgresql",
+ :host => nil,
+ :username => "postgres",
+ :password => "postgres",
+ :database => db2
+) \ No newline at end of file
diff --git a/activerecord/test/connections/native_sqlite/connection.rb b/activerecord/test/connections/native_sqlite/connection.rb
new file mode 100644
index 0000000000..db688bdb70
--- /dev/null
+++ b/activerecord/test/connections/native_sqlite/connection.rb
@@ -0,0 +1,34 @@
+print "Using native SQlite\n"
+require 'fixtures/course'
+require 'logger'
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+BASE_DIR = File.expand_path(File.dirname(__FILE__) + '/../../fixtures')
+sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite"
+sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite"
+
+def make_connection(clazz, db_file, db_definitions_file)
+ unless File.exist?(db_file)
+ puts "SQLite database not found at #{db_file}. Rebuilding it."
+ sqlite_command = "sqlite #{db_file} 'create table a (a integer); drop table a;'"
+ puts "Executing '#{sqlite_command}'"
+ `#{sqlite_command}`
+ clazz.establish_connection(
+ :adapter => "sqlite",
+ :dbfile => db_file)
+ script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}")
+ # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time
+ script.split(';').each do
+ |command|
+ clazz.connection.execute(command) unless command.strip.empty?
+ end
+ else
+ clazz.establish_connection(
+ :adapter => "sqlite",
+ :dbfile => db_file)
+ end
+end
+
+make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql')
+make_connection(Course, sqlite_test_db2, 'sqlite2.sql')
+
diff --git a/activerecord/test/connections/native_sqlserver/connection.rb b/activerecord/test/connections/native_sqlserver/connection.rb
new file mode 100644
index 0000000000..b198f21c4b
--- /dev/null
+++ b/activerecord/test/connections/native_sqlserver/connection.rb
@@ -0,0 +1,15 @@
+print "Using native SQLServer\n"
+require 'fixtures/course'
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "sqlserver",
+ :dsn => "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;"
+)
+
+Course.establish_connection(
+ :adapter => "sqlserver",
+ :dsn => "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test2;User Id=sa;Password=password;"
+)
diff --git a/activerecord/test/deprecated_associations_test.rb b/activerecord/test/deprecated_associations_test.rb
new file mode 100755
index 0000000000..cb3d1aec8a
--- /dev/null
+++ b/activerecord/test/deprecated_associations_test.rb
@@ -0,0 +1,335 @@
+require 'abstract_unit'
+require 'fixtures/developer'
+require 'fixtures/project'
+require 'fixtures/company'
+require 'fixtures/topic'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/reply'
+
+# Can't declare new classes in test case methods, so tests before that
+bad_collection_keys = false
+begin
+ class Car < ActiveRecord::Base; has_many :wheels, :name => "wheels"; end
+rescue ActiveRecord::ActiveRecordError
+ bad_collection_keys = true
+end
+raise "ActiveRecord should have barked on bad collection keys" unless bad_collection_keys
+
+
+class DeprecatedAssociationsTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts", "companies", "accounts", "developers", "projects", "developers_projects", "topics"
+ @signals37 = Firm.find(1)
+ end
+
+ def test_has_many_find
+ assert_equal 2, Firm.find_first.clients.length
+ end
+
+ def test_has_many_orders
+ assert_equal "Summit", Firm.find_first.clients.first.name
+ end
+
+ def test_has_many_class_name
+ assert_equal "Microsoft", Firm.find_first.clients_sorted_desc.first.name
+ end
+
+ def test_has_many_foreign_key
+ assert_equal "Microsoft", Firm.find_first.clients_of_firm.first.name
+ end
+
+ def test_has_many_conditions
+ assert_equal "Microsoft", Firm.find_first.clients_like_ms.first.name
+ end
+
+ def test_has_many_sql
+ firm = Firm.find_first
+ assert_equal "Microsoft", firm.clients_using_sql.first.name
+ assert_equal 1, firm.clients_using_sql_count
+ assert_equal 1, Firm.find_first.clients_using_sql_count
+ end
+
+ def test_has_many_queries
+ assert Firm.find_first.has_clients?
+ firm = Firm.find_first
+ assert_equal 2, firm.clients_count # tests using class count
+ firm.clients
+ assert firm.has_clients?
+ assert_equal 2, firm.clients_count # tests using collection length
+ end
+
+ def test_has_many_dependence
+ assert_equal 2, Client.find_all.length
+ Firm.find_first.destroy
+ assert_equal 0, Client.find_all.length
+ end
+
+ def test_has_many_dependence_with_transaction_support_on_failure
+ assert_equal 2, Client.find_all.length
+
+ firm = Firm.find_first
+ clients = firm.clients
+ clients.last.instance_eval { def before_destroy() raise "Trigger rollback" end }
+
+ firm.destroy rescue "do nothing"
+
+ assert_equal 2, Client.find_all.length
+ end
+
+ def test_has_one_dependence
+ firm = Firm.find(1)
+ assert firm.has_account?
+ firm.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_has_one_dependence_with_missing_association
+ Account.destroy_all
+ firm = Firm.find(1)
+ assert !firm.has_account?
+ firm.destroy
+ end
+
+ def test_belongs_to
+ assert_equal @signals37.name, Client.find(3).firm.name
+ assert Client.find(3).has_firm?, "Microsoft should have a firm"
+ # assert !Company.find(1).has_firm?, "37signals shouldn't have a firm"
+ end
+
+ def test_belongs_to_with_different_class_name
+ assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
+ assert Company.find(3).has_firm_with_other_name?, "Microsoft should have a firm"
+ end
+
+ def test_belongs_to_with_condition
+ assert_equal Company.find(1).name, Company.find(3).firm_with_condition.name
+ assert Company.find(3).has_firm_with_condition?, "Microsoft should have a firm"
+ end
+
+
+ def test_belongs_to_equality
+ assert Company.find(3).firm?(Company.find(1)), "Microsoft should have 37signals as firm"
+ assert_raises(RuntimeError) { !Company.find(3).firm?(Company.find(3)) } # "Summit shouldn't have itself as firm"
+ end
+
+ def test_has_one
+ assert @signals37.account?(Account.find(1))
+ assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
+ assert @signals37.has_account?, "37signals should have an account"
+ assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
+ assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
+
+ assert !Account.find(2).has_firm?, "Unknown isn't linked"
+ assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
+ end
+
+ def test_has_many_dependence_on_account
+ assert_equal 2, Account.find_all.length
+ @signals37.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
+ def test_find_in
+ assert_equal Client.find(2).name, @signals37.find_in_clients(2).name
+ assert_raises(ActiveRecord::RecordNotFound) { @signals37.find_in_clients(6) }
+ end
+
+ def test_force_reload
+ firm = Firm.new
+ firm.save
+ firm.clients.each {|c|} # forcing to load all clients
+ assert firm.clients.empty?, "New firm shouldn't have client objects"
+ assert !firm.has_clients?, "New firm shouldn't have clients"
+ assert_equal 0, firm.clients_count, "New firm should have 0 clients"
+
+ client = Client.new("firm_id" => firm.id)
+ client.save
+
+ assert firm.clients.empty?, "New firm should have cached no client objects"
+ assert !firm.has_clients?, "New firm should have cached a no-clients response"
+ assert_equal 0, firm.clients_count, "New firm should have cached 0 clients count"
+
+ assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
+ assert firm.has_clients?(true), "New firm should have reloaded with a have-clients response"
+ assert_equal 1, firm.clients_count(true), "New firm should have reloaded clients count"
+ end
+
+ def test_included_in_collection
+ assert @signals37.clients.include?(Client.find(2))
+ end
+
+ def test_build_to_collection
+ assert_equal 1, @signals37.clients_of_firm_count
+ new_client = @signals37.build_to_clients_of_firm("name" => "Another Client")
+ assert_equal "Another Client", new_client.name
+ assert new_client.save
+
+ assert new_client.firm?(@signals37)
+ assert_equal 2, @signals37.clients_of_firm_count(true)
+ end
+
+ def test_create_in_collection
+ assert_equal @signals37.create_in_clients_of_firm("name" => "Another Client"), @signals37.clients_of_firm(true).last
+ end
+
+ def test_succesful_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account
+ assert !account.save
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_create_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ assert_equal firm.create_account("credit_limit" => 1000), firm.account
+ end
+
+ def test_has_and_belongs_to_many
+ david = Developer.find(1)
+ assert david.has_projects?
+ assert_equal 2, david.projects_count
+
+ active_record = Project.find(1)
+ assert active_record.has_developers?
+ assert_equal 2, active_record.developers_count
+ assert_equal david.name, active_record.developers.first.name
+ end
+
+ def test_has_and_belongs_to_many_removing
+ david = Developer.find(1)
+ active_record = Project.find(1)
+
+ david.remove_projects(active_record)
+
+ assert_equal 1, david.projects_count
+ assert_equal 1, active_record.developers_count
+ end
+
+ def test_has_and_belongs_to_many_zero
+ david = Developer.find(1)
+ david.remove_projects(Project.find_all)
+
+ assert_equal 0, david.projects_count
+ assert !david.has_projects?
+ end
+
+ def test_has_and_belongs_to_many_adding
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+
+ jamis.add_projects(action_controller)
+
+ assert_equal 2, jamis.projects_count
+ assert_equal 2, action_controller.developers_count
+ end
+
+ def test_has_and_belongs_to_many_adding_from_the_project
+ jamis = Developer.find(2)
+ action_controller = Project.find(2)
+
+ action_controller.add_developers(jamis)
+
+ assert_equal 2, jamis.projects_count
+ assert_equal 2, action_controller.developers_count
+ end
+
+ def test_has_and_belongs_to_many_adding_a_collection
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.save
+
+ aridridel.add_projects([ Project.find(1), Project.find(2) ])
+ assert_equal 2, aridridel.projects_count
+ end
+
+ def test_belongs_to_counter
+ topic = Topic.create("title" => "Apple", "content" => "hello world")
+ assert_equal 0, topic.send(:read_attribute, "replies_count"), "No replies yet"
+
+ reply = topic.create_in_replies("title" => "I'm saying no!", "content" => "over here")
+ assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ reply.destroy
+ assert_equal 0, Topic.find(topic.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
+ def test_natural_assignment_of_has_one
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ apple.account = citibank
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_of_belongs_to
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm = apple
+ assert_equal apple.id, citibank.firm_id
+ end
+
+ def test_natural_assignment_of_has_many
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+ assert_equal apple.id, natural.firm_id
+ assert_equal Client.find(natural.id), Firm.find(apple.id).clients.find { |c| c.id == natural.id }
+ apple.clients.delete natural
+ assert_nil Firm.find(apple.id).clients.find { |c| c.id == natural.id }
+ end
+
+
+ def test_natural_adding_of_has_and_belongs_to_many
+ rails = Project.create("name" => "Rails")
+ ap = Project.create("name" => "Action Pack")
+ john = Developer.create("name" => "John")
+ mike = Developer.create("name" => "Mike")
+ rails.developers << john
+ rails.developers << mike
+
+ assert_equal Developer.find(john.id), Project.find(rails.id).developers.find { |d| d.id == john.id }
+ assert_equal Developer.find(mike.id), Project.find(rails.id).developers.find { |d| d.id == mike.id }
+ assert_equal Project.find(rails.id), Developer.find(mike.id).projects.find { |p| p.id == rails.id }
+ assert_equal Project.find(rails.id), Developer.find(john.id).projects.find { |p| p.id == rails.id }
+ ap.developers << john
+ assert_equal Developer.find(john.id), Project.find(ap.id).developers.find { |d| d.id == john.id }
+ assert_equal Project.find(ap.id), Developer.find(john.id).projects.find { |p| p.id == ap.id }
+
+ ap.developers.delete john
+ assert_nil Project.find(ap.id).developers.find { |d| d.id == john.id }
+ assert_nil Developer.find(john.id).projects.find { |p| p.id == ap.id }
+ end
+
+ def test_storing_in_pstore
+ require "pstore"
+ require "tmpdir"
+ apple = Firm.create("name" => "Apple")
+ natural = Client.new("name" => "Natural Company")
+ apple.clients << natural
+
+ db = PStore.new(File.join(Dir.tmpdir, "ar-pstore-association-test"))
+ db.transaction do
+ db["apple"] = apple
+ end
+
+ db = PStore.new(File.join(Dir.tmpdir, "ar-pstore-association-test"))
+ db.transaction do
+ assert_equal "Natural Company", db["apple"].clients.first.name
+ end
+ end
+
+ def test_has_many_find_all
+ assert_equal 2, Firm.find_first.find_all_in_clients("type = 'Client'").length
+ assert_equal 1, Firm.find_first.find_all_in_clients("name = 'Summit'").length
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/finder_test.rb b/activerecord/test/finder_test.rb
new file mode 100755
index 0000000000..d369f6b033
--- /dev/null
+++ b/activerecord/test/finder_test.rb
@@ -0,0 +1,67 @@
+require 'abstract_unit'
+require 'fixtures/company'
+require 'fixtures/topic'
+
+class FinderTest < Test::Unit::TestCase
+ def setup
+ @company_fixtures = create_fixtures("companies")
+ @topic_fixtures = create_fixtures("topics")
+ end
+
+ def test_find
+ assert_equal(@topic_fixtures["first"]["title"], Topic.find(1).title)
+ end
+
+ def test_find_by_ids
+ assert_equal(2, Topic.find(1, 2).length)
+ assert_equal(@topic_fixtures["second"]["title"], Topic.find([ 2 ]).title)
+ end
+
+ def test_find_by_ids_missing_one
+ assert_raises(ActiveRecord::RecordNotFound) {
+ Topic.find(1, 2, 45)
+ }
+ end
+
+ def test_find_with_entire_select_statement
+ topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
+
+ assert_equal(1, topics.size)
+ assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
+ end
+
+ def test_find_first
+ first = Topic.find_first "title = 'The First Topic'"
+ assert_equal(@topic_fixtures["first"]["title"], first.title)
+ end
+
+ def test_find_first_failing
+ first = Topic.find_first "title = 'The First Topic!'"
+ assert_nil(first)
+ end
+
+ def test_unexisting_record_exception_handling
+ assert_raises(ActiveRecord::RecordNotFound) {
+ Topic.find(1).parent
+ }
+
+ Topic.find(2).parent
+ end
+
+ def test_find_on_conditions
+ assert Topic.find_on_conditions(1, "approved = 0")
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find_on_conditions(1, "approved = 1") }
+ end
+
+ def test_condition_interpolation
+ assert_kind_of Firm, Company.find_first(["name = '%s'", "37signals"])
+ assert_nil Company.find_first(["name = '%s'", "37signals!"])
+ assert_nil Company.find_first(["name = '%s'", "37signals!' OR 1=1"])
+ assert_kind_of Time, Topic.find_first(["id = %d", 1]).written_on
+ end
+
+ def test_string_sanitation
+ assert_equal "something '' 1=1", ActiveRecord::Base.sanitize("something ' 1=1")
+ assert_equal "something select table", ActiveRecord::Base.sanitize("something; select table")
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/accounts.yml b/activerecord/test/fixtures/accounts.yml
new file mode 100644
index 0000000000..21a0aab52a
--- /dev/null
+++ b/activerecord/test/fixtures/accounts.yml
@@ -0,0 +1,8 @@
+signals37:
+ id: 1
+ firm_id: 1
+ credit_limit: 50
+
+unknown:
+ id: 2
+ credit_limit: 50
diff --git a/activerecord/test/fixtures/auto_id.rb b/activerecord/test/fixtures/auto_id.rb
new file mode 100644
index 0000000000..d720e2be5e
--- /dev/null
+++ b/activerecord/test/fixtures/auto_id.rb
@@ -0,0 +1,4 @@
+class AutoId < ActiveRecord::Base
+ def self.table_name () "auto_id_tests" end
+ def self.primary_key () "auto_id" end
+end
diff --git a/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char b/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char
new file mode 100644
index 0000000000..ef27947f27
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/attr_with_numeric_first_char
@@ -0,0 +1 @@
+1b => 1
diff --git a/activerecord/test/fixtures/bad_fixtures/attr_with_spaces b/activerecord/test/fixtures/bad_fixtures/attr_with_spaces
new file mode 100644
index 0000000000..46fd6f2fe9
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/attr_with_spaces
@@ -0,0 +1 @@
+a b => 1
diff --git a/activerecord/test/fixtures/bad_fixtures/blank_line b/activerecord/test/fixtures/bad_fixtures/blank_line
new file mode 100644
index 0000000000..3ea1f71743
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/blank_line
@@ -0,0 +1,3 @@
+a => 1
+
+b => 2
diff --git a/activerecord/test/fixtures/bad_fixtures/duplicate_attributes b/activerecord/test/fixtures/bad_fixtures/duplicate_attributes
new file mode 100644
index 0000000000..cc0236f26f
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/duplicate_attributes
@@ -0,0 +1,3 @@
+a => 1
+b => 2
+a => 3
diff --git a/activerecord/test/fixtures/bad_fixtures/missing_value b/activerecord/test/fixtures/bad_fixtures/missing_value
new file mode 100644
index 0000000000..fb59ec33e8
--- /dev/null
+++ b/activerecord/test/fixtures/bad_fixtures/missing_value
@@ -0,0 +1 @@
+a =>
diff --git a/activerecord/test/fixtures/column_name.rb b/activerecord/test/fixtures/column_name.rb
new file mode 100644
index 0000000000..ec07205a3a
--- /dev/null
+++ b/activerecord/test/fixtures/column_name.rb
@@ -0,0 +1,3 @@
+class ColumnName < ActiveRecord::Base
+ def self.table_name () "colnametests" end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/companies/first_client b/activerecord/test/fixtures/companies/first_client
new file mode 100755
index 0000000000..800c694eeb
--- /dev/null
+++ b/activerecord/test/fixtures/companies/first_client
@@ -0,0 +1,6 @@
+id => 2
+type => Client
+firm_id => 1
+client_of => 2
+name => Summit
+ruby_type => Client
diff --git a/activerecord/test/fixtures/companies/first_firm b/activerecord/test/fixtures/companies/first_firm
new file mode 100755
index 0000000000..22d876dad1
--- /dev/null
+++ b/activerecord/test/fixtures/companies/first_firm
@@ -0,0 +1,4 @@
+id => 1
+type => Firm
+name => 37signals
+ruby_type => Firm
diff --git a/activerecord/test/fixtures/companies/second_client b/activerecord/test/fixtures/companies/second_client
new file mode 100755
index 0000000000..69f8adc49c
--- /dev/null
+++ b/activerecord/test/fixtures/companies/second_client
@@ -0,0 +1,6 @@
+id => 3
+type => Client
+firm_id => 1
+client_of => 1
+name => Microsoft
+ruby_type => Client
diff --git a/activerecord/test/fixtures/company.rb b/activerecord/test/fixtures/company.rb
new file mode 100755
index 0000000000..b5ee055948
--- /dev/null
+++ b/activerecord/test/fixtures/company.rb
@@ -0,0 +1,37 @@
+class Company < ActiveRecord::Base
+ attr_protected :rating
+end
+
+
+class Firm < Company
+ has_many :clients, :order => "id", :dependent => true
+ has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
+ has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
+ has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id"
+ has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
+
+ has_one :account, :dependent => true
+end
+
+class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+ belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id"
+ belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
+ belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => "1 = 1"
+end
+
+
+class SpecialClient < Client
+end
+
+class VerySpecialClient < SpecialClient
+end
+
+class Account < ActiveRecord::Base
+ belongs_to :firm
+
+ protected
+ def validate
+ errors.add_on_empty "credit_limit"
+ end
+end
diff --git a/activerecord/test/fixtures/company_in_module.rb b/activerecord/test/fixtures/company_in_module.rb
new file mode 100644
index 0000000000..a484ed5eaf
--- /dev/null
+++ b/activerecord/test/fixtures/company_in_module.rb
@@ -0,0 +1,47 @@
+module MyApplication
+ module Business
+ class Company < ActiveRecord::Base
+ attr_protected :rating
+ end
+
+ class Firm < Company
+ has_many :clients, :order => "id", :dependent => true
+ has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
+ has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
+ has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id"
+ has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
+
+ has_one :account, :dependent => true
+ end
+
+ class Client < Company
+ belongs_to :firm, :foreign_key => "client_of"
+ belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
+ end
+
+ class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+
+ protected
+ def validate
+ errors.add_on_boundry_breaking("name", 3..20)
+ end
+ end
+
+ class Project < ActiveRecord::Base
+ has_and_belongs_to_many :developers
+ end
+
+ end
+
+ module Billing
+ class Account < ActiveRecord::Base
+ belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+
+ protected
+ def validate
+ errors.add_on_empty "credit_limit"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/fixtures/course.rb b/activerecord/test/fixtures/course.rb
new file mode 100644
index 0000000000..8a40fa740d
--- /dev/null
+++ b/activerecord/test/fixtures/course.rb
@@ -0,0 +1,3 @@
+class Course < ActiveRecord::Base
+ has_many :entrants
+end
diff --git a/activerecord/test/fixtures/courses/java b/activerecord/test/fixtures/courses/java
new file mode 100644
index 0000000000..84b10d390b
--- /dev/null
+++ b/activerecord/test/fixtures/courses/java
@@ -0,0 +1,2 @@
+id => 2
+name => Java Development
diff --git a/activerecord/test/fixtures/courses/ruby b/activerecord/test/fixtures/courses/ruby
new file mode 100644
index 0000000000..db42f96d27
--- /dev/null
+++ b/activerecord/test/fixtures/courses/ruby
@@ -0,0 +1,2 @@
+id => 1
+name => Ruby Development
diff --git a/activerecord/test/fixtures/customer.rb b/activerecord/test/fixtures/customer.rb
new file mode 100644
index 0000000000..5275a5209d
--- /dev/null
+++ b/activerecord/test/fixtures/customer.rb
@@ -0,0 +1,30 @@
+class Customer < ActiveRecord::Base
+ composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ]
+ composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
+end
+
+class Address
+ attr_reader :street, :city, :country
+
+ def initialize(street, city, country)
+ @street, @city, @country = street, city, country
+ end
+
+ def close_to?(other_address)
+ city == other_address.city && country == other_address.country
+ end
+end
+
+class Money
+ attr_reader :amount, :currency
+
+ EXCHANGE_RATES = { "USD_TO_DKK" => 6, "DKK_TO_USD" => 0.6 }
+
+ def initialize(amount, currency = "USD")
+ @amount, @currency = amount, currency
+ end
+
+ def exchange_to(other_currency)
+ Money.new((amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor, other_currency)
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/customers/david b/activerecord/test/fixtures/customers/david
new file mode 100644
index 0000000000..69b9c32376
--- /dev/null
+++ b/activerecord/test/fixtures/customers/david
@@ -0,0 +1,6 @@
+id => 1
+name => David
+balance => 50
+address_street => Funny Street
+address_city => Scary Town
+address_country => Loony Land \ No newline at end of file
diff --git a/activerecord/test/fixtures/db_definitions/mysql.sql b/activerecord/test/fixtures/db_definitions/mysql.sql
new file mode 100755
index 0000000000..766c0ec71f
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/mysql.sql
@@ -0,0 +1,97 @@
+CREATE TABLE `accounts` (
+ `id` int(11) NOT NULL auto_increment,
+ `firm_id` int(11) default NULL,
+ `credit_limit` int(5) default NULL,
+ PRIMARY KEY (`id`)
+) TYPE=InnoDB;
+
+CREATE TABLE `companies` (
+ `id` int(11) NOT NULL auto_increment,
+ `type` varchar(50) default NULL,
+ `ruby_type` varchar(50) default NULL,
+ `firm_id` int(11) default NULL,
+ `name` varchar(50) default NULL,
+ `client_of` int(11) default NULL,
+ `rating` int(11) default NULL default 1,
+ PRIMARY KEY (`id`)
+) TYPE=InnoDB;
+
+
+CREATE TABLE `topics` (
+ `id` int(11) NOT NULL auto_increment,
+ `title` varchar(255) default NULL,
+ `author_name` varchar(255) default NULL,
+ `author_email_address` varchar(255) default NULL,
+ `written_on` datetime default NULL,
+ `last_read` date default NULL,
+ `content` text,
+ `approved` tinyint(1) default 1,
+ `replies_count` int(11) default 0,
+ `parent_id` int(11) default NULL,
+ `type` varchar(50) default NULL,
+ PRIMARY KEY (`id`)
+) TYPE=InnoDB;
+
+CREATE TABLE `developers` (
+ `id` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `projects` (
+ `id` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `developers_projects` (
+ `developer_id` int(11) NOT NULL,
+ `project_id` int(11) NOT NULL,
+ `joined_on` date default NULL
+);
+
+CREATE TABLE `customers` (
+ `id` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ `balance` int(6) default 0,
+ `address_street` varchar(100) default NULL,
+ `address_city` varchar(100) default NULL,
+ `address_country` varchar(100) default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `movies` (
+ `movieid` int(11) NOT NULL auto_increment,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`movieid`)
+);
+
+CREATE TABLE `subscribers` (
+ `nick` varchar(100) NOT NULL,
+ `name` varchar(100) default NULL,
+ PRIMARY KEY (`nick`)
+);
+
+CREATE TABLE `booleantests` (
+ `id` int(11) NOT NULL auto_increment,
+ `value` integer default NULL,
+ PRIMARY KEY (`id`)
+);
+
+CREATE TABLE `auto_id_tests` (
+ `auto_id` int(11) NOT NULL auto_increment,
+ `value` integer default NULL,
+ PRIMARY KEY (`auto_id`)
+);
+
+CREATE TABLE `entrants` (
+ `id` INTEGER NOT NULL PRIMARY KEY,
+ `name` VARCHAR(255) NOT NULL,
+ `course_id` INTEGER NOT NULL
+);
+
+CREATE TABLE `colnametests` (
+ `id` int(11) NOT NULL auto_increment,
+ `references` int(11) NOT NULL,
+ PRIMARY KEY (`id`)
+);
diff --git a/activerecord/test/fixtures/db_definitions/mysql2.sql b/activerecord/test/fixtures/db_definitions/mysql2.sql
new file mode 100644
index 0000000000..0a16a0a2f9
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/mysql2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE `courses` (
+ `id` INTEGER NOT NULL PRIMARY KEY,
+ `name` VARCHAR(255) NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/postgresql.sql b/activerecord/test/fixtures/db_definitions/postgresql.sql
new file mode 100644
index 0000000000..e83356627b
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/postgresql.sql
@@ -0,0 +1,114 @@
+SET search_path = public, pg_catalog;
+
+CREATE TABLE accounts (
+ id serial,
+ firm_id integer,
+ credit_limit integer,
+ PRIMARY KEY (id)
+);
+SELECT setval('accounts_id_seq', 100);
+
+CREATE TABLE companies (
+ id serial,
+ "type" character varying(50),
+ "ruby_type" character varying(50),
+ firm_id integer,
+ name character varying(50),
+ client_of integer,
+ rating integer default 1,
+ PRIMARY KEY (id)
+);
+SELECT setval('companies_id_seq', 100);
+
+CREATE TABLE developers_projects (
+ developer_id integer NOT NULL,
+ project_id integer NOT NULL,
+ joined_on date
+);
+
+CREATE TABLE developers (
+ id serial,
+ name character varying(100),
+ PRIMARY KEY (id)
+);
+SELECT setval('developers_id_seq', 100);
+
+CREATE TABLE projects (
+ id serial,
+ name character varying(100),
+ PRIMARY KEY (id)
+);
+SELECT setval('projects_id_seq', 100);
+
+CREATE TABLE topics (
+ id serial,
+ title character varying(255),
+ author_name character varying(255),
+ author_email_address character varying(255),
+ written_on timestamp without time zone,
+ last_read date,
+ content text,
+ replies_count integer default 0,
+ parent_id integer,
+ "type" character varying(50),
+ approved smallint DEFAULT 1,
+ PRIMARY KEY (id)
+);
+SELECT setval('topics_id_seq', 100);
+
+CREATE TABLE customers (
+ id serial,
+ name character varying,
+ balance integer default 0,
+ address_street character varying,
+ address_city character varying,
+ address_country character varying,
+ PRIMARY KEY (id)
+);
+SELECT setval('customers_id_seq', 100);
+
+CREATE TABLE movies (
+ movieid serial,
+ name text,
+ PRIMARY KEY (movieid)
+);
+
+CREATE TABLE subscribers (
+ nick text NOT NULL,
+ name text,
+ PRIMARY KEY (nick)
+);
+
+CREATE TABLE booleantests (
+ id serial,
+ value boolean,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE defaults (
+ id serial,
+ modified_date date default CURRENT_DATE,
+ fixed_date date default '2004-01-01',
+ modified_time timestamp default CURRENT_TIMESTAMP,
+ fixed_time timestamp default '2004-01-01 00:00:00.000000-00',
+ char1 char(1) default 'Y',
+ char2 character varying(50) default 'a varchar field',
+ char3 text default 'a text field'
+);
+
+CREATE TABLE auto_id_tests (
+ auto_id serial,
+ value integer,
+ PRIMARY KEY (auto_id)
+);
+
+CREATE TABLE entrants (
+ id serial,
+ name text,
+ course_id integer
+);
+
+CREATE TABLE colnametests (
+ id serial,
+ "references" integer NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/postgresql2.sql b/activerecord/test/fixtures/db_definitions/postgresql2.sql
new file mode 100644
index 0000000000..b58a45eff7
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/postgresql2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE courses (
+ id serial,
+ name text
+); \ No newline at end of file
diff --git a/activerecord/test/fixtures/db_definitions/sqlite.sql b/activerecord/test/fixtures/db_definitions/sqlite.sql
new file mode 100644
index 0000000000..cb617305dc
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlite.sql
@@ -0,0 +1,86 @@
+CREATE TABLE 'accounts' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'firm_id' INTEGER DEFAULT NULL,
+ 'credit_limit' INTEGER DEFAULT NULL
+);
+
+CREATE TABLE 'companies' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'type' VARCHAR(255) DEFAULT NULL,
+ 'ruby_type' VARCHAR(255) DEFAULT NULL,
+ 'firm_id' INTEGER DEFAULT NULL,
+ 'name' TEXT DEFAULT NULL,
+ 'client_of' INTEGER DEFAULT NULL,
+ 'rating' INTEGER DEFAULT 1
+);
+
+
+CREATE TABLE 'topics' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'title' VARCHAR(255) DEFAULT NULL,
+ 'author_name' VARCHAR(255) DEFAULT NULL,
+ 'author_email_address' VARCHAR(255) DEFAULT NULL,
+ 'written_on' DATETIME DEFAULT NULL,
+ 'last_read' DATE DEFAULT NULL,
+ 'content' TEXT,
+ 'approved' INTEGER DEFAULT 1,
+ 'replies_count' INTEGER DEFAULT 0,
+ 'parent_id' INTEGER DEFAULT NULL,
+ 'type' VARCHAR(255) DEFAULT NULL
+);
+
+CREATE TABLE 'developers' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'projects' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'developers_projects' (
+ 'developer_id' INTEGER NOT NULL,
+ 'project_id' INTEGER NOT NULL,
+ 'joined_on' DATE DEFAULT NULL
+);
+
+CREATE TABLE 'customers' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'name' VARCHAR(255) DEFAULT NULL,
+ 'balance' INTEGER DEFAULT 0,
+ 'address_street' TEXT DEFAULT NULL,
+ 'address_city' TEXT DEFAULT NULL,
+ 'address_country' TEXT DEFAULT NULL
+);
+
+CREATE TABLE 'movies' (
+ 'movieid' INTEGER PRIMARY KEY NOT NULL,
+ 'name' VARCHAR(255) DEFAULT NULL
+);
+
+CREATE TABLE subscribers (
+ 'nick' VARCHAR(255) PRIMARY KEY NOT NULL,
+ 'name' VARCHAR(255) DEFAULT NULL
+);
+
+CREATE TABLE 'booleantests' (
+ 'id' INTEGER PRIMARY KEY NOT NULL,
+ 'value' INTEGER DEFAULT NULL
+);
+
+CREATE TABLE 'auto_id_tests' (
+ 'auto_id' INTEGER PRIMARY KEY NOT NULL,
+ 'value' INTEGER DEFAULT NULL
+);
+
+CREATE TABLE 'entrants' (
+ 'id' INTEGER NOT NULL PRIMARY KEY,
+ 'name' VARCHAR(255) NOT NULL,
+ 'course_id' INTEGER NOT NULL
+);
+
+CREATE TABLE 'colnametests' (
+ 'id' INTEGER NOT NULL PRIMARY KEY,
+ 'references' INTEGER NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/sqlite2.sql b/activerecord/test/fixtures/db_definitions/sqlite2.sql
new file mode 100644
index 0000000000..19b123968a
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlite2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE 'courses' (
+ 'id' INTEGER NOT NULL PRIMARY KEY,
+ 'name' VARCHAR(255) NOT NULL
+);
diff --git a/activerecord/test/fixtures/db_definitions/sqlserver.sql b/activerecord/test/fixtures/db_definitions/sqlserver.sql
new file mode 100644
index 0000000000..0ae9780273
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlserver.sql
@@ -0,0 +1,96 @@
+CREATE TABLE accounts (
+ id int NOT NULL IDENTITY(1, 1),
+ firm_id int default NULL,
+ credit_limit int default NULL,
+ PRIMARY KEY (id)
+)
+
+CREATE TABLE companies (
+ id int NOT NULL IDENTITY(1, 1),
+ type varchar(50) default NULL,
+ ruby_type varchar(50) default NULL,
+ firm_id int default NULL,
+ name varchar(50) default NULL,
+ client_of int default NULL,
+ companies_count int default 0,
+ rating int default 1,
+ PRIMARY KEY (id)
+)
+
+CREATE TABLE topics (
+ id int NOT NULL IDENTITY(1, 1),
+ title varchar(255) default NULL,
+ author_name varchar(255) default NULL,
+ author_email_address varchar(255) default NULL,
+ written_on datetime default NULL,
+ last_read datetime default NULL,
+ content text,
+ approved tinyint default 1,
+ replies_count int default 0,
+ parent_id int default NULL,
+ type varchar(50) default NULL,
+ PRIMARY KEY (id)
+)
+
+CREATE TABLE developers (
+ id int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE projects (
+ id int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE developers_projects (
+ developer_id int NOT NULL,
+ project_id int NOT NULL
+);
+
+CREATE TABLE customers (
+ id int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ balance int default 0,
+ address_street varchar(100) default NULL,
+ address_city varchar(100) default NULL,
+ address_country varchar(100) default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE movies (
+ movieid int NOT NULL IDENTITY(1, 1),
+ name varchar(100) default NULL,
+ PRIMARY KEY (movieid)
+);
+
+CREATE TABLE subscribers (
+ nick varchar(100) NOT NULL,
+ name varchar(100) default NULL,
+ PRIMARY KEY (nick)
+);
+
+CREATE TABLE booleantests (
+ id int NOT NULL IDENTITY(1, 1),
+ value integer default NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE auto_id_tests (
+ auto_id int NOT NULL IDENTITY(1, 1),
+ value int default NULL,
+ PRIMARY KEY (auto_id)
+);
+
+CREATE TABLE entrants (
+ id int NOT NULL PRIMARY KEY,
+ name varchar(255) NOT NULL,
+ course_id int NOT NULL
+);
+
+CREATE TABLE colnametests (
+ id int NOT NULL IDENTITY(1, 1),
+ [references] int NOT NULL,
+ PRIMARY KEY (id)
+); \ No newline at end of file
diff --git a/activerecord/test/fixtures/db_definitions/sqlserver2.sql b/activerecord/test/fixtures/db_definitions/sqlserver2.sql
new file mode 100644
index 0000000000..dc4f9ed364
--- /dev/null
+++ b/activerecord/test/fixtures/db_definitions/sqlserver2.sql
@@ -0,0 +1,4 @@
+CREATE TABLE courses (
+ id int NOT NULL PRIMARY KEY,
+ name varchar(255) NOT NULL
+);
diff --git a/activerecord/test/fixtures/default.rb b/activerecord/test/fixtures/default.rb
new file mode 100644
index 0000000000..887e9cc999
--- /dev/null
+++ b/activerecord/test/fixtures/default.rb
@@ -0,0 +1,2 @@
+class Default < ActiveRecord::Base
+end
diff --git a/activerecord/test/fixtures/developer.rb b/activerecord/test/fixtures/developer.rb
new file mode 100644
index 0000000000..737fc3824b
--- /dev/null
+++ b/activerecord/test/fixtures/developer.rb
@@ -0,0 +1,8 @@
+class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+
+ protected
+ def validate
+ errors.add_on_boundry_breaking("name", 3..20)
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers.yml b/activerecord/test/fixtures/developers.yml
new file mode 100644
index 0000000000..733455f789
--- /dev/null
+++ b/activerecord/test/fixtures/developers.yml
@@ -0,0 +1,13 @@
+david:
+ id: 1
+ name: David
+
+jamis:
+ id: 2
+ name: Jamis
+
+<% for digit in 3..10 %>
+dev_<%= digit %>:
+ id: <%= digit %>
+ name: fixture_<%= digit %>
+<% end %> \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers_projects/david_action_controller b/activerecord/test/fixtures/developers_projects/david_action_controller
new file mode 100644
index 0000000000..e6e9d0e59b
--- /dev/null
+++ b/activerecord/test/fixtures/developers_projects/david_action_controller
@@ -0,0 +1,3 @@
+developer_id => 1
+project_id => 2
+joined_on => 2004-10-10 \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers_projects/david_active_record b/activerecord/test/fixtures/developers_projects/david_active_record
new file mode 100644
index 0000000000..2ef474c10d
--- /dev/null
+++ b/activerecord/test/fixtures/developers_projects/david_active_record
@@ -0,0 +1,3 @@
+developer_id => 1
+project_id => 1
+joined_on => 2004-10-10 \ No newline at end of file
diff --git a/activerecord/test/fixtures/developers_projects/jamis_active_record b/activerecord/test/fixtures/developers_projects/jamis_active_record
new file mode 100644
index 0000000000..91beb80797
--- /dev/null
+++ b/activerecord/test/fixtures/developers_projects/jamis_active_record
@@ -0,0 +1,2 @@
+developer_id => 2
+project_id => 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures/entrant.rb b/activerecord/test/fixtures/entrant.rb
new file mode 100644
index 0000000000..4682ce48c8
--- /dev/null
+++ b/activerecord/test/fixtures/entrant.rb
@@ -0,0 +1,3 @@
+class Entrant < ActiveRecord::Base
+ belongs_to :course
+end
diff --git a/activerecord/test/fixtures/entrants/first b/activerecord/test/fixtures/entrants/first
new file mode 100644
index 0000000000..e45cd6c1c2
--- /dev/null
+++ b/activerecord/test/fixtures/entrants/first
@@ -0,0 +1,3 @@
+id => 1
+course_id => 1
+name => Ruby Developer
diff --git a/activerecord/test/fixtures/entrants/second b/activerecord/test/fixtures/entrants/second
new file mode 100644
index 0000000000..38cd702476
--- /dev/null
+++ b/activerecord/test/fixtures/entrants/second
@@ -0,0 +1,3 @@
+id => 2
+course_id => 1
+name => Ruby Guru
diff --git a/activerecord/test/fixtures/entrants/third b/activerecord/test/fixtures/entrants/third
new file mode 100644
index 0000000000..594ac77af0
--- /dev/null
+++ b/activerecord/test/fixtures/entrants/third
@@ -0,0 +1,3 @@
+id => 3
+course_id => 2
+name => Java Lover
diff --git a/activerecord/test/fixtures/movie.rb b/activerecord/test/fixtures/movie.rb
new file mode 100644
index 0000000000..6384b4c801
--- /dev/null
+++ b/activerecord/test/fixtures/movie.rb
@@ -0,0 +1,5 @@
+class Movie < ActiveRecord::Base
+ def self.primary_key
+ "movieid"
+ end
+end
diff --git a/activerecord/test/fixtures/movies/first b/activerecord/test/fixtures/movies/first
new file mode 100644
index 0000000000..0feaeac7b0
--- /dev/null
+++ b/activerecord/test/fixtures/movies/first
@@ -0,0 +1,2 @@
+movieid => 1
+name => Terminator
diff --git a/activerecord/test/fixtures/movies/second b/activerecord/test/fixtures/movies/second
new file mode 100644
index 0000000000..b3c506b7da
--- /dev/null
+++ b/activerecord/test/fixtures/movies/second
@@ -0,0 +1,2 @@
+movieid => 2
+name => Gladiator
diff --git a/activerecord/test/fixtures/project.rb b/activerecord/test/fixtures/project.rb
new file mode 100644
index 0000000000..1ccf39d7cf
--- /dev/null
+++ b/activerecord/test/fixtures/project.rb
@@ -0,0 +1,4 @@
+class Project < ActiveRecord::Base
+ has_and_belongs_to_many :developers, :uniq => true
+ has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/projects/action_controller b/activerecord/test/fixtures/projects/action_controller
new file mode 100644
index 0000000000..b3f00ae727
--- /dev/null
+++ b/activerecord/test/fixtures/projects/action_controller
@@ -0,0 +1,2 @@
+id => 2
+name => Active Controller \ No newline at end of file
diff --git a/activerecord/test/fixtures/projects/active_record b/activerecord/test/fixtures/projects/active_record
new file mode 100644
index 0000000000..31131a7f30
--- /dev/null
+++ b/activerecord/test/fixtures/projects/active_record
@@ -0,0 +1,2 @@
+id => 1
+name => Active Record \ No newline at end of file
diff --git a/activerecord/test/fixtures/reply.rb b/activerecord/test/fixtures/reply.rb
new file mode 100755
index 0000000000..51dfe21d2d
--- /dev/null
+++ b/activerecord/test/fixtures/reply.rb
@@ -0,0 +1,21 @@
+class Reply < Topic
+ belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
+
+ attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read
+
+ def validate
+ errors.add("title", "Empty") unless attribute_present? "title"
+ errors.add("content", "Empty") unless attribute_present? "content"
+ end
+
+ def validate_on_create
+ errors.add("title", "is Wrong Create") if attribute_present?("title") && title == "Wrong Create"
+ if attribute_present?("title") && attribute_present?("content") && content == "Mismatch"
+ errors.add("title", "is Content Mismatch")
+ end
+ end
+
+ def validate_on_update
+ errors.add("title", "is Wrong Update") if attribute_present?("title") && title == "Wrong Update"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/subscriber.rb b/activerecord/test/fixtures/subscriber.rb
new file mode 100644
index 0000000000..3f1ade0d83
--- /dev/null
+++ b/activerecord/test/fixtures/subscriber.rb
@@ -0,0 +1,5 @@
+class Subscriber < ActiveRecord::Base
+ def self.primary_key
+ "nick"
+ end
+end
diff --git a/activerecord/test/fixtures/subscribers/first b/activerecord/test/fixtures/subscribers/first
new file mode 100644
index 0000000000..5287e26e4d
--- /dev/null
+++ b/activerecord/test/fixtures/subscribers/first
@@ -0,0 +1,2 @@
+nick => alterself
+name => Luke Holden
diff --git a/activerecord/test/fixtures/subscribers/second b/activerecord/test/fixtures/subscribers/second
new file mode 100644
index 0000000000..2345e4475a
--- /dev/null
+++ b/activerecord/test/fixtures/subscribers/second
@@ -0,0 +1,2 @@
+nick => webster132
+name => David Heinemeier Hansson
diff --git a/activerecord/test/fixtures/topic.rb b/activerecord/test/fixtures/topic.rb
new file mode 100755
index 0000000000..55c94e9e88
--- /dev/null
+++ b/activerecord/test/fixtures/topic.rb
@@ -0,0 +1,20 @@
+class Topic < ActiveRecord::Base
+ has_many :replies, :foreign_key => "parent_id"
+ serialize :content
+
+ before_create :default_written_on
+ before_destroy :destroy_children #'self.class.delete_all "parent_id = #{id}"'
+
+ def parent
+ self.class.find(parent_id)
+ end
+
+ protected
+ def default_written_on
+ self.written_on = Time.now unless attribute_present?("written_on")
+ end
+
+ def destroy_children
+ self.class.delete_all "parent_id = #{id}"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/fixtures/topics/first b/activerecord/test/fixtures/topics/first
new file mode 100755
index 0000000000..9972a578c8
--- /dev/null
+++ b/activerecord/test/fixtures/topics/first
@@ -0,0 +1,9 @@
+id => 1
+title => The First Topic
+author_name => David
+author_email_address => david@loudthinking.com
+written_on => 2003-07-16 15:28
+last_read => 2004-04-15
+content => Have a nice day
+approved => 0
+replies_count => 0 \ No newline at end of file
diff --git a/activerecord/test/fixtures/topics/second b/activerecord/test/fixtures/topics/second
new file mode 100755
index 0000000000..f669b4fef4
--- /dev/null
+++ b/activerecord/test/fixtures/topics/second
@@ -0,0 +1,8 @@
+id => 2
+title => The Second Topic's of the day
+author_name => Mary
+written_on => 2003-07-15 15:28
+content => Have a great day!
+approved => 1
+replies_count => 2
+parent_id => 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures_test.rb b/activerecord/test/fixtures_test.rb
new file mode 100755
index 0000000000..015b6ababe
--- /dev/null
+++ b/activerecord/test/fixtures_test.rb
@@ -0,0 +1,84 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/developer'
+require 'fixtures/company'
+
+class FixturesTest < Test::Unit::TestCase
+ fixtures :topics, :developers, :accounts, :developers
+
+ FIXTURES = %w( accounts companies customers
+ developers developers_projects entrants
+ movies projects subscribers topics )
+ MATCH_ATTRIBUTE_NAME = /[a-zA-Z][-_\w]*/
+
+ def test_clean_fixtures
+ FIXTURES.each do |name|
+ fixtures = nil
+ assert_nothing_raised { fixtures = create_fixtures(name) }
+ assert_kind_of(Fixtures, fixtures)
+ fixtures.each { |name, fixture|
+ fixture.each { |key, value|
+ assert_match(MATCH_ATTRIBUTE_NAME, key)
+ }
+ }
+ end
+ end
+
+ def test_multiple_clean_fixtures
+ fixtures_array = nil
+ assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) }
+ assert_kind_of(Array, fixtures_array)
+ fixtures_array.each { |fixtures| assert_kind_of(Fixtures, fixtures) }
+ end
+
+ def test_attributes
+ topics = create_fixtures("topics")
+ assert_equal("The First Topic", topics["first"]["title"])
+ assert_nil(topics["second"]["author_email_address"])
+ end
+
+ def test_inserts
+ topics = create_fixtures("topics")
+ firstRow = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'")
+ assert_equal("The First Topic", firstRow["title"])
+
+ secondRow = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'Mary'")
+ assert_nil(secondRow["author_email_address"])
+ end
+
+ def test_bad_format
+ path = File.join(File.dirname(__FILE__), 'fixtures', 'bad_fixtures')
+ Dir.entries(path).each do |file|
+ next unless File.file?(file) and file !~ %r(^.|.yaml$)
+ assert_raise(Fixture::FormatError) {
+ Fixture.new(bad_fixtures_path, file)
+ }
+ end
+ end
+
+ def test_logger_level_invariant
+ level = ActiveRecord::Base.logger.level
+ create_fixtures('topics')
+ assert_equal level, ActiveRecord::Base.logger.level
+ end
+
+ def test_instantiation
+ topics = create_fixtures("topics")
+ assert_kind_of Topic, topics["first"].find
+ end
+
+ def test_complete_instantiation
+ assert_equal 2, @topics.size
+ assert_equal "The First Topic", @first.title
+ end
+
+ def test_fixtures_from_root_yml_with_instantiation
+ # assert_equal 2, @accounts.size
+ assert_equal 50, @unknown.credit_limit
+ end
+
+ def test_erb_in_fixtures
+ assert_equal 10, @developers.size
+ assert_equal "fixture_5", @dev_5.name
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/inflector_test.rb b/activerecord/test/inflector_test.rb
new file mode 100644
index 0000000000..4665558e74
--- /dev/null
+++ b/activerecord/test/inflector_test.rb
@@ -0,0 +1,121 @@
+require 'abstract_unit'
+
+class InflectorTest < Test::Unit::TestCase
+ SingularToPlural = {
+ "search" => "searches",
+ "switch" => "switches",
+ "fix" => "fixes",
+ "box" => "boxes",
+ "process" => "processes",
+ "address" => "addresses",
+ "case" => "cases",
+ "stack" => "stacks",
+
+ "category" => "categories",
+ "query" => "queries",
+ "ability" => "abilities",
+ "agency" => "agencies",
+
+ "wife" => "wives",
+ "safe" => "saves",
+ "half" => "halves",
+
+ "salesperson" => "salespeople",
+ "person" => "people",
+
+ "spokesman" => "spokesmen",
+ "man" => "men",
+ "woman" => "women",
+
+ "basis" => "bases",
+ "diagnosis" => "diagnoses",
+
+ "datum" => "data",
+ "medium" => "media",
+
+ "node_child" => "node_children",
+ "child" => "children",
+
+ "experience" => "experiences",
+ "day" => "days",
+
+ "comment" => "comments",
+ "foobar" => "foobars"
+ }
+
+ CamelToUnderscore = {
+ "Product" => "product",
+ "SpecialGuest" => "special_guest",
+ "AbstractApplicationController" => "abstract_application_controller"
+ }
+
+ ClassNameToForeignKeyWithUnderscore = {
+ "Person" => "person_id",
+ "MyApplication::Billing::Account" => "account_id"
+ }
+
+ ClassNameToForeignKeyWithoutUnderscore = {
+ "Person" => "personid",
+ "MyApplication::Billing::Account" => "accountid"
+ }
+
+ ClassNameToTableName = {
+ "PrimarySpokesman" => "primary_spokesmen",
+ "NodeChild" => "node_children"
+ }
+
+ def test_pluralize
+ SingularToPlural.each do |singular, plural|
+ assert_equal(plural, Inflector.pluralize(singular))
+ end
+
+ assert_equal("plurals", Inflector.pluralize("plurals"))
+ end
+
+ def test_singularize
+ SingularToPlural.each do |singular, plural|
+ assert_equal(singular, Inflector.singularize(plural))
+ end
+ end
+
+ def test_camelize
+ CamelToUnderscore.each do |camel, underscore|
+ assert_equal(camel, Inflector.camelize(underscore))
+ end
+ end
+
+ def test_underscore
+ CamelToUnderscore.each do |camel, underscore|
+ assert_equal(underscore, Inflector.underscore(camel))
+ end
+
+ assert_equal "html_tidy", Inflector.underscore("HTMLTidy")
+ assert_equal "html_tidy_generator", Inflector.underscore("HTMLTidyGenerator")
+ end
+
+ def test_demodulize
+ assert_equal "Account", Inflector.demodulize("MyApplication::Billing::Account")
+ end
+
+ def test_foreign_key
+ ClassNameToForeignKeyWithUnderscore.each do |klass, foreign_key|
+ assert_equal(foreign_key, Inflector.foreign_key(klass))
+ end
+
+ ClassNameToForeignKeyWithoutUnderscore.each do |klass, foreign_key|
+ assert_equal(foreign_key, Inflector.foreign_key(klass, false))
+ end
+ end
+
+ def test_tableize
+ ClassNameToTableName.each do |class_name, table_name|
+ assert_equal(table_name, Inflector.tableize(class_name))
+ end
+ end
+
+ def test_classify
+ ClassNameToTableName.each do |class_name, table_name|
+ assert_equal(class_name, Inflector.classify(table_name))
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/inheritance_test.rb b/activerecord/test/inheritance_test.rb
new file mode 100755
index 0000000000..6f8175801d
--- /dev/null
+++ b/activerecord/test/inheritance_test.rb
@@ -0,0 +1,125 @@
+require 'abstract_unit'
+require 'fixtures/company'
+
+
+class InheritanceTest < Test::Unit::TestCase
+ def setup
+ @company_fixtures = create_fixtures "companies"
+ end
+
+ def switch_to_alt_inheritance_column
+ # we don't want misleading test results, so get rid of the values in the type column
+ Company.find_all(nil, "id").each do |c|
+ c['type'] = nil
+ c.save
+ end
+
+ def Company.inheritance_column() "ruby_type" end
+ end
+
+ def test_inheritance_find
+ assert Company.find(1).kind_of?(Firm), "37signals should be a firm"
+ assert Firm.find(1).kind_of?(Firm), "37signals should be a firm"
+ assert Company.find(2).kind_of?(Client), "Summit should be a client"
+ assert Client.find(2).kind_of?(Client), "Summit should be a client"
+ end
+
+ def test_alt_inheritance_find
+ switch_to_alt_inheritance_column
+ test_inheritance_find
+ end
+
+ def test_inheritance_find_all
+ companies = Company.find_all(nil, "id")
+ assert companies[0].kind_of?(Firm), "37signals should be a firm"
+ assert companies[1].kind_of?(Client), "Summit should be a client"
+ end
+
+ def test_alt_inheritance_find_all
+ switch_to_alt_inheritance_column
+ test_inheritance_find_all
+ end
+
+ def test_inheritance_save
+ firm = Firm.new
+ firm.name = "Next Angle"
+ firm.save
+
+ next_angle = Company.find(firm.id)
+ assert next_angle.kind_of?(Firm), "Next Angle should be a firm"
+ end
+
+ def test_alt_inheritance_save
+ switch_to_alt_inheritance_column
+ test_inheritance_save
+ end
+
+ def test_inheritance_condition
+ assert_equal 3, Company.find_all.length
+ assert_equal 1, Firm.find_all.length
+ assert_equal 2, Client.find_all.length
+ end
+
+ def test_alt_inheritance_condition
+ switch_to_alt_inheritance_column
+ test_inheritance_condition
+ end
+
+ def test_finding_incorrect_type_data
+ assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) }
+ assert_nothing_raised { Firm.find(1) }
+ end
+
+ def test_alt_finding_incorrect_type_data
+ switch_to_alt_inheritance_column
+ test_finding_incorrect_type_data
+ end
+
+ def test_update_all_within_inheritance
+ Client.update_all "name = 'I am a client'"
+ assert_equal "I am a client", Client.find_all.first.name
+ assert_equal "37signals", Firm.find_all.first.name
+ end
+
+ def test_alt_update_all_within_inheritance
+ switch_to_alt_inheritance_column
+ test_update_all_within_inheritance
+ end
+
+ def test_destroy_all_within_inheritance
+ Client.destroy_all
+ assert_equal 0, Client.find_all.length
+ assert_equal 1, Firm.find_all.length
+ end
+
+ def test_alt_destroy_all_within_inheritance
+ switch_to_alt_inheritance_column
+ test_destroy_all_within_inheritance
+ end
+
+ def test_find_first_within_inheritance
+ assert_kind_of Firm, Company.find_first("name = '37signals'")
+ assert_kind_of Firm, Firm.find_first("name = '37signals'")
+ assert_nil Client.find_first("name = '37signals'")
+ end
+
+ def test_alt_find_first_within_inheritance
+ switch_to_alt_inheritance_column
+ test_find_first_within_inheritance
+ end
+
+ def test_complex_inheritance
+ very_special_client = VerySpecialClient.create("name" => "veryspecial")
+ assert_equal very_special_client, VerySpecialClient.find_first("name = 'veryspecial'")
+ assert_equal very_special_client, SpecialClient.find_first("name = 'veryspecial'")
+ assert_equal very_special_client, Company.find_first("name = 'veryspecial'")
+ assert_equal very_special_client, Client.find_first("name = 'veryspecial'")
+ assert_equal 1, Client.find_all("name = 'Summit'").size
+ assert_equal very_special_client, Client.find(very_special_client.id)
+ end
+
+ def test_alt_complex_inheritance
+ switch_to_alt_inheritance_column
+ test_complex_inheritance
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/lifecycle_test.rb b/activerecord/test/lifecycle_test.rb
new file mode 100755
index 0000000000..8b34c8c24c
--- /dev/null
+++ b/activerecord/test/lifecycle_test.rb
@@ -0,0 +1,110 @@
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/developer'
+
+class Topic; def after_find() end end
+class Developer; def after_find() end end
+
+class TopicManualObserver
+ include Singleton
+
+ attr_reader :action, :object, :callbacks
+
+ def initialize
+ Topic.add_observer(self)
+ @callbacks = []
+ end
+
+ def update(callback_method, object)
+ @callbacks << { "callback_method" => callback_method, "object" => object }
+ end
+
+ def has_been_notified?
+ !@callbacks.empty?
+ end
+end
+
+class TopicaObserver < ActiveRecord::Observer
+ def self.observed_class() Topic end
+
+ attr_reader :topic
+
+ def after_find(topic)
+ @topic = topic
+ end
+end
+
+class TopicObserver < ActiveRecord::Observer
+ attr_reader :topic
+
+ def after_find(topic)
+ @topic = topic
+ end
+end
+
+class MultiObserver < ActiveRecord::Observer
+ attr_reader :record
+
+ def self.observed_class() [ Topic, Developer ] end
+
+ def after_find(record)
+ @record = record
+ end
+
+end
+
+class LifecycleTest < Test::Unit::TestCase
+ def setup
+ @topics, @developers = create_fixtures("topics", "developers")
+ end
+
+ def test_before_destroy
+ assert_equal 2, Topic.count
+ Topic.find(1).destroy
+ assert_equal 0, Topic.count
+ end
+
+ def test_after_save
+ topic_observer = TopicManualObserver.instance
+
+ topic = Topic.find(1)
+ topic.title = "hello"
+ topic.save
+
+ assert topic_observer.has_been_notified?
+ assert_equal :after_save, topic_observer.callbacks.last["callback_method"]
+ end
+
+ def test_observer_update_on_save
+ topic_observer = TopicManualObserver.instance
+
+ topic = Topic.find(1)
+ assert topic_observer.has_been_notified?
+ assert_equal :after_find, topic_observer.callbacks.first["callback_method"]
+ end
+
+ def test_auto_observer
+ topic_observer = TopicaObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic_observer.topic.title, topic.title
+ end
+
+ def test_infered_auto_observer
+ topic_observer = TopicObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal topic_observer.topic.title, topic.title
+ end
+
+ def test_observing_two_classes
+ multi_observer = MultiObserver.instance
+
+ topic = Topic.find(1)
+ assert_equal multi_observer.record.title, topic.title
+
+ developer = Developer.find(1)
+ assert_equal multi_observer.record.name, developer.name
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/modules_test.rb b/activerecord/test/modules_test.rb
new file mode 100644
index 0000000000..f43bb1d077
--- /dev/null
+++ b/activerecord/test/modules_test.rb
@@ -0,0 +1,29 @@
+require 'abstract_unit'
+# require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'fixtures/company_in_module'
+
+class ModulesTest < Test::Unit::TestCase
+ def setup
+ create_fixtures "accounts"
+ create_fixtures "companies"
+ create_fixtures "projects"
+ create_fixtures "developers"
+ end
+
+ def test_module_spanning_associations
+ assert MyApplication::Business::Firm.find_first.has_clients?, "Firm should have clients"
+ firm = MyApplication::Business::Firm.find_first
+ assert_nil firm.class.table_name.match('::'), "Firm shouldn't have the module appear in its table name"
+ assert_equal 2, firm.clients_count, "Firm should have two clients"
+ end
+
+ def test_module_spanning_has_and_belongs_to_many_associations
+ project = MyApplication::Business::Project.find_first
+ project.developers << MyApplication::Business::Developer.create("name" => "John")
+ assert "John", project.developers.last.name
+ end
+
+ def test_associations_spanning_cross_modules
+ assert MyApplication::Billing::Account.find(1).has_firm?, "37signals account should be able to backtrack"
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/multiple_db_test.rb b/activerecord/test/multiple_db_test.rb
new file mode 100644
index 0000000000..f2f73c0dda
--- /dev/null
+++ b/activerecord/test/multiple_db_test.rb
@@ -0,0 +1,46 @@
+require 'abstract_unit'
+require 'fixtures/course'
+require 'fixtures/entrant'
+
+class MultipleDbTest < Test::Unit::TestCase
+ def setup
+ @courses = create_fixtures("courses") { Course.retrieve_connection }
+ @entrants = create_fixtures("entrants")
+ end
+
+ def test_connected
+ assert_not_nil Entrant.connection
+ assert_not_nil Course.connection
+ end
+
+ def test_proper_connection
+ assert_not_equal(Entrant.connection, Course.connection)
+ assert_equal(Entrant.connection, Entrant.retrieve_connection)
+ assert_equal(Course.connection, Course.retrieve_connection)
+ assert_equal(ActiveRecord::Base.connection, Entrant.connection)
+ end
+
+ def test_find
+ c1 = Course.find(1)
+ assert_equal "Ruby Development", c1.name
+ c2 = Course.find(2)
+ assert_equal "Java Development", c2.name
+ e1 = Entrant.find(1)
+ assert_equal "Ruby Developer", e1.name
+ e2 = Entrant.find(2)
+ assert_equal "Ruby Guru", e2.name
+ e3 = Entrant.find(3)
+ assert_equal "Java Lover", e3.name
+ end
+
+ def test_associations
+ c1 = Course.find(1)
+ assert_equal 2, c1.entrants_count
+ e1 = Entrant.find(1)
+ assert_equal e1.course.id, c1.id
+ c2 = Course.find(2)
+ assert_equal 1, c2.entrants_count
+ e3 = Entrant.find(3)
+ assert_equal e3.course.id, c2.id
+ end
+end
diff --git a/activerecord/test/pk_test.rb b/activerecord/test/pk_test.rb
new file mode 100644
index 0000000000..aefaebde6e
--- /dev/null
+++ b/activerecord/test/pk_test.rb
@@ -0,0 +1,59 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/subscriber'
+require 'fixtures/movie'
+
+class PrimaryKeysTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @subscribers = create_fixtures "subscribers"
+ @movies = create_fixtures "movies"
+ end
+
+ def test_integer_key
+ topic = Topic.find(1)
+ assert_equal(@topics["first"]["author_name"], topic.author_name)
+ topic = Topic.find(2)
+ assert_equal(@topics["second"]["author_name"], topic.author_name)
+
+ topic = Topic.new
+ topic.title = "New Topic"
+ assert_equal(nil, topic.id)
+ assert_nothing_raised{ topic.save }
+ id = topic.id
+
+ topicReloaded = Topic.find(id)
+ assert_equal("New Topic", topicReloaded.title)
+ end
+
+ def test_string_key
+ subscriber = Subscriber.find(@subscribers["first"]["nick"])
+ assert_equal(@subscribers["first"]["name"], subscriber.name)
+ subscriber = Subscriber.find(@subscribers["second"]["nick"])
+ assert_equal(@subscribers["second"]["name"], subscriber.name)
+
+ subscriber = Subscriber.new
+ subscriber.id = "jdoe"
+ assert_equal("jdoe", subscriber.id)
+ subscriber.name = "John Doe"
+ assert_nothing_raised{ subscriber.save }
+
+ subscriberReloaded = Subscriber.find("jdoe")
+ assert_equal("John Doe", subscriberReloaded.name)
+ end
+
+ def test_find_with_more_than_one_string_key
+ assert_equal 2, Subscriber.find(@subscribers["first"]["nick"], @subscribers["second"]["nick"]).length
+ end
+
+ def test_primary_key_prefix
+ ActiveRecord::Base.primary_key_prefix_type = :table_name
+ assert_equal "topicid", Topic.primary_key
+
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ assert_equal "topic_id", Topic.primary_key
+
+ ActiveRecord::Base.primary_key_prefix_type = nil
+ assert_equal "id", Topic.primary_key
+ end
+end
diff --git a/activerecord/test/reflection_test.rb b/activerecord/test/reflection_test.rb
new file mode 100644
index 0000000000..5d7e9d1197
--- /dev/null
+++ b/activerecord/test/reflection_test.rb
@@ -0,0 +1,78 @@
+#require File.dirname(__FILE__) + '/../dev-utils/eval_debugger'
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/customer'
+require 'fixtures/company'
+require 'fixtures/company_in_module'
+
+class ReflectionTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @customers = create_fixtures "customers"
+ @companies = create_fixtures "companies"
+ @first = Topic.find(1)
+ end
+
+ def test_read_attribute_names
+ assert_equal(
+ %w( id title author_name author_email_address written_on last_read content approved replies_count parent_id type ).sort,
+ @first.attribute_names
+ )
+ end
+
+ def test_columns
+ assert_equal 11, Topic.columns.length
+ end
+
+ def test_content_columns
+ assert_equal 7, Topic.content_columns.length
+ end
+
+ def test_column_string_type_and_limit
+ assert_equal :string, @first.column_for_attribute("title").type
+ assert_equal 255, @first.column_for_attribute("title").limit
+ end
+
+ def test_human_name_for_column
+ assert_equal "Author name", @first.column_for_attribute("author_name").human_name
+ end
+
+ def test_integer_columns
+ assert_equal :integer, @first.column_for_attribute("id").type
+ end
+
+ def test_aggregation_reflection
+ reflection_for_address = ActiveRecord::Reflection::AggregateReflection.new(
+ :address, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ )
+
+ reflection_for_balance = ActiveRecord::Reflection::AggregateReflection.new(
+ :balance, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ )
+
+ assert_equal(
+ [ reflection_for_address, reflection_for_balance ],
+ Customer.reflect_on_all_aggregations
+ )
+
+ assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address)
+
+ assert_equal Address, Customer.reflect_on_aggregation(:address).klass
+ end
+
+ def test_association_reflection
+ reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(
+ :clients, { :order => "id", :dependent => true }, Firm
+ )
+
+ assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
+
+ assert_equal Client, Firm.reflect_on_association(:clients).klass
+ assert_equal Client, Firm.reflect_on_association(:clients_of_firm).klass
+ end
+
+ def test_association_reflection_in_modules
+ assert_equal MyApplication::Business::Client, MyApplication::Business::Firm.reflect_on_association(:clients_of_firm).klass
+ assert_equal MyApplication::Business::Firm, MyApplication::Billing::Account.reflect_on_association(:firm).klass
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/thread_safety_test.rb b/activerecord/test/thread_safety_test.rb
new file mode 100644
index 0000000000..635240c6af
--- /dev/null
+++ b/activerecord/test/thread_safety_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+
+class ThreadSafetyTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @threads = []
+ end
+
+ def test_threading_on_transactions
+ # SQLite breaks down under thread banging
+ # Jamis Buck (author of SQLite-ruby): "I know that sqlite itself is not designed for concurrent access"
+ if ActiveRecord::ConnectionAdapters.const_defined? :SQLiteAdapter
+ return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLiteAdapter)
+ end
+
+ 5.times do |thread_number|
+ @threads << Thread.new(thread_number) do |thread_number|
+ first, second = Topic.find(1, 2)
+ Topic.transaction(first, second) do
+ Topic.logger.info "started #{thread_number}"
+ first.approved = 1
+ second.approved = 0
+ first.save
+ second.save
+ Topic.logger.info "ended #{thread_number}"
+ end
+ end
+ end
+
+ @threads.each { |t| t.join }
+ end
+end
diff --git a/activerecord/test/transactions_test.rb b/activerecord/test/transactions_test.rb
new file mode 100644
index 0000000000..18b2ea3e65
--- /dev/null
+++ b/activerecord/test/transactions_test.rb
@@ -0,0 +1,110 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+
+
+class TransactionTest < Test::Unit::TestCase
+ def setup
+ @topics = create_fixtures "topics"
+ @first, @second = Topic.find(1, 2)
+ end
+
+ def test_successful
+ Topic.transaction do
+ @first.approved = 1
+ @second.approved = 0
+ @first.save
+ @second.save
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_successful_with_instance_method
+ @first.transaction do
+ @first.approved = 1
+ @second.approved = 0
+ @first.save
+ @second.save
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+ def test_failing_on_exception
+ begin
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ raise "Bad things!"
+ end
+ rescue
+ # caught it
+ end
+
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
+
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
+ end
+
+ def test_failing_with_object_rollback
+ begin
+ Topic.transaction(@first, @second) do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ raise "Bad things!"
+ end
+ rescue
+ # caught it
+ end
+
+ assert !@first.approved?, "First shouldn't have been approved"
+ assert @second.approved?, "Second should still be approved"
+ end
+
+ def test_callback_rollback_in_save
+ add_exception_raising_after_save_callback_to_topic
+
+ begin
+ @first.approved = true
+ @first.save
+ flunk
+ rescue => e
+ assert_equal "Make the transaction rollback", e.message
+ assert !Topic.find(1).approved?
+ ensure
+ remove_exception_raising_after_save_callback_to_topic
+ end
+ end
+
+ def xtest_nested_explicit_transactions
+ Topic.transaction do
+ Topic.transaction do
+ @first.approved = 1
+ @second.approved = 0
+ @first.save
+ @second.save
+ end
+ end
+
+ assert Topic.find(1).approved?, "First should have been approved"
+ assert !Topic.find(2).approved?, "Second should have been unapproved"
+ end
+
+
+ private
+ def add_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
+ end
+
+ def remove_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { remove_method :after_save }
+ end
+end
diff --git a/activerecord/test/unconnected_test.rb b/activerecord/test/unconnected_test.rb
new file mode 100755
index 0000000000..0966dd9b06
--- /dev/null
+++ b/activerecord/test/unconnected_test.rb
@@ -0,0 +1,24 @@
+require 'abstract_unit'
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestUnconnectedAdaptor < Test::Unit::TestCase
+
+ def setup
+ @connection = ActiveRecord::Base.remove_connection
+ end
+
+ def teardown
+ ActiveRecord::Base.establish_connection(@connection)
+ end
+
+ def test_unconnected
+ assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+ assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.new.save
+ end
+ end
+end
diff --git a/activerecord/test/validations_test.rb b/activerecord/test/validations_test.rb
new file mode 100755
index 0000000000..27a9b21c7d
--- /dev/null
+++ b/activerecord/test/validations_test.rb
@@ -0,0 +1,126 @@
+require 'abstract_unit'
+require 'fixtures/topic'
+require 'fixtures/reply'
+require 'fixtures/developer'
+
+
+class ValidationsTest < Test::Unit::TestCase
+ def setup
+ @topic_fixtures = create_fixtures("topics")
+ @developers = create_fixtures("developers")
+ end
+
+ def test_single_field_validation
+ r = Reply.new
+ r.title = "There's no content!"
+ assert !r.save, "A reply without content shouldn't be saveable"
+
+ r.content = "Messa content!"
+ assert r.save, "A reply with content should be saveable"
+ end
+
+ def test_single_attr_validation_and_error_msg
+ r = Reply.new
+ r.title = "There's no content!"
+ r.save
+ assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error"
+ assert_equal 1, r.errors.count
+ end
+
+ def test_double_attr_validation_and_error_msg
+ r = Reply.new
+ assert !r.save
+
+ assert r.errors.invalid?("title"), "A reply without title should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("title"), "A reply without title should contain an error"
+
+ assert r.errors.invalid?("content"), "A reply without content should mark that attribute as invalid"
+ assert_equal "Empty", r.errors.on("content"), "A reply without content should contain an error"
+
+ assert_equal 2, r.errors.count
+ end
+
+ def test_error_on_create
+ r = Reply.new
+ r.title = "Wrong Create"
+ assert !r.save
+ assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid"
+ assert_equal "is Wrong Create", r.errors.on("title"), "A reply with a bad content should contain an error"
+ end
+
+
+ def test_error_on_update
+ r = Reply.new
+ r.title = "Bad"
+ r.content = "Good"
+
+ assert r.save, "First save should be successful"
+
+ r.title = "Wrong Update"
+ assert !r.save, "Second save should fail"
+
+ assert r.errors.invalid?("title"), "A reply with a bad title should mark that attribute as invalid"
+ assert_equal "is Wrong Update", r.errors.on("title"), "A reply with a bad content should contain an error"
+ end
+
+ def test_single_error_per_attr_iteration
+ r = Reply.new
+ r.save
+
+ errors = []
+ r.errors.each { |attr, msg| errors << [attr, msg] }
+
+ assert errors.include?(["title", "Empty"])
+ assert errors.include?(["content", "Empty"])
+ end
+
+ def test_multiple_errors_per_attr_iteration_with_full_error_composition
+ r = Reply.new
+ r.title = "Wrong Create"
+ r.content = "Mismatch"
+ r.save
+
+ errors = []
+ r.errors.each_full { |error| errors << error }
+
+ assert_equal "Title is Wrong Create", errors[0]
+ assert_equal "Title is Content Mismatch", errors[1]
+ assert_equal 2, r.errors.count
+ end
+
+ def test_errors_on_base
+ r = Reply.new
+ r.content = "Mismatch"
+ r.save
+ r.errors.add_to_base "Reply is not dignifying"
+
+ errors = []
+ r.errors.each_full { |error| errors << error }
+
+ assert_equal "Reply is not dignifying", r.errors.on_base
+
+ assert errors.include?("Title Empty")
+ assert errors.include?("Reply is not dignifying")
+ assert_equal 2, r.errors.count
+ end
+
+ def test_create_without_validation
+ reply = Reply.new
+ assert !reply.save
+ assert reply.save(false)
+ end
+
+ def test_errors_on_boundary_breaking
+ developer = Developer.new("name" => "xs")
+ assert !developer.save
+ assert_equal "is too short (min is 3 characters)", developer.errors.on("name")
+
+ developer.name = "All too very long for this boundary, it really is"
+ assert !developer.save
+ assert_equal "is too long (max is 20 characters)", developer.errors.on("name")
+
+ developer.name = "Just right"
+ assert developer.save
+ end
+end