",
+ form("post")
+ )
+ end
+
+ def test_form_with_date
+ def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end
+
+ assert_equal(
+ "",
+ form("post")
+ )
+ end
+
+ def test_form_with_datetime
+ def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end
+ @post.written_on = Time.gm(2004, 6, 15, 16, 30)
+
+ assert_equal(
+ "",
+ form("post")
+ )
+ end
+end
\ No newline at end of file
diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb
new file mode 100755
index 0000000000..a8ad37918d
--- /dev/null
+++ b/actionpack/test/template/date_helper_test.rb
@@ -0,0 +1,104 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper'
+
+class DateHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::DateHelper
+
+ def test_distance_in_words
+ from = Time.mktime(2004, 3, 6, 21, 41, 18)
+
+ assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 25))
+ assert_equal "5 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 46, 25))
+ assert_equal "about 1 hour", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 22, 47, 25))
+ assert_equal "about 3 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 0, 41))
+ assert_equal "about 4 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 1, 20))
+ assert_equal "2 days", distance_of_time_in_words(from, Time.mktime(2004, 3, 9, 15, 40))
+ end
+
+ def test_select_day
+ expected = "\n"
+
+ assert_equal expected, select_day(Time.mktime(2003, 8, 16))
+ assert_equal expected, select_day(16)
+ end
+
+ def test_select_day_with_blank
+ expected = "\n"
+
+ assert_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true)
+ assert_equal expected, select_day(16, :include_blank => true)
+ end
+
+ def test_select_month
+ expected = "\n"
+
+ assert_equal expected, select_month(Time.mktime(2003, 8, 16))
+ assert_equal expected, select_month(8)
+ end
+
+ def test_select_month_with_numbers
+ expected = "\n"
+
+ assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true)
+ assert_equal expected, select_month(8, :use_month_numbers => true)
+ end
+
+ def test_select_month_with_numbers_and_names
+ expected = "\n"
+
+ assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true)
+ assert_equal expected, select_month(8, :add_month_numbers => true)
+ end
+
+ def test_select_year
+ expected = "\n"
+
+ assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005)
+ assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005)
+ end
+
+ def test_select_year_with_type_discarding
+ expected = "\n"
+
+ assert_equal expected, select_year(
+ Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ assert_equal expected, select_year(
+ 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ end
+
+
+ def test_select_date
+ expected = "\n"
+
+ expected << "\n"
+
+ expected << "\n"
+
+ assert_equal expected, select_date(
+ Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]"
+ )
+ end
+end
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
new file mode 100644
index 0000000000..8f3d5ebb94
--- /dev/null
+++ b/actionpack/test/template/form_helper_test.rb
@@ -0,0 +1,124 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_helper'
+
+class FormHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::FormHelper
+
+ old_verbose, $VERBOSE = $VERBOSE, nil
+ Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on)
+ $VERBOSE = old_verbose
+
+ def setup
+ @post = Post.new
+ def @post.errors() Class.new{ def on(field) field == "author_name" end }.new end
+
+ @post.title = "Hello World"
+ @post.author_name = ""
+ @post.body = "Back to the hill and over it again!"
+ @post.secret = 1
+ @post.written_on = Date.new(2004, 6, 15)
+ end
+
+ def test_text_field
+ assert_equal(
+ '', text_field("post", "title")
+ )
+ assert_equal(
+ '', password_field("post", "title")
+ )
+ assert_equal(
+ '', password_field("person", "name")
+ )
+ end
+
+ def test_text_field_with_escapes
+ @post.title = "Hello World"
+ assert_equal(
+ '', text_field("post", "title")
+ )
+ end
+
+ def test_text_field_with_options
+ assert_equal(
+ '',
+ text_field("post", "title", "size" => "35")
+ )
+ end
+
+ def test_text_field_assuming_size
+ assert_equal(
+ '',
+ text_field("post", "title", "maxlength" => 35)
+ )
+ end
+
+ def test_check_box
+ assert_equal(
+ '',
+ check_box("post", "secret")
+ )
+
+ @post.secret = 0
+ assert_equal(
+ '',
+ check_box("post", "secret")
+ )
+
+ @post.secret = true
+ assert_equal(
+ '',
+ check_box("post", "secret")
+ )
+ end
+
+ def test_text_area
+ assert_equal(
+ '',
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_area_with_escapes
+ @post.body = "Back to the hill and over it again!"
+ assert_equal(
+ '',
+ text_area("post", "body")
+ )
+ end
+
+ def test_date_selects
+ assert_equal(
+ '',
+ text_area("post", "body")
+ )
+ end
+
+
+ def test_explicit_name
+ assert_equal(
+ '', text_field("post", "title", "name" => "dont guess")
+ )
+ assert_equal(
+ '',
+ text_area("post", "body", "name" => "really!")
+ )
+ assert_equal(
+ '',
+ check_box("post", "secret", "name" => "i mean it")
+ )
+ end
+
+ def test_explicit_id
+ assert_equal(
+ '', text_field("post", "title", "id" => "dont guess")
+ )
+ assert_equal(
+ '',
+ text_area("post", "body", "id" => "really!")
+ )
+ assert_equal(
+ '',
+ check_box("post", "secret", "id" => "i mean it")
+ )
+ end
+end
diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb
new file mode 100644
index 0000000000..fa0a37aa36
--- /dev/null
+++ b/actionpack/test/template/form_options_helper_test.rb
@@ -0,0 +1,165 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_options_helper'
+
+class FormOptionsHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::FormOptionsHelper
+
+ old_verbose, $VERBOSE = $VERBOSE, nil
+ Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin)
+ Continent = Struct.new('Continent', :continent_name, :countries)
+ Country = Struct.new('Country', :country_id, :country_name)
+ $VERBOSE = old_verbose
+
+ def test_collection_options
+ @posts = [
+ Post.new(" went home", "", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ assert_equal(
+ "\n\n",
+ options_from_collection_for_select(@posts, "author_name", "title")
+ )
+ end
+
+
+ def test_collection_options_with_preselected_value
+ @posts = [
+ Post.new(" went home", "", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ assert_equal(
+ "\n\n",
+ options_from_collection_for_select(@posts, "author_name", "title", "Babe")
+ )
+ end
+
+ def test_collection_options_with_preselected_value_array
+ @posts = [
+ Post.new(" went home", "", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ assert_equal(
+ "\n\n",
+ options_from_collection_for_select(@posts, "author_name", "title", [ "Babe", "Cabe" ])
+ )
+ end
+
+ def test_array_options_for_select
+ assert_equal(
+ "\n\n",
+ options_for_select([ "", "USA", "Sweden" ])
+ )
+ end
+
+ def test_array_options_for_select_with_selection
+ assert_equal(
+ "\n\n",
+ options_for_select([ "Denmark", "", "Sweden" ], "")
+ )
+ end
+
+ def test_array_options_for_select_with_selection_array
+ assert_equal(
+ "\n\n",
+ options_for_select([ "Denmark", "", "Sweden" ], [ "", "Sweden" ])
+ )
+ end
+
+ def test_hash_options_for_select
+ assert_equal(
+ "\n",
+ options_for_select({ "$" => "Dollar", "" => "" })
+ )
+ end
+
+ def test_hash_options_for_select_with_selection
+ assert_equal(
+ "\n",
+ options_for_select({ "$" => "Dollar", "" => "" }, "Dollar")
+ )
+ end
+
+ def test_hash_options_for_select_with_selection
+ assert_equal(
+ "\n",
+ options_for_select({ "$" => "Dollar", "" => "" }, [ "Dollar", "" ])
+ )
+ end
+
+ def test_html_option_groups_from_collection
+ @continents = [
+ Continent.new("", [Country.new("", ""), Country.new("so", "Somalia")] ),
+ Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] )
+ ]
+
+ assert_equal(
+ "",
+ option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk")
+ )
+ end
+
+ def test_select
+ @post = Post.new
+ @post.category = ""
+ assert_equal(
+ "",
+ select("post", "category", %w( abe hest))
+ )
+ end
+
+ def test_select_with_blank
+ @post = Post.new
+ @post.category = ""
+ assert_equal(
+ "",
+ select("post", "category", %w( abe hest), :include_blank => true)
+ )
+ end
+
+ def test_collection_select
+ @posts = [
+ Post.new(" went home", "", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_equal(
+ "",
+ collection_select("post", "author_name", @posts, "author_name", "author_name")
+ )
+ end
+
+ def test_collection_select_with_blank_and_style
+ @posts = [
+ Post.new(" went home", "", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!")
+ ]
+
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_equal(
+ "",
+ collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px")
+ )
+ end
+
+ def test_country_select
+ @post = Post.new
+ @post.origin = "Denmark"
+ assert_equal(
+ "",
+ country_select("post", "origin")
+ )
+ end
+end
diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb
new file mode 100644
index 0000000000..c3289af50c
--- /dev/null
+++ b/actionpack/test/template/tag_helper_test.rb
@@ -0,0 +1,18 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper'
+
+class TagHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::UrlHelper
+
+ def test_tag
+ assert_equal "", tag("p", "class" => "show")
+ end
+
+ def test_content_tag
+ assert_equal "Create", content_tag("a", "Create", "href" => "create")
+ end
+
+ # FIXME: Test form tag
+end
\ No newline at end of file
diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb
new file mode 100644
index 0000000000..347420a72b
--- /dev/null
+++ b/actionpack/test/template/text_helper_test.rb
@@ -0,0 +1,62 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/text_helper'
+
+class TextHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::TextHelper
+
+ def test_truncate
+ assert_equal "Hello World!", truncate("Hello World!", 12)
+ assert_equal "Hello Worl...", truncate("Hello World!!", 12)
+ end
+
+ def test_strip_links
+ assert_equal "on my mind", strip_links("on my mind")
+ end
+
+ def test_highlighter
+ assert_equal(
+ "This is a beautiful morning",
+ highlight("This is a beautiful morning", "beautiful")
+ )
+
+ assert_equal(
+ "This is a beautiful morning, but also a beautiful day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful")
+ )
+
+ assert_equal(
+ "This is a beautiful morning, but also a beautiful day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful", '\1')
+ )
+ end
+
+ def test_highlighter_with_regexp
+ assert_equal(
+ "This is a beautiful! morning",
+ highlight("This is a beautiful! morning", "beautiful!")
+ )
+
+ assert_equal(
+ "This is a beautiful! morning",
+ highlight("This is a beautiful! morning", "beautiful! morning")
+ )
+
+ assert_equal(
+ "This is a beautiful? morning",
+ highlight("This is a beautiful? morning", "beautiful? morning")
+ )
+ end
+
+ def test_excerpt
+ assert_equal("...is a beautiful morni...", excerpt("This is a beautiful morning", "beautiful", 5))
+ assert_equal("This is a...", excerpt("This is a beautiful morning", "this", 5))
+ assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", 5))
+ assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", 5))
+ assert_nil excerpt("This is a beautiful morning", "day")
+ end
+
+ def test_pluralization
+ assert_equal("1 count", pluralize(1, "count"))
+ assert_equal("2 counts", pluralize(2, "count"))
+ end
+end
\ No newline at end of file
diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb
new file mode 100644
index 0000000000..198b26b113
--- /dev/null
+++ b/actionpack/test/template/url_helper_test.rb
@@ -0,0 +1,49 @@
+require 'test/unit'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper'
+require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper'
+
+class UrlHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::UrlHelper
+ include ActionView::Helpers::TagHelper
+
+ def setup
+ @controller = Class.new do
+ def url_for(options, *parameters_for_method_reference)
+ "http://www.world.com"
+ end
+ end
+ @controller = @controller.new
+ end
+
+ # todo: missing test cases
+ def test_link_tag_with_straight_url
+ assert_equal "Hello", link_to("Hello", "http://www.world.com")
+ end
+
+ def test_link_tag_with_javascript_confirm
+ assert_equal(
+ "Hello",
+ link_to("Hello", "http://www.world.com", :confirm => "Are you sure?")
+ )
+ end
+
+ def test_link_unless_current
+ @params = { "controller" => "weblog", "action" => "show"}
+ assert_equal "Showing", link_to_unless_current("Showing", :action => "show", :controller => "weblog")
+ assert "Listing", link_to_unless_current("Listing", :action => "list", :controller => "weblog")
+ end
+
+ def test_mail_to
+ assert_equal "david@loudthinking.com", mail_to("david@loudthinking.com")
+ assert_equal "David Heinemeier Hansson", mail_to("david@loudthinking.com", "David Heinemeier Hansson")
+ assert_equal(
+ "David Heinemeier Hansson",
+ mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin")
+ )
+ end
+
+ def test_link_with_nil_html_options
+ assert "Hello",
+ link_to("Hello", {:action => 'myaction'}, nil)
+ end
+end
\ No newline at end of file
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 examples/shared_setup.rb 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//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
Binary files /dev/null and b/activerecord/examples/associations.png 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:
+ # * Customer#balance, Customer#balance=(money)
+ # * Customer#address, Customer#address=(address)
+ #
+ # 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
+ # composed_of :address would add address and address=(new_address).
+ #
+ # Options are:
+ # * :class_name - specify the class name of the association. Use it only if that name can't be infered
+ # from the part id. So composed_of :address 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.
+ # * :mapping - 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:
+ # * Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)
+ # * Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,
+ # Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager
+ # * Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),
+ # Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),
+ # Project#milestones.build, Project#milestones.create
+ # * Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),
+ # Project#categories.delete(category1)
+ #
+ # == 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 MyApplication::Business::Company.find(firm.id). 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 :class_name, 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
+ # has_many :clients would add among others has_clients?.
+ # * collection(force_reload = false) - returns an array of all the associated objects.
+ # An empty array is returned if none are found.
+ # * collection<<(object, ...) - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # * collection.delete(object, ...) - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
+ # * collection.clear - removes every object from the collection. This does not destroy the objects.
+ # * collection.empty? - returns true if there are no associated objects.
+ # * collection.size - returns the number of associated objects.
+ # * collection.find(id) - finds an associated object responding to the +id+ and that
+ # meets the condition that it has to be associated with this object.
+ # * collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil) - 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.
+ # * collection.build(attributes = {}) - 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.
+ # * collection.create(attributes = {}) - 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 has_many :clients, which will add:
+ # * Firm#clients (similar to Clients.find_all "firm_id = #{id}")
+ # * Firm#clients<<
+ # * Firm#clients.delete
+ # * Firm#clients.clear
+ # * Firm#clients.empty? (similar to firm.clients.size == 0)
+ # * Firm#clients.size (similar to Client.count "firm_id = #{id}")
+ # * Firm#clients.find (similar to Client.find_on_conditions(id, "firm_id = #{id}"))
+ # * Firm#clients.find_all (similar to Client.find_all "firm_id = #{id}")
+ # * Firm#clients.build (similar to Client.new("firm_id" => id))
+ # * Firm#clients.create (similar to c = Client.new("client_id" => id); c.save; c)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * :class_name - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So has_many :products 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.
+ # * :conditions - 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%'".
+ # * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
+ # such as "last_name, first_name DESC"
+ # * :foreign_key - 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.
+ # * :dependent - if set to true all the associated object are destroyed alongside this object.
+ # May not be set if :exclusively_dependent is also set.
+ # * :exclusively_dependent - 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.
+ # * :finder_sql - 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
+ # has_one :manager would add among others has_manager?.
+ # * association(force_reload = false) - returns the associated object. Nil is returned if none is found.
+ # * association=(associate) - assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # and saves the associate object.
+ # * association?(object, force_reload = false) - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * association.nil? - returns true if there is no associated object.
+ # * build_association(attributes = {}) - 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.
+ # * create_association(attributes = {}) - 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 has_one :beneficiary, which will add:
+ # * Account#beneficiary (similar to Beneficiary.find_first "account_id = #{id}")
+ # * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save)
+ # * Account#beneficiary? (similar to account.beneficiary == some_beneficiary)
+ # * Account#beneficiary.nil?
+ # * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id))
+ # * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b)
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * :class_name - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So has_one :manager 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.
+ # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "rank = 5".
+ # * :order - 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"
+ # * :dependent - if set to true the associated object is destroyed alongside this object
+ # * :foreign_key - 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
+ # belongs_to :author would add among others has_author?.
+ # * association(force_reload = false) - returns the associated object. Nil is returned if none is found.
+ # * association=(associate) - assigns the associate object, extracts the primary key, and sets it as the foreign key.
+ # * association?(object, force_reload = false) - returns true if the +object+ is of the same type and has the
+ # same id as the associated object.
+ # * association.nil? - returns true if there is no associated object.
+ #
+ # Example: An Post class declares has_one :author, which will add:
+ # * Post#author (similar to Author.find(author_id))
+ # * Post#author=(author) (similar to post.author_id = author.id)
+ # * Post#author? (similar to post.author == some_author)
+ # * Post#author.nil?
+ # The declaration can also include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * :class_name - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So has_one :author 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.
+ # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * :order - 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"
+ # * :foreign_key - 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.
+ # * :counter_cache - 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
+ # has_and_belongs_to_many :categories would add among others +add_categories+.
+ # * collection(force_reload = false) - returns an array of all the associated objects.
+ # An empty array is returned if none is found.
+ # * collection<<(object, ...) - 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).
+ # * collection.push_with_attributes(object, join_attributes) - adds one to the collection by creating an association in the join table that
+ # also holds the attributes from join_attributes (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).
+ # * collection.delete(object, ...) - removes one or more objects from the collection by removing their associations from the join table.
+ # This does not destroy the objects.
+ # * collection.clear - removes every object from the collection. This does not destroy the objects.
+ # * collection.empty? - returns true if there are no associated objects.
+ # * collection.size - returns the number of associated objects.
+ #
+ # Example: An Developer class declares has_and_belongs_to_many :projects, which will add:
+ # * Developer#projects
+ # * Developer#projects<<
+ # * Developer#projects.delete
+ # * Developer#projects.clear
+ # * Developer#projects.empty?
+ # * Developer#projects.size
+ # * Developer#projects.find(id)
+ # The declaration may include an options hash to specialize the behavior of the association.
+ #
+ # Options are:
+ # * :class_name - specify the class name of the association. Use it only if that name can't be infered
+ # from the association name. So has_and_belongs_to_many :projects 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.
+ # * :join_table - 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.
+ # * :foreign_key - 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.
+ # * :association_foreign_key - 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.
+ # * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE"
+ # sql fragment, such as "authorized = 1".
+ # * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
+ # * :uniq - if set to true, duplicate associated objects will be ignored by accessors and query methods
+ # * :finder_sql - overwrite the default generated SQL used to fetch the association with a manual one
+ # * :delete_sql - overwrite the default generated SQL used to remove links between the associated
+ # classes with a manual one
+ # * :insert_sql - 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 Base.inheritance_column). 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 establish_connection didn't include a
+ # :adapter key.
+ # * +AdapterNotSpecified+ -- the :adapter key used in establish_connection 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 :class_name option of
+ # the serialize definition.
+ # * +ConnectionNotEstablished+ -- no connection has been established. Use establish_connection 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 DiscussionBoard.increment_counter("post_count",
+ # discussion_board_id) 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 new(attributes) and
+ # attributes=(attributes). 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
+ # new(attributes) and attributes=(attributes). 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 file_name 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 attr_name 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 attr_name 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 attr_name 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 attr_name 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
+# 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
+
+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
+# 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 = < 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 @developers = Fixtures.create_fixtures(".", "developers") 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 @developers hash, so @developers["david"]["name"] will return
+# "David Heinemeier Hansson" and @developers["david"]["birthday"] will return Date.new(1979, 10, 15).
+#
+# 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 on(attribute)
+ # 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 on(attribute).
+ # 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 attribute_present?).
+ 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 Marshaled
+ # (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 Marshaled 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/" 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("", inverted["bar"])
+ assert_equal("", 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
diff --git a/railties/CHANGELOG b/railties/CHANGELOG
new file mode 100644
index 0000000000..60eb1c3e2a
--- /dev/null
+++ b/railties/CHANGELOG
@@ -0,0 +1,265 @@
+*CVS*
+
+* Added breakpoint support by default to the WEBrick dispatcher. This means that you can break out of execution at any point in
+ the code, investigate and change the model, AND then resume execution! Example:
+
+ class WeblogController < ActionController::Base
+ def index
+ @posts = Post.find_all
+ breakpoint "Breaking out from the list"
+ end
+ end
+
+ So the controller will accept the action, run the first line, then present you with a IRB prompt in the WEBrick window (you shouldn't
+ run as daemon when you want to use this). Here you can do things like:
+
+ Executing breakpoint "Breaking out from the list" at .../webrick_server.rb:16 in 'breakpoint'
+
+ >> @posts.inspect
+ => "[#nil, \"body\"=>nil, \"id\"=>\"1\"}>,
+ #\"Rails you know!\", \"body\"=>\"Only ten..\", \"id\"=>\"2\"}>]"
+ >> @posts.first.title = "hello from a breakpoint"
+ => "hello from a breakpoint"
+
+ ...and even better is that you can examine how your runtime objects actually work:
+
+ >> f = @posts.first
+ => #nil, "body"=>nil, "id"=>"1"}>
+ >> f.
+ Display all 152 possibilities? (y or n)
+
+ Finally, when you're ready to resume execution, you press CTRL-D
+
+* Changed environments to be configurable through an environment variable. By default, the environment is "development", but you
+ can change that and set your own by configuring the Apache vhost with a string like (mod_env must be available on the server):
+
+ SetEnv RAILS_ENV production
+
+ ...if you're using WEBrick, you can pick the environment to use with the command-line parameters -e/--environment, like this:
+
+ ruby public/dispatcher.servlet -e production
+
+* Added a new default environment called "development", which leaves the production environment to be tuned exclusively for that.
+
+* Added a start_server in the root of the Rails application to make it even easier to get started
+
+* Fixed public/.htaccess to use RewriteBase and share the same rewrite rules for all the dispatch methods
+
+* Fixed webrick_server to handle requests in a serialized manner (the Rails reloading infrastructure is not thread-safe)
+
+* Added support for controllers in directories. So you can have:
+
+ app/controllers/account_controller.rb # URL: /account/
+ app/controllers/admin/account_controller.rb # URL: /admin/account/
+
+ NOTE: You need to update your public/.htaccess with the new rules to pick it up
+
+* Added reloading for associations and dependencies 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_associations = false and ActionController::Base.reload_dependencies = false in production environments.
+
+* Added support for sub-directories in app/models. So now you can have something like Basecamp with:
+
+ app/models/accounting
+ app/models/project
+ app/models/participants
+ app/models/settings
+
+ It's poor man's namespacing, but only for file-system organization. You still require files just like before.
+ Nothing changes inside the files themselves.
+
+
+* Fixed a few references in the tests generated by new_mailer [bitsweat]
+
+* Added support for mocks in testing with test/mocks
+
+* Cleaned up the environments a bit and added global constant RAILS_ROOT
+
+
+*0.8.5* (9)
+
+* Made dev-util available to all tests, so you can insert breakpoints in any test case to get an IRB prompt at that point [bitsweat]:
+
+ def test_complex_stuff
+ @david.projects << @new_project
+ breakpoint "Let's have a closer look at @david"
+ end
+
+ You need to install dev-utils yourself for this to work ("gem install dev-util").
+
+* Added shared generator behavior so future upgrades should be possible without manually copying over files [bitsweat]
+
+* Added the new helper style to both controller and helper templates [bitsweat]
+
+* Added new_crud generator for creating a model and controller at the same time with explicit scaffolding [bitsweat]
+
+* Added configuration of Test::Unit::TestCase.fixture_path to test_helper to concide with the new AR fixtures style
+
+* Fixed that new_model was generating singular table/fixture names
+
+* Upgraded to Action Mailer 0.4.0
+
+* Upgraded to Action Pack 0.9.5
+
+* Upgraded to Active Record 1.1.0
+
+
+*0.8.0 (15)*
+
+* Removed custom_table_name option for new_model now that the Inflector is as powerful as it is
+
+* Changed the default rake action to just do testing and separate API generation and coding statistics into a "doc" task.
+
+* Fixed WEBrick dispatcher to handle missing slashes in the URLs gracefully [alexey]
+
+* Added user option for all postgresql tool calls in the rakefile [elvstone]
+
+* Fixed problem with running "ruby public/dispatch.servlet" instead of "cd public; ruby dispatch.servlet" [alexey]
+
+* Fixed WEBrick server so that it no longer hardcodes the ruby interpreter used to "ruby" but will get the one used based
+ on the Ruby runtime configuration. [Marcel Molina Jr.]
+
+* Fixed Dispatcher so it'll route requests to magic_beans to MagicBeansController/magic_beans_controller.rb [Caio Chassot]
+
+* "new_controller MagicBeans" and "new_model SubscriptionPayments" will now both behave properly as they use the new Inflector.
+
+* Fixed problem with MySQL foreign key constraint checks in Rake :clone_production_structure_to_test target [Andreas Schwarz]
+
+* Changed WEBrick server to by default be auto-reloading, which is slower but makes source changes instant.
+ Class compilation cache can be turned on with "-c" or "--cache-classes".
+
+* Added "-b/--binding" option to WEBrick dispatcher to bind the server to a specific IP address (default: 127.0.0.1) [Kevin Temp]
+
+* dispatch.fcgi now DOESN'T set FCGI_PURE_RUBY as it was slowing things down for now reason [Andreas Schwarz]
+
+* Added new_mailer generator to work with Action Mailer
+
+* Included new framework: Action Mailer 0.3
+
+* Upgraded to Action Pack 0.9.0
+
+* Upgraded to Active Record 1.0.0
+
+
+*0.7.0*
+
+* Added an optional second argument to the new_model script that allows the programmer to specify the table name,
+ which will used to generate a custom table_name method in the model and will also be used in the creation of fixtures.
+ [Kevin Radloff]
+
+* script/new_model now turns AccountHolder into account_holder instead of accountholder [Kevin Radloff]
+
+* Fixed the faulty handleing of static files with WEBrick [Andreas Schwarz]
+
+* Unified function_test_helper and unit_test_helper into test_helper
+
+* Fixed bug with the automated production => test database dropping on PostgreSQL [dhawkins]
+
+* create_fixtures in both the functional and unit test helper now turns off the log during fixture generation
+ and can generate more than one fixture at a time. Which makes it possible for assignments like:
+
+ @people, @projects, @project_access, @companies, @accounts =
+ create_fixtures "people", "projects", "project_access", "companies", "accounts"
+
+* Upgraded to Action Pack 0.8.5 (locally-scoped variables, partials, advanced send_file)
+
+* Upgraded to Active Record 0.9.5 (better table_name guessing, cloning, find_all_in_collection)
+
+
+*0.6.5*
+
+* No longer specifies a template for rdoc, so it'll use whatever is default (you can change it in the rakefile)
+
+* The new_model generator will now use the same rules for plural wordings as Active Record
+ (so Category will give categories, not categorys) [Kevin Radloff]
+
+* dispatch.fcgi now sets FCGI_PURE_RUBY to true to ensure that it's the Ruby version that's loaded [danp]
+
+* Made the GEM work with Windows
+
+* Fixed bug where mod_ruby would "forget" the load paths added when switching between controllers
+
+* PostgreSQL are now supported for the automated production => test database dropping [Kevin Radloff]
+
+* Errors thrown by the dispatcher are now properly handled in FCGI.
+
+* Upgraded to Action Pack 0.8.0 (lots and lots and lots of fixes)
+
+* Upgraded to Active Record 0.9.4 (a bunch of fixes)
+
+
+*0.6.0*
+
+* Added AbstractionApplicationController as a superclass for all controllers generated. This class can be used
+ to carry filters and methods that are to be shared by all. It has an accompanying ApplicationHelper that all
+ controllers will also automatically have available.
+
+* Added environments that can be included from any script to get the full Active Record and Action Controller
+ context running. This can be used by maintenance scripts or to interact with the model through IRB. Example:
+
+ require 'config/environments/production'
+
+ for account in Account.find_all
+ account.recalculate_interests
+ end
+
+ A short migration script for an account model that had it's interest calculation strategy changed.
+
+* Accessing the index of a controller with "/weblog" will now redirect to "/weblog/" (only on Apache, not WEBrick)
+
+* Simplified the default Apache config so even remote requests are served off CGI as a default.
+ You'll now have to do something specific to activate mod_ruby and FCGI (like using the force urls).
+ This should make it easier for new comers that start on an external server.
+
+* Added more of the necessary Apache options to .htaccess to make it easier to setup
+
+* Upgraded to Action Pack 0.7.9 (lots of fixes)
+
+* Upgraded to Active Record 0.9.3 (lots of fixes)
+
+
+*0.5.7*
+
+* Fixed bug in the WEBrick dispatcher that prevented it from getting parameters from the URL
+ (through GET requests or otherwise)
+
+* Added lib in root as a place to store app specific libraries
+
+* Added lib and vendor to load_path, so anything store within can be loaded directly.
+ Hence lib/redcloth.rb can be loaded with require "redcloth"
+
+* Upgraded to Action Pack 0.7.8 (lots of fixes)
+
+* Upgraded to Active Record 0.9.2 (minor upgrade)
+
+
+*0.5.6*
+
+* Upgraded to Action Pack 0.7.7 (multipart form fix)
+
+* Updated the generated template stubs to valid XHTML files
+
+* Ensure that controllers generated are capitalized, so "new_controller TodoLists"
+ gives the same as "new_controller Todolists" and "new_controller todolists".
+
+
+*0.5.5*
+
+* Works on Windows out of the box! (Dropped symlinks)
+
+* Added webrick dispatcher: Try "ruby public/dispatch.servlet --help" [Florian Gross]
+
+* Report errors about initialization to browser (instead of attempting to use uninitialized logger)
+
+* Upgraded to Action Pack 0.7.6
+
+* Upgraded to Active Record 0.9.1
+
+* Added distinct 500.html instead of reusing 404.html
+
+* Added MIT license
+
+
+*0.5.0*
+
+* First public release
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
new file mode 100644
index 0000000000..5919c288e4
--- /dev/null
+++ b/railties/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/railties/README b/railties/README
new file mode 100644
index 0000000000..4702f8a6c8
--- /dev/null
+++ b/railties/README
@@ -0,0 +1,121 @@
+== Welcome to Rails
+
+Rails is a web-application and persistance framework that includes everything
+needed to create database-backed web-applications according to the
+Model-View-Control pattern of separation. This pattern splits the view (also
+called the presentation) into "dumb" templates that are primarily responsible
+for inserting pre-build data in between HTML tags. The model contains the
+"smart" domain objects (such as Account, Product, Person, Post) that holds all
+the business logic and knows how to persist themselves to a database. The
+controller handles the incoming requests (such as Save New Account, Update
+Product, Show Post) by manipulating the model and directing data to the view.
+
+In Rails, the model is handled by what's called a object-relational mapping
+layer entitled Active Record. This layer allows you to present the data from
+database rows as objects and embellish these data objects with business logic
+methods. You can read more about Active Record in
+link:files/vendor/activerecord/README.html.
+
+The controller and view is handled by the Action Pack, which handles both
+layers by its two parts: Action View and Action Controller. These two layers
+are bundled in a single package due to their heavy interdependence. This is
+unlike the relationship between the Active Record and Action Pack that is much
+more separate. Each of these packages can be used independently outside of
+Rails. You can read more about Action Pack in
+link:files/vendor/actionpack/README.html.
+
+
+== Requirements
+
+* Database and driver (MySQL, PostgreSQL, or SQLite)
+* Rake[http://rake.rubyforge.org] for running tests and the generating documentation
+
+== Optionals
+
+* Apache 1.3.x or 2.x (or any FastCGI-capable webserver with a
+ mod_rewrite-like module)
+* FastCGI (or mod_ruby) for production performance (CGI is used for
+ development)
+
+== Getting started
+
+1a. Setup Apache for the Rails application (see "Example for Apache conf")
+1b. Run the WEBrick servlet: ruby public/dispatch.servlet --help
+2. Go to http://rails/ (or whatever is your ServerName) and check
+ that you get the "Congratulations, you're on Rails!" screen
+3. Follow the guidelines on the "Congratulations, you're on Rails!" screen
+
+
+== Example for Apache conf
+
+
+ ServerName rails
+ DocumentRoot /path/tapplication/public/
+ ErrorLog /path/application/log/apache.log
+
+
+ Options ExecCGI FollowSymLinks
+ AllowOverride all
+ Allow from all
+ Order allow,deny
+
+
+
+NOTE: Be sure that CGIs can be executed in that directory as well. So ExecCGI
+should be on and ".cgi" should respond. All requests from 127.0.0.1 goes
+through CGI, so no Apache restart is necessary for changes. All other requests
+goes through FCGI (or mod_ruby) that requires restart to show changes.
+
+
+== Debugging Rails
+
+Have "tail -f" commands running on both the apache.log, production.log, and
+test.log files. Rails will automatically display debugging and runtime
+information to these files. Debugging info will also be shown in the browser
+on requests from 127.0.0.1.
+
+
+== Description of contents
+
+app
+ Holds all the code that's specific to this particular application.
+
+app/controllers
+ Holds controllers that should be named like weblog_controller.rb for
+ automated URL mapping. All controllers should descend from
+ ActionController::Base.
+
+app/models
+ Holds models that should be named like post.rb.
+ Most models will descent from ActiveRecord::Base.
+
+app/views
+ Holds the template files for the view that should be named like
+ weblog/index.rhtml for the WeblogController#index action. All views uses eRuby
+ syntax. This directory can also be used to keep stylesheets, images, and so on
+ that can be symlinked to public.
+
+app/helpers
+ Holds view helpers that should be named like weblog_helper.rb.
+
+config
+ Configuration files for Apache, database, and other dependencies.
+
+lib
+ Application specific libraries. Basically, any kind of custom code that doesn't
+ belong controllers, models, or helpers. This directory is in the load path.
+
+public
+ The directory available for Apache, which includes symbolic links to other
+ parts of the structure that are to be made available. Refrain from placing
+ actual files in here if you're using CVS and don't want to check in this
+ directory.
+
+script
+ Helper scripts for automation and generation.
+
+test
+ Unit and functional tests along with fixtures.
+
+vendor
+ External libraries that the application depend on. This directory is in the load path.
diff --git a/railties/Rakefile b/railties/Rakefile
new file mode 100644
index 0000000000..aef5a4e884
--- /dev/null
+++ b/railties/Rakefile
@@ -0,0 +1,279 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+require 'date'
+
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'rails'
+PKG_VERSION = '0.8.5' + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}"
+
+desc "Default Task"
+task :default => [ :fresh_rails ]
+
+desc "Generates a fresh Rails package with documentation"
+task :fresh_rails => [ :make_dir_structure, :initialize_file_stubs, :copy_vendor_libraries, :copy_ties_content, :generate_documentation ]
+
+desc "Generates a fresh Rails package using GEMs with documentation"
+task :fresh_gem_rails => [ :make_dir_structure, :initialize_file_stubs, :copy_ties_content, :copy_gem_environment ]
+
+desc "Generates a fresh Rails package without documentation (faster)"
+task :fresh_rails_without_docs => [ :make_dir_structure, :initialize_file_stubs, :copy_vendor_libraries, :copy_ties_content ]
+
+desc "Packages the fresh Rails package with documentation"
+task :package => [ :clean, :fresh_rails ] do
+ system %{cd ..; tar -czvf #{PKG_NAME}-#{PKG_VERSION}.tgz #{PKG_NAME}}
+ system %{cd ..; zip -r #{PKG_NAME}-#{PKG_VERSION}.zip #{PKG_NAME}}
+end
+
+task :clean do
+ File.rm_rf "#{PKG_DESTINATION}"
+end
+
+
+# Make directory structure ----------------------------------------------------------------
+
+desc "Make the directory structure for the new Rails application"
+task :make_dir_structure => [ :make_base_dirs, :make_app_dirs, :make_public_dirs, :make_test_dirs ] do
+end
+
+task :make_base_dirs do
+ File.rm_rf PKG_DESTINATION
+ File.mkdir "#{PKG_DESTINATION}"
+ File.mkdir "#{PKG_DESTINATION}/app"
+ File.mkdir "#{PKG_DESTINATION}/config"
+ File.mkdir "#{PKG_DESTINATION}/config/environments"
+ File.mkdir "#{PKG_DESTINATION}/db"
+ File.mkdir "#{PKG_DESTINATION}/doc"
+ File.mkdir "#{PKG_DESTINATION}/log"
+ File.mkdir "#{PKG_DESTINATION}/lib"
+ File.mkdir "#{PKG_DESTINATION}/public"
+ File.mkdir "#{PKG_DESTINATION}/script"
+ File.mkdir "#{PKG_DESTINATION}/test"
+ File.mkdir "#{PKG_DESTINATION}/vendor"
+end
+
+task :make_app_dirs do
+ File.mkdir "#{PKG_DESTINATION}/app/models"
+ File.mkdir "#{PKG_DESTINATION}/app/controllers"
+ File.mkdir "#{PKG_DESTINATION}/app/helpers"
+ File.mkdir "#{PKG_DESTINATION}/app/views"
+ File.mkdir "#{PKG_DESTINATION}/app/views/layouts"
+end
+
+task :make_public_dirs do
+ File.mkdir "#{PKG_DESTINATION}/public/images"
+ File.mkdir "#{PKG_DESTINATION}/public/javascripts"
+ File.mkdir "#{PKG_DESTINATION}/public/stylesheets"
+ File.mkdir "#{PKG_DESTINATION}/public/_doc"
+end
+
+task :make_test_dirs do
+ File.mkdir "#{PKG_DESTINATION}/test/fixtures"
+ File.mkdir "#{PKG_DESTINATION}/test/unit"
+ File.mkdir "#{PKG_DESTINATION}/test/functional"
+ File.mkdir "#{PKG_DESTINATION}/test/mocks/development"
+ File.mkdir "#{PKG_DESTINATION}/test/mocks/testing"
+end
+
+
+# Initialize file stubs -------------------------------------------------------------------
+
+desc "Initialize empty file stubs (such as for logging)"
+task :initialize_file_stubs => [ :initialize_log_files ] do
+end
+
+task :initialize_log_files do
+ chmod 0777, "#{PKG_DESTINATION}/log"
+
+ File.touch "#{PKG_DESTINATION}/log/apache.log"
+ File.touch "#{PKG_DESTINATION}/log/production.log"
+
+ chmod 0777, "#{PKG_DESTINATION}/log/apache.log"
+ chmod 0777, "#{PKG_DESTINATION}/log/production.log"
+end
+
+
+# Copy Vendors ----------------------------------------------------------------------------
+
+desc "Copy in all the Rails packages to vendor"
+task :copy_vendor_libraries => [ :copy_action_pack, :copy_active_record, :copy_ties, :copy_action_mailer ]
+
+task :copy_action_pack do
+ File.cp_r "../actionpack", "#{PKG_DESTINATION}/vendor/actionpack"
+end
+
+task :copy_active_record do
+ File.cp_r "../activerecord", "#{PKG_DESTINATION}/vendor/activerecord"
+end
+
+task :copy_action_mailer do
+ File.cp_r "../actionmailer", "#{PKG_DESTINATION}/vendor/actionmailer"
+end
+
+task :copy_ties do
+ File.cp_r "../railties", "#{PKG_DESTINATION}/vendor/railties"
+end
+
+
+# Copy Ties Content -----------------------------------------------------------------------
+
+# :link_apache_config
+desc "Make copies of all the default content of ties"
+task :copy_ties_content => [
+ :copy_rootfiles, :copy_dispatches, :copy_html_files, :copy_abstract_application,
+ :copy_configs, :copy_generators, :copy_test_helpers, :copy_docs_in_public,
+ :copy_app_doc_readme ] do
+end
+
+task :copy_dispatches do
+ File.cp "dispatches/dispatch.rb", "#{PKG_DESTINATION}/public/dispatch.rb"
+ chmod 0755, "#{PKG_DESTINATION}/public/dispatch.rb"
+
+ File.cp "dispatches/dispatch.rb", "#{PKG_DESTINATION}/public/dispatch.cgi"
+ chmod 0755, "#{PKG_DESTINATION}/public/dispatch.cgi"
+
+ File.cp "dispatches/dispatch.fcgi", "#{PKG_DESTINATION}/public/dispatch.fcgi"
+ chmod 0755, "#{PKG_DESTINATION}/public/dispatch.fcgi"
+
+ File.cp "dispatches/dispatch.servlet", "#{PKG_DESTINATION}/public/dispatch.servlet"
+
+ File.cp "dispatches/start_server", "#{PKG_DESTINATION}/start_server"
+ chmod 0755, "#{PKG_DESTINATION}/start_server"
+end
+
+task :copy_html_files do
+ File.cp "html/404.html", "#{PKG_DESTINATION}/public/404.html"
+ File.cp "html/500.html", "#{PKG_DESTINATION}/public/500.html"
+ File.cp "html/index.html", "#{PKG_DESTINATION}/public/index.html"
+end
+
+task :copy_abstract_application do
+ File.cp "helpers/abstract_application.rb", "#{PKG_DESTINATION}/app/controllers/abstract_application.rb"
+ File.cp "helpers/application_helper.rb", "#{PKG_DESTINATION}/app/helpers/application_helper.rb"
+end
+
+task :copy_configs do
+ File.cp "configs/database.yml", "#{PKG_DESTINATION}/config/database.yml"
+
+ File.cp "configs/apache.conf", "#{PKG_DESTINATION}/public/.htaccess"
+
+ File.cp "environments/shared.rb", "#{PKG_DESTINATION}/config/environment.rb"
+ File.cp "environments/production.rb", "#{PKG_DESTINATION}/config/environments/production.rb"
+ File.cp "environments/development.rb", "#{PKG_DESTINATION}/config/environments/development.rb"
+ File.cp "environments/test.rb", "#{PKG_DESTINATION}/config/environments/test.rb"
+end
+
+task :copy_generators do
+ File.cp "generators/new_controller.rb", "#{PKG_DESTINATION}/script/new_controller"
+ File.cp "generators/new_model.rb", "#{PKG_DESTINATION}/script/new_model"
+ File.cp "generators/new_mailer.rb", "#{PKG_DESTINATION}/script/new_mailer"
+ File.cp "generators/new_crud.rb", "#{PKG_DESTINATION}/script/new_crud"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_controller"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_model"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_mailer"
+ chmod 0755, "#{PKG_DESTINATION}/script/new_crud"
+end
+
+task :copy_rootfiles do
+ File.cp "fresh_rakefile", "#{PKG_DESTINATION}/Rakefile"
+ File.cp "README", "#{PKG_DESTINATION}/README"
+end
+
+task :copy_test_helpers do
+ File.cp "helpers/test_helper.rb", "#{PKG_DESTINATION}/test/test_helper.rb"
+end
+
+task :copy_docs_in_public do
+ File.cp "doc/index.html", "#{PKG_DESTINATION}/public/_doc/index.html"
+end
+
+task :copy_app_doc_readme do
+ File.cp "doc/README_FOR_APP", "#{PKG_DESTINATION}/doc/README_FOR_APP"
+end
+
+task :link_apache_config do
+ cd "#{PKG_DESTINATION}/config/"
+ ln_s "../public/.htaccess", "apache.conf"
+ cd "../../railties"
+end
+
+
+# Generate documentation ------------------------------------------------------------------
+
+desc "Generate documentation for the framework and for the empty application"
+task :generate_documentation => [ :generate_app_doc, :generate_rails_framework_doc ] do
+end
+
+task :generate_rails_framework_doc do
+ system %{cd #{PKG_DESTINATION}; rake apidoc}
+end
+
+task :generate_app_doc do
+ File.cp "doc/README_FOR_APP", "#{PKG_DESTINATION}/doc/README_FOR_APP"
+ system %{cd #{PKG_DESTINATION}; rake appdoc}
+end
+
+
+# Generate GEM ----------------------------------------------------------------------------
+
+task :copy_gem_environment do
+ File.cp "environments/shared_for_gem.rb", "#{PKG_DESTINATION}/config/environment.rb"
+end
+
+
+PKG_FILES = FileList[
+ '[a-zA-Z]*',
+ 'bin/**/*',
+ 'configs/**/*',
+ 'doc/**/*',
+ 'dispatches/**/*',
+ 'environments/**/*',
+ 'generators/**/*',
+ 'helpers/**/*',
+ 'html/**/*',
+ 'lib/**/*'
+]
+
+spec = Gem::Specification.new do |s|
+ s.name = 'rails'
+ s.version = PKG_VERSION
+ s.summary = "Web-application framework with template engine, control-flow layer, and ORM."
+ s.description = <<-EOF
+ Rails is a framework for building web-application using CGI, FCGI, mod_ruby, or WEBrick
+ on top of either MySQL, PostgreSQL, or SQLite with eRuby-based templates.
+ EOF
+
+ s.add_dependency('rake', '>= 0.4.11')
+ s.add_dependency('activerecord', '>= 1.1.0')
+ s.add_dependency('actionpack', '>= 0.9.5')
+ s.add_dependency('actionmailer', '>= 0.4.0')
+ s.add_dependency('dev-utils', '>= 1.0.1')
+
+ s.files = PKG_FILES.to_a
+ s.require_path = 'lib'
+
+ s.bindir = "bin" # Use these for applications.
+ s.executables = ["rails"]
+ s.default_executable = "rails"
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://www.rubyonrails.org"
+ s.rubyforge_project = "rails"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+end
+
+# Publish beta gem
+desc "Publish the API documentation"
+task :pgem => [:gem] 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
\ No newline at end of file
diff --git a/railties/bin/rails b/railties/bin/rails
new file mode 100755
index 0000000000..846f02ac69
--- /dev/null
+++ b/railties/bin/rails
@@ -0,0 +1,28 @@
+if ARGV[0]
+ ENV["RAILS_PKG_DESTINATION"] = File.expand_path(ARGV[0])
+ if RUBY_PLATFORM =~ /mswin32/
+ Dir.chdir File.dirname(__FILE__)
+ system %{rake.cmd fresh_gem_rails}
+ else
+ system %{ cd #{File.dirname(__FILE__)}; rake fresh_gem_rails }
+ end
+else
+ puts <<-HELP
+
+NAME
+ rails - creates a new Rails installation
+
+SYNOPSIS
+ rails [full path]
+
+DESCRIPTION
+ This generator will create a suggested directory structure, lots of minor helper
+ files, and a default configuration for creating a new Rails application. Once the
+ generator is done, you're adviced to look at the README in the root of the folder.
+
+EXAMPLE
+ rails ~/Code/Ruby/weblog
+
+ This will generate a new Rails installation in the ~/Code/Ruby/weblog folder.
+HELP
+end
\ No newline at end of file
diff --git a/railties/configs/apache.conf b/railties/configs/apache.conf
new file mode 100755
index 0000000000..feb2e32c4e
--- /dev/null
+++ b/railties/configs/apache.conf
@@ -0,0 +1,31 @@
+# General Apache options
+AddHandler fastcgi-script .fcgi
+AddHandler cgi-script .cgi
+Options +FollowSymLinks +ExecCGI
+
+# Make sure that mod_ruby.c has been added and loaded as a module with Apache
+RewriteEngine On
+
+# Change extension from .cgi to .fcgi to switch to FCGI and to .rb to switch to mod_ruby
+RewriteBase /dispatch.cgi
+
+RewriteRule ^dispatch.servlet$ / [R]
+
+# Enable this rewrite rule to point to the controller/action that should serve root.
+# RewriteRule ^$ /controller/action
+
+# Add missing slash
+RewriteRule ^([-_a-zA-Z0-9]+)$ /$1/ [R]
+
+# Default rewriting rules.
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ ?controller=$1&action=$2&id=$3 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ ?controller=$1&action=$2 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/$ ?controller=$1&action=index [QSA,L]
+
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ ?module=$1&controller=$2&action=$3&id=$4 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ ?module=$1&controller=$2&action=$3 [QSA,L]
+RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/$ ?module=$1&controller=$2&action=index [QSA,L]
+
+# You can also point these error messages to a controller/action
+ErrorDocument 500 /500.html
+ErrorDocument 404 /404.html
\ No newline at end of file
diff --git a/railties/configs/database.yml b/railties/configs/database.yml
new file mode 100644
index 0000000000..0f9a7eb20e
--- /dev/null
+++ b/railties/configs/database.yml
@@ -0,0 +1,20 @@
+development:
+ adapter: mysql
+ database: rails_development
+ host: localhost
+ username: root
+ password:
+
+test:
+ adapter: mysql
+ database: rails_test
+ host: localhost
+ username: root
+ password:
+
+production:
+ adapter: mysql
+ database: rails_production
+ host: localhost
+ username: root
+ password:
diff --git a/railties/dispatches/dispatch.fcgi b/railties/dispatches/dispatch.fcgi
new file mode 100755
index 0000000000..dc43f03b19
--- /dev/null
+++ b/railties/dispatches/dispatch.fcgi
@@ -0,0 +1,7 @@
+#!/usr/local/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment"
+require 'dispatcher'
+require 'fcgi'
+
+FCGI.each_cgi { |cgi| Dispatcher.dispatch(cgi, Dispatcher::DEFAULT_SESSION_OPTIONS, File.dirname(__FILE__) + "/500.html") }
\ No newline at end of file
diff --git a/railties/dispatches/dispatch.rb b/railties/dispatches/dispatch.rb
new file mode 100755
index 0000000000..eb2c95e813
--- /dev/null
+++ b/railties/dispatches/dispatch.rb
@@ -0,0 +1,10 @@
+#!/usr/local/bin/ruby
+
+require File.dirname(__FILE__) + "/../config/environment"
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
+require "dispatcher"
+
+ADDITIONAL_LOAD_PATHS.flatten.each { |dir| $:.unshift "#{RAILS_ROOT}/#{dir}" }
+Dispatcher.dispatch
\ No newline at end of file
diff --git a/railties/dispatches/dispatch.servlet b/railties/dispatches/dispatch.servlet
new file mode 100644
index 0000000000..a1fa403a67
--- /dev/null
+++ b/railties/dispatches/dispatch.servlet
@@ -0,0 +1,49 @@
+#!/usr/local/bin/ruby
+
+require 'webrick'
+require 'optparse'
+
+OPTIONS = {
+ :port => 3000,
+ :ip => "127.0.0.1",
+ :environment => "development",
+ :server_root => File.expand_path(File.dirname(__FILE__)),
+ :server_type => WEBrick::SimpleServer
+}
+
+ARGV.options do |opts|
+ script_name = File.basename($0)
+ opts.banner = "Usage: ruby #{script_name} [options]"
+
+ opts.separator ""
+
+ opts.on("-p", "--port=port", Integer,
+ "Runs Rails on the specified port.",
+ "Default: 3000") { |OPTIONS[:port]| }
+ opts.on("-b", "--binding=ip", String,
+ "Binds Rails to the specified ip.",
+ "Default: 127.0.0.1") { |OPTIONS[:ip]| }
+ opts.on("-i", "--index=controller", String,
+ "Specifies an index controller that requests for root will go to (instead of congratulations screen)."
+ ) { |OPTIONS[:index_controller]| }
+ opts.on("-e", "--environment=name", String,
+ "Specifies the environment to run this server under (test/development/production).",
+ "Default: development") { |OPTIONS[:environment]| }
+ opts.on("-d", "--daemon",
+ "Make Rails run as a Daemon (only works if fork is available -- meaning on *nix)."
+ ) { OPTIONS[:server_type] = WEBrick::Daemon }
+
+ opts.separator ""
+
+ opts.on("-h", "--help",
+ "Show this help message.") { puts opts; exit }
+
+ opts.parse!
+end
+
+ENV["RAILS_ENV"] = OPTIONS[:environment]
+require File.dirname(__FILE__) + "/../config/environment"
+require 'webrick_server'
+
+puts "=> Rails application started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}"
+DispatchServlet.dispatch(OPTIONS)
\ No newline at end of file
diff --git a/railties/dispatches/start_server b/railties/dispatches/start_server
new file mode 100644
index 0000000000..c6ecb4e4fe
--- /dev/null
+++ b/railties/dispatches/start_server
@@ -0,0 +1 @@
+ruby public/dispatch.servlet
\ No newline at end of file
diff --git a/railties/doc/README_FOR_APP b/railties/doc/README_FOR_APP
new file mode 100644
index 0000000000..ac6c149122
--- /dev/null
+++ b/railties/doc/README_FOR_APP
@@ -0,0 +1,2 @@
+Use this README file to introduce your application and point to useful places in the API for learning more.
+Run "rake appdoc" to generate API documentation for your models and controllers.
\ No newline at end of file
diff --git a/railties/doc/apache_protection b/railties/doc/apache_protection
new file mode 100644
index 0000000000..37676c2c63
--- /dev/null
+++ b/railties/doc/apache_protection
@@ -0,0 +1,3 @@
+Order Deny,Allow
+Deny from all
+Allow from 127.0.0.1
\ No newline at end of file
diff --git a/railties/doc/index.html b/railties/doc/index.html
new file mode 100644
index 0000000000..57e25b75fa
--- /dev/null
+++ b/railties/doc/index.html
@@ -0,0 +1,94 @@
+
+
+ Rails: Welcome on board
+
+
+
+
+
Congratulations, you're on Rails!
+
+
+ You've succesfully configured your web server to point at this Rails application.
+
+
+
Before you move on, verify that the following conditions have been met:
+
+
+
The log directory and the empty log files must be writable to the web server (chmod -R 777 log).
+
+ The shebang line in the public/dispatch* files must reference your Ruby installation.
+ You might need to change it to #!/usr/bin/env ruby or point directly at the installation.
+
+
+ Rails on Apache needs to have the cgi handler and mod_rewrite enabled.
+ Somewhere in your httpd.conf, you should have:
+ AddHandler cgi-script .cgi
+ LoadModule rewrite_module libexec/httpd/mod_rewrite.so
+ AddModule mod_rewrite.c
+
+
+
+
Take the following steps to get started:
+
+
+
Create empty production and test databases for your application.
+ Warning: Don't point your test database at your production database, it'll destroy the latter on test runs!
+
Edit config/database.yml with your database settings.
+
Create a new controller using the script/new_controller generator
+ Help: Run with no arguments for documentation
+
Create a new model using the script/new_model generator
+ Help: Run with no arguments for documentation
+
See all the tests run and fail by running rake.
+
Develop your Rails application!
+
Setup FastCGI or mod_ruby to get production-level performance
+
+
+
+ Having problems getting up and running? First try debugging it yourself by looking at the log files. Then try the friendly Rails
+ community on IRC (howto IRC). It's on FreeNET in channel #rubyonrails.
+
+
+
+
\ No newline at end of file
diff --git a/railties/environments/development.rb b/railties/environments/development.rb
new file mode 100644
index 0000000000..81d5a73403
--- /dev/null
+++ b/railties/environments/development.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.logger = ActionController::Base.logger = ActionMailer::Base.logger = Logger.new("#{RAILS_ROOT}/log/development.log")
+ActiveRecord::Base.establish_connection(:development)
\ No newline at end of file
diff --git a/railties/environments/production.rb b/railties/environments/production.rb
new file mode 100644
index 0000000000..1ecda598de
--- /dev/null
+++ b/railties/environments/production.rb
@@ -0,0 +1,6 @@
+ActiveRecord::Base.logger = ActionController::Base.logger = ActionMailer::Base.logger = Logger.new("#{RAILS_ROOT}/log/production.log")
+ActiveRecord::Base.establish_connection(:production)
+
+ActionController::Base.consider_all_requests_local = false
+ActionController::Base.reload_dependencies = false
+ActiveRecord::Base.reload_associations = false
\ No newline at end of file
diff --git a/railties/environments/shared.rb b/railties/environments/shared.rb
new file mode 100644
index 0000000000..f5d4771db6
--- /dev/null
+++ b/railties/environments/shared.rb
@@ -0,0 +1,35 @@
+RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + "/../")
+RAILS_ENV = ENV['RAILS_ENV'] || 'development'
+
+ADDITIONAL_LOAD_PATHS = [
+ "app/models",
+ "app/controllers",
+ "app/helpers",
+ "app",
+ "config",
+ "lib",
+ "vendor",
+ "vendor/railties",
+ "vendor/railties/lib",
+ "vendor/activerecord/lib",
+ "vendor/actionpack/lib",
+ "vendor/actionmailer/lib",
+]
+
+ADDITIONAL_LOAD_PATHS.unshift(Dir["#{RAILS_ROOT}/app/models/[a-z]*"].collect{ |dir| "app/models/#{File.basename(dir)}" })
+ADDITIONAL_LOAD_PATHS.unshift("test/mocks/#{RAILS_ENV}")
+
+ADDITIONAL_LOAD_PATHS.flatten.each { |dir| $: << "#{RAILS_ROOT}/#{dir}" }
+
+
+require 'active_record'
+require 'action_controller'
+require 'action_mailer'
+
+require 'yaml'
+
+ActionController::Base.template_root = ActionMailer::Base.template_root = "#{RAILS_ROOT}/app/views/"
+ActiveRecord::Base.configurations = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
+
+ActionController::Base.require_or_load 'abstract_application'
+ActionController::Base.require_or_load "environments/#{RAILS_ENV}"
\ No newline at end of file
diff --git a/railties/environments/shared_for_gem.rb b/railties/environments/shared_for_gem.rb
new file mode 100644
index 0000000000..1ae6746122
--- /dev/null
+++ b/railties/environments/shared_for_gem.rb
@@ -0,0 +1,23 @@
+RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + "/../")
+RAILS_ENV = ENV['RAILS_ENV'] || 'development'
+
+ADDITIONAL_LOAD_PATHS = [ "app/models", "app/controllers", "app/helpers", "config", "lib", "vendor" ]
+ADDITIONAL_LOAD_PATHS.unshift(Dir["#{RAILS_ROOT}/app/models/[a-z]*"].collect{ |dir| "app/models/#{File.basename(dir)}" })
+ADDITIONAL_LOAD_PATHS.unshift("test/mocks/#{RAILS_ENV}")
+
+ADDITIONAL_LOAD_PATHS.flatten.each { |dir| $:.unshift "#{RAILS_ROOT}/#{dir}" }
+
+require 'rubygems'
+
+require_gem 'activerecord'
+require_gem 'actionpack'
+require_gem 'actionmailer'
+require_gem 'rails'
+
+require 'yaml'
+
+ActionController::Base.template_root = ActionMailer::Base.template_root = "#{RAILS_ROOT}/app/views/"
+ActiveRecord::Base.configurations = YAML::load(File.open("#{RAILS_ROOT}/config/database.yml"))
+
+ActionController::Base.require_or_load 'abstract_application'
+ActionController::Base.require_or_load "environments/#{RAILS_ENV}"
diff --git a/railties/environments/test.rb b/railties/environments/test.rb
new file mode 100644
index 0000000000..6ab6a1f50a
--- /dev/null
+++ b/railties/environments/test.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.logger = ActionController::Base.logger = ActionMailer::Base.logger = Logger.new("#{RAILS_ROOT}/log/test.log")
+ActiveRecord::Base.establish_connection(:test)
diff --git a/railties/fresh_rakefile b/railties/fresh_rakefile
new file mode 100755
index 0000000000..3b746a49a5
--- /dev/null
+++ b/railties/fresh_rakefile
@@ -0,0 +1,104 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+$VERBOSE = nil
+
+require File.dirname(__FILE__) + '/config/environment'
+require 'code_statistics'
+
+desc "Run all the tests on a fresh test database"
+task :default => [ :clone_development_structure_to_test, :test_units, :test_functional ]
+
+desc "Generate API documentatio, show coding stats"
+task :doc => [ :appdoc, :stats ]
+
+
+desc "Run the unit tests in test/unit"
+Rake::TestTask.new("test_units") { |t|
+ t.libs << "test"
+ t.pattern = 'test/unit/*_test.rb'
+ t.verbose = true
+}
+
+desc "Run the functional tests in test/functional"
+Rake::TestTask.new("test_functional") { |t|
+ t.libs << "test"
+ t.pattern = 'test/functional/*_test.rb'
+ t.verbose = true
+}
+
+desc "Generate documentation for the application"
+Rake::RDocTask.new("appdoc") { |rdoc|
+ rdoc.rdoc_dir = 'doc/app'
+ rdoc.title = "Rails Application Documentation"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('doc/README_FOR_APP')
+ rdoc.rdoc_files.include('app/**/*.rb')
+}
+
+desc "Generate documentation for the Rails framework"
+Rake::RDocTask.new("apidoc") { |rdoc|
+ rdoc.rdoc_dir = 'doc/api'
+ rdoc.title = "Rails Framework Documentation"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('vendor/railties/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/railties/MIT-LICENSE')
+ rdoc.rdoc_files.include('vendor/activerecord/README')
+ rdoc.rdoc_files.include('vendor/activerecord/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/activerecord/lib/active_record/**/*.rb')
+ rdoc.rdoc_files.exclude('vendor/activerecord/lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('vendor/actionpack/README')
+ rdoc.rdoc_files.include('vendor/actionpack/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/actionpack/lib/action_controller/**/*.rb')
+ rdoc.rdoc_files.include('vendor/actionpack/lib/action_view/**/*.rb')
+ rdoc.rdoc_files.include('vendor/actionmailer/README')
+ rdoc.rdoc_files.include('vendor/actionmailer/CHANGELOG')
+ rdoc.rdoc_files.include('vendor/actionmailer/lib/action_mailer/base.rb')
+}
+
+desc "Report code statistics (KLOCs, etc) from the application"
+task :stats do
+ CodeStatistics.new(
+ ["Controllers", "app/controllers"],
+ ["Helpers", "app/helpers"],
+ ["Models", "app/models"],
+ ["Units", "test/unit"],
+ ["Functionals", "test/functional"]
+ ).to_s
+end
+
+desc "Recreate the test databases from the development structure"
+task :clone_development_structure_to_test => [ :db_structure_dump, :purge_test_database ] do
+ if ActiveRecord::Base.configurations["test"]["adapter"] == "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
+ IO.readlines("db/development_structure.sql").join.split("\n\n").each do |table|
+ ActiveRecord::Base.connection.execute(table)
+ end
+ elsif ActiveRecord::Base.configurations["test"]["adapter"] == "postgresql"
+ `psql -U #{ActiveRecord::Base.configurations["test"]["username"]} -f db/development_structure.sql #{ActiveRecord::Base.configurations["test"]["database"]}`
+ end
+end
+
+desc "Dump the database structure to a SQL file"
+task :db_structure_dump do
+ if ActiveRecord::Base.configurations["test"]["adapter"] == "mysql"
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["test"])
+ File.open("db/development_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ elsif ActiveRecord::Base.configurations["test"]["adapter"] == "postgresql"
+ `pg_dump -U #{ActiveRecord::Base.configurations["test"]["username"]} -s -f db/development_structure.sql #{ActiveRecord::Base.configurations["test"]["database"]}`
+ end
+end
+
+desc "Drop the test database and bring it back again"
+task :purge_test_database do
+ if ActiveRecord::Base.configurations["test"]["adapter"] == "mysql"
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["development"])
+ ActiveRecord::Base.connection.recreate_database(ActiveRecord::Base.configurations["test"]["database"])
+ elsif ActiveRecord::Base.configurations["test"]["adapter"] == "postgresql"
+ `dropdb -U #{ActiveRecord::Base.configurations["test"]["username"]} #{ActiveRecord::Base.configurations["test"]["database"]}`
+ `createdb -U #{ActiveRecord::Base.configurations["test"]["username"]} #{ActiveRecord::Base.configurations["test"]["database"]}`
+ end
+end
diff --git a/railties/generators/new_controller.rb b/railties/generators/new_controller.rb
new file mode 100755
index 0000000000..3060c06382
--- /dev/null
+++ b/railties/generators/new_controller.rb
@@ -0,0 +1,43 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+unless ARGV.empty?
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ actions = ARGV
+ Generator::Controller.new(rails_root, name, actions).generate
+else
+ puts <<-END_HELP
+
+NAME
+ new_controller - create controller and view stub files
+
+SYNOPSIS
+ new_controller ControllerName action [action ...]
+
+DESCRIPTION
+ The new_controller generator takes the name of the new controller as the
+ first argument and a variable number of view names as subsequent arguments.
+ The controller name should be supplied without a "Controller" suffix. The
+ generator will add that itself.
+
+ From the passed arguments, new_controller generates a controller file in
+ app/controllers with a render action for each of the view names passed.
+ It then creates a controller test suite in test/functional with one failing
+ test case. Finally, it creates an HTML stub for each of the view names in
+ app/views under a directory with the same name as the controller.
+
+EXAMPLE
+ new_controller Blog list display new edit
+
+ This will generate a BlogController class in
+ app/controllers/blog_controller.rb, a BlogHelper class in
+ app/helpers/blog_helper.rb and a BlogControllerTest in
+ test/functional/blog_controller_test.rb. It will also create list.rhtml,
+ display.rhtml, new.rhtml, and edit.rhtml in app/views/blog.
+
+ The BlogController class will have the following methods: list, display, new, edit.
+ Each will default to render the associated template file.
+END_HELP
+end
diff --git a/railties/generators/new_crud.rb b/railties/generators/new_crud.rb
new file mode 100755
index 0000000000..4eaa1cb1f3
--- /dev/null
+++ b/railties/generators/new_crud.rb
@@ -0,0 +1,34 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+unless ARGV.empty?
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ actions = ARGV
+ Generator::Model.new(rails_root, name).generate
+ Generator::Controller.new(rails_root, name, actions, :scaffold => true).generate
+else
+ puts <<-END_HELP
+
+NAME
+ new_crud - create a model and a controller scaffold
+
+SYNOPSIS
+ new_crud ModelName [action ...]
+
+DESCRIPTION
+ The new_crud generator takes the name of the new model as the
+ first argument and an optional list of controller actions as the
+ subsequent arguments. All actions may be omitted since the controller
+ will have scaffolding automatically set up for this model.
+
+EXAMPLE
+ new_crud Account
+
+ This will generate an Account model and controller with scaffolding.
+ Now create the accounts table in your database and browse to
+ http://localhost/account/ -- voila, you're on Rails!
+
+END_HELP
+end
diff --git a/railties/generators/new_mailer.rb b/railties/generators/new_mailer.rb
new file mode 100644
index 0000000000..05d0c9ae82
--- /dev/null
+++ b/railties/generators/new_mailer.rb
@@ -0,0 +1,43 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+unless ARGV.empty?
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ actions = ARGV
+ Generator::Mailer.new(rails_root, name, actions).generate
+else
+ puts <<-END_HELP
+
+NAME
+ new_mailer - create mailer and view stub files
+
+SYNOPSIS
+ new_mailer MailerName action [action ...]
+
+DESCRIPTION
+ The new_mailer generator takes the name of the new mailer class as the
+ first argument and a variable number of mail action names as subsequent
+ arguments.
+
+ From the passed arguments, new_mailer generates a class file in
+ app/models with a mail action for each of the mail action names passed.
+ It then creates a mail test suite in test/unit with one stub test case
+ and one stub fixture per mail action. Finally, it creates a template stub
+ for each of the mail action names in app/views under a directory with the
+ same name as the class.
+
+EXAMPLE
+ new_mailer Notifications signup forgot_password invoice
+
+ This will generate a Notifications class in
+ app/models/notifications.rb, a NotificationsTest in
+ test/unit/notifications_test.rb, and signup, forgot_password, and invoice
+ in test/fixture/notification. It will also create signup.rhtml,
+ forgot_password.rhtml, and invoice.rhtml in app/views/notifications.
+
+ The Notifications class will have the following methods: signup,
+ forgot_password, and invoice.
+END_HELP
+end
diff --git a/railties/generators/new_model.rb b/railties/generators/new_model.rb
new file mode 100755
index 0000000000..f6fbf5f002
--- /dev/null
+++ b/railties/generators/new_model.rb
@@ -0,0 +1,31 @@
+#!/usr/local/bin/ruby
+require File.dirname(__FILE__) + '/../config/environment'
+require 'generator'
+
+if ARGV.size == 1
+ rails_root = File.dirname(__FILE__) + '/..'
+ name = ARGV.shift
+ Generator::Model.new(rails_root, name).generate
+else
+ puts <<-HELP
+
+NAME
+ new_model - create model stub files
+
+SYNOPSIS
+ new_model ModelName
+
+DESCRIPTION
+ The new_model generator takes a model name (in CamelCase) and generates
+ a new, empty model in app/models, a test suite in test/unit with one
+ failing test case, and a fixtures directory in test/fixtures.
+
+EXAMPLE
+ new_model Account
+
+ This will generate an Account class in app/models/account.rb, an
+ AccountTest in test/unit/account_test.rb, and the directory
+ test/fixtures/account.
+
+HELP
+end
diff --git a/railties/generators/templates/controller.erb b/railties/generators/templates/controller.erb
new file mode 100644
index 0000000000..600f5d2c59
--- /dev/null
+++ b/railties/generators/templates/controller.erb
@@ -0,0 +1,19 @@
+class <%= class_name %>Controller < AbstractApplicationController
+ helper :<%= file_name %>
+<% if options[:scaffold] -%>
+ model :<%= file_name %>
+ scaffold :<%= options[:scaffold] %>
+
+ <%- for action in actions -%>
+ #def <%= action %>
+ #end
+
+ <%- end -%>
+<% else -%>
+ <%- for action in actions -%>
+ def <%= action %>
+ end
+
+ <%- end -%>
+<% end -%>
+end
diff --git a/railties/generators/templates/controller_test.erb b/railties/generators/templates/controller_test.erb
new file mode 100644
index 0000000000..5577379c62
--- /dev/null
+++ b/railties/generators/templates/controller_test.erb
@@ -0,0 +1,17 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>_controller'
+
+# Re-raise errors caught by the controller.
+class <%= class_name %>Controller; def rescue_action(e) raise e end; end
+
+class <%= class_name %>ControllerTest < Test::Unit::TestCase
+ def setup
+ @controller = <%= class_name %>Controller.new
+ @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
+ end
+
+ # Replace this with your real tests
+ def test_truth
+ assert true
+ end
+end
diff --git a/railties/generators/templates/controller_view.rhtml b/railties/generators/templates/controller_view.rhtml
new file mode 100644
index 0000000000..d8a310df50
--- /dev/null
+++ b/railties/generators/templates/controller_view.rhtml
@@ -0,0 +1,10 @@
+
+
+
+ <%= class_name %>#<%= action %>
+
+
+
<%= class_name %>#<%= action %>
+
Find me in app/views/<%= file_name %>/<%= action %>.rhtml
+
+
diff --git a/railties/generators/templates/helper.erb b/railties/generators/templates/helper.erb
new file mode 100644
index 0000000000..3fe2ecdc74
--- /dev/null
+++ b/railties/generators/templates/helper.erb
@@ -0,0 +1,2 @@
+module <%= class_name %>Helper
+end
diff --git a/railties/generators/templates/mailer.erb b/railties/generators/templates/mailer.erb
new file mode 100644
index 0000000000..5afc254923
--- /dev/null
+++ b/railties/generators/templates/mailer.erb
@@ -0,0 +1,15 @@
+require 'action_mailer'
+
+class <%= class_name %> < ActionMailer::Base
+
+<% for action in actions -%>
+ def <%= action %>(sent_on = Time.now)
+ @recipients = ''
+ @from = ''
+ @subject = ''
+ @body = {}
+ @sent_on = sent_on
+ end
+
+<% end -%>
+end
diff --git a/railties/generators/templates/mailer_action.rhtml b/railties/generators/templates/mailer_action.rhtml
new file mode 100644
index 0000000000..b481906829
--- /dev/null
+++ b/railties/generators/templates/mailer_action.rhtml
@@ -0,0 +1,3 @@
+<%= class_name %>#<%= action %>
+
+Find me in app/views/<%= file_name %>/<%= action %>.rhtml
diff --git a/railties/generators/templates/mailer_fixture.rhtml b/railties/generators/templates/mailer_fixture.rhtml
new file mode 100644
index 0000000000..f315d430ed
--- /dev/null
+++ b/railties/generators/templates/mailer_fixture.rhtml
@@ -0,0 +1,4 @@
+<%= class_name %>#<%= action %>
+
+Find me in test/fixtures/<%= file_name %>/<%= action %>.
+I'm tested against the view in app/views/<%= file_name %>/<%= action %>.
diff --git a/railties/generators/templates/mailer_test.erb b/railties/generators/templates/mailer_test.erb
new file mode 100644
index 0000000000..f17d614195
--- /dev/null
+++ b/railties/generators/templates/mailer_test.erb
@@ -0,0 +1,37 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+
+ def setup
+ ActionMailer::Base.delivery_method = :test
+ ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries = []
+
+ @expected = TMail::Mail.new
+ @expected.to = 'test@localhost'
+ @expected.from = 'test@localhost'
+ @expected.subject = '<%= class_name %> test mail'
+ end
+
+<% for action in actions -%>
+ def test_<%= action %>
+ @expected.body = read_fixture('<%= action %>')
+ @expected.date = Time.now
+
+ created = nil
+ assert_nothing_raised { created = <%= class_name %>.create_<%= action %>(@expected.date) }
+ assert_not_nil created
+ assert_equal @expected.encoded, created.encoded
+
+ assert_nothing_raised { <%= class_name %>.deliver_<%= action %>(@expected.date) }
+ assert_equal @expected.encoded, ActionMailer::Base.deliveries.first.encoded
+ end
+
+<% end -%>
+ private
+ def read_fixture(action)
+ IO.readlines("#{FIXTURES_PATH}/<%= file_name %>/#{action}")
+ end
+end
diff --git a/railties/generators/templates/model.erb b/railties/generators/templates/model.erb
new file mode 100644
index 0000000000..8d4c89e912
--- /dev/null
+++ b/railties/generators/templates/model.erb
@@ -0,0 +1,2 @@
+class <%= class_name %> < ActiveRecord::Base
+end
diff --git a/railties/generators/templates/model_test.erb b/railties/generators/templates/model_test.erb
new file mode 100644
index 0000000000..a3ad2b72fb
--- /dev/null
+++ b/railties/generators/templates/model_test.erb
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../test_helper'
+require '<%= file_name %>'
+
+class <%= class_name %>Test < Test::Unit::TestCase
+ fixtures :<%= table_name %>
+
+ # Replace this with your real tests
+ def test_truth
+ assert true
+ end
+end
\ No newline at end of file
diff --git a/railties/helpers/abstract_application.rb b/railties/helpers/abstract_application.rb
new file mode 100644
index 0000000000..fa26cd0399
--- /dev/null
+++ b/railties/helpers/abstract_application.rb
@@ -0,0 +1,5 @@
+# The filters added to this controller will be run for all controllers in the application.
+# Likewise will all the methods added be available for all controllers.
+class AbstractApplicationController < ActionController::Base
+ helper :application
+end
\ No newline at end of file
diff --git a/railties/helpers/application_helper.rb b/railties/helpers/application_helper.rb
new file mode 100644
index 0000000000..0392b53b46
--- /dev/null
+++ b/railties/helpers/application_helper.rb
@@ -0,0 +1,3 @@
+# The methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+end
diff --git a/railties/helpers/test_helper.rb b/railties/helpers/test_helper.rb
new file mode 100644
index 0000000000..d348f26517
--- /dev/null
+++ b/railties/helpers/test_helper.rb
@@ -0,0 +1,16 @@
+ENV["RAILS_ENV"] ||= "test"
+require File.dirname(__FILE__) + "/../config/environment"
+
+require 'test/unit'
+require 'active_record/fixtures'
+require 'action_controller/test_process'
+
+# Make rubygems available for testing if possible
+begin require('rubygems'); rescue LoadError; end
+begin require('dev-utils/debug'); rescue LoadError; end
+
+def create_fixtures(*table_names)
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_names)
+end
+
+Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
\ No newline at end of file
diff --git a/railties/html/404.html b/railties/html/404.html
new file mode 100644
index 0000000000..edbc89bf99
--- /dev/null
+++ b/railties/html/404.html
@@ -0,0 +1,6 @@
+
+
+
File not found
+
Change this error message for pages not found in public/404.html
+
+
\ No newline at end of file
diff --git a/railties/html/500.html b/railties/html/500.html
new file mode 100644
index 0000000000..ee0c919c4a
--- /dev/null
+++ b/railties/html/500.html
@@ -0,0 +1,6 @@
+
+
+
Application error (Apache)
+
Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html
+
+
\ No newline at end of file
diff --git a/railties/html/index.html b/railties/html/index.html
new file mode 100644
index 0000000000..4949c64a5a
--- /dev/null
+++ b/railties/html/index.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/railties/lib/code_statistics.rb b/railties/lib/code_statistics.rb
new file mode 100644
index 0000000000..53e7feb1c0
--- /dev/null
+++ b/railties/lib/code_statistics.rb
@@ -0,0 +1,71 @@
+class CodeStatistics
+ def initialize(*pairs)
+ @pairs = pairs
+ @statistics = calculate_statistics
+ @total = calculate_total if pairs.length > 1
+ end
+
+ def to_s
+ print_header
+ @statistics.each{ |k, v| print_line(k, v) }
+ print_splitter
+
+ if @total
+ print_line("Total", @total)
+ print_splitter
+ end
+ end
+
+ private
+ def calculate_statistics
+ @pairs.inject({}) { |stats, pair| stats[pair.first] = calculate_directory_statistics(pair.last); stats }
+ end
+
+ def calculate_directory_statistics(directory, pattern = /.*rb/)
+ stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 }
+
+ Dir.foreach(directory) do |file_name|
+ next unless file_name =~ pattern
+
+ f = File.open(directory + "/" + file_name)
+
+ while line = f.gets
+ stats["lines"] += 1
+ stats["classes"] += 1 if line =~ /class [A-Z]/
+ stats["methods"] += 1 if line =~ /def [a-z]/
+ stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ /^\s*#/
+ end
+ end
+
+ stats
+ end
+
+ def calculate_total
+ total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 }
+ @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } }
+ total
+ end
+
+ def print_header
+ print_splitter
+ puts "| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |"
+ print_splitter
+ end
+
+ def print_splitter
+ puts "+----------------------+-------+-------+---------+---------+-----+-------+"
+ end
+
+ def print_line(name, statistics)
+ m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0
+ loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0
+
+ puts "| #{name.ljust(20)} " +
+ "| #{statistics["lines"].to_s.rjust(5)} " +
+ "| #{statistics["codelines"].to_s.rjust(5)} " +
+ "| #{statistics["classes"].to_s.rjust(7)} " +
+ "| #{statistics["methods"].to_s.rjust(7)} " +
+ "| #{m_over_c.to_s.rjust(3)} " +
+ "| #{loc_over_m.to_s.rjust(5)} |"
+ end
+end
\ No newline at end of file
diff --git a/railties/lib/dispatcher.rb b/railties/lib/dispatcher.rb
new file mode 100644
index 0000000000..aa7ae98edd
--- /dev/null
+++ b/railties/lib/dispatcher.rb
@@ -0,0 +1,55 @@
+#--
+# 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.
+#++
+
+class Dispatcher
+ DEFAULT_SESSION_OPTIONS = { "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" }
+
+ def self.dispatch(cgi = CGI.new, session_options = DEFAULT_SESSION_OPTIONS, error_page = nil)
+ begin
+ request = ActionController::CgiRequest.new(cgi, session_options)
+ response = ActionController::CgiResponse.new(cgi)
+
+ controller_name = request.parameters["controller"].gsub(/[^_a-zA-Z0-9]/, "").untaint
+
+ if module_name = request.parameters["module"]
+ Module.new do
+ ActionController::Base.require_or_load "#{module_name}/#{Inflector.underscore(controller_name)}_controller"
+ Object.const_get("#{Inflector.camelize(controller_name)}Controller").process(request, response).out
+ end
+ else
+ ActionController::Base.require_or_load "#{Inflector.underscore(controller_name)}_controller"
+ Object.const_get("#{Inflector.camelize(controller_name)}Controller").process(request, response).out
+ end
+ rescue Exception => e
+ begin
+ ActionController::Base.logger.info "\n\nException throw during dispatch: #{e.message}\n#{e.backtrace.join("\n")}"
+ rescue Exception
+ # Couldn't log error
+ end
+
+ if error_page then cgi.out{ IO.readlines(error_page) } else raise e end
+ ensure
+ ActiveRecord::Base.reset_associations_loaded
+ end
+ end
+end
diff --git a/railties/lib/generator.rb b/railties/lib/generator.rb
new file mode 100644
index 0000000000..28b41c60f0
--- /dev/null
+++ b/railties/lib/generator.rb
@@ -0,0 +1,112 @@
+require 'fileutils'
+require 'active_record/support/inflector'
+
+module Generator
+ class GeneratorError < StandardError; end
+
+ class Base
+ @@template_root = File.dirname(__FILE__) + '/../generators/templates'
+ cattr_accessor :template_root
+
+ attr_reader :rails_root, :class_name, :file_name, :table_name,
+ :actions, :options
+
+ def initialize(rails_root, object_name, actions = [], options = {})
+ @rails_root = rails_root
+ @class_name = Inflector.camelize(object_name)
+ @file_name = Inflector.underscore(@class_name)
+ @table_name = Inflector.pluralize(@file_name)
+ @actions = actions
+ @options = options
+
+ # Use local templates if rails_root/generators directory exists.
+ local_template_root = File.join(@rails_root, 'generators')
+ if File.directory?(local_template_root)
+ self.class.template_root = local_template_root
+ end
+ end
+
+ protected
+
+ # Generate a file in a fresh Rails app from an ERB template.
+ # Takes a template path relative to +template_root+, a
+ # destination path relative to +rails_root+, evaluates the template,
+ # and writes the result to the destination.
+ def generate_file(template_file_path, rails_file_path, eval_binding = nil)
+ # Determine full paths for source and destination files.
+ template_path = File.join(template_root, template_file_path)
+ rails_path = File.join(rails_root, rails_file_path)
+
+ # Create destination directories.
+ FileUtils.mkdir_p(File.dirname(rails_path))
+
+ # Render template and write result.
+ eval_binding ||= binding
+ contents = ERB.new(File.read(template_path), nil, '-').result(eval_binding)
+ File.open(rails_path, 'w') { |file| file.write(contents) }
+ end
+ end
+
+ # Generate controller, helper, functional test, and views.
+ class Controller < Base
+ def generate
+ options[:scaffold] = file_name if options[:scaffold]
+
+ # Controller class.
+ generate_file "controller.erb", "app/controllers/#{file_name}_controller.rb"
+
+ # Helper class.
+ generate_file "helper.erb", "app/helpers/#{file_name}_helper.rb"
+
+ # Function test.
+ generate_file "controller_test.erb", "test/functional/#{file_name}_controller_test.rb"
+
+ # View template for each action.
+ @actions.each do |action|
+ generate_file "controller_view.rhtml",
+ "app/views/#{file_name}/#{action}.rhtml",
+ binding
+ end
+ end
+ end
+
+ # Generate model, unit test, and fixtures.
+ class Model < Base
+ def generate
+
+ # Model class.
+ generate_file "model.erb", "app/models/#{file_name}.rb"
+
+ # Model unit test.
+ generate_file "model_test.erb", "test/unit/#{file_name}_test.rb"
+
+ # Test fixtures directory.
+ FileUtils.mkdir_p("test/fixtures/#{table_name}")
+ end
+ end
+
+ # Generate mailer, helper, functional test, and views.
+ class Mailer < Base
+ def generate
+
+ # Mailer class.
+ generate_file "mailer.erb", "app/models/#{file_name}.rb"
+
+ # Mailer unit test.
+ generate_file "mailer_test.erb", "test/unit/#{file_name}_test.rb"
+
+ # Test fixtures directory.
+ FileUtils.mkdir_p("test/fixtures/#{table_name}")
+
+ # View template and fixture for each action.
+ @actions.each do |action|
+ generate_file "mailer_action.rhtml",
+ "app/views/#{file_name}/#{action}.rhtml",
+ binding
+ generate_file "mailer_fixture.rhtml",
+ "test/fixtures/#{table_name}/#{action}",
+ binding
+ end
+ end
+ end
+end
diff --git a/railties/lib/webrick_server.rb b/railties/lib/webrick_server.rb
new file mode 100644
index 0000000000..66c78fbd5f
--- /dev/null
+++ b/railties/lib/webrick_server.rb
@@ -0,0 +1,159 @@
+# Donated by Florian Gross
+
+require 'webrick'
+require 'cgi'
+require 'stringio'
+
+begin
+ require 'dev-utils/debug'
+ require 'irb/completion'
+
+ module DevUtils::Debug
+ alias_method :breakpoint_without_io, :breakpoint unless method_defined?(:breakpoint_without_io)
+
+ def breakpoint(name = nil, context = nil, &block)
+ $new_stdin, $new_stdout = $stdin, $stdout
+ $stdin, $stdout = $old_stdin, $old_stdout
+ breakpoint_without_io(name, context, &block)
+ $stdin, $stdout = $new_stdin, $new_stdout
+ end
+ end
+rescue LoadError
+ # dev utils not available
+end
+
+include WEBrick
+
+class DispatchServlet < WEBrick::HTTPServlet::AbstractServlet
+ REQUEST_MUTEX = Mutex.new
+
+ def self.dispatch(options = {})
+ Socket.do_not_reverse_lookup = true # patch for OS X
+
+ server = WEBrick::HTTPServer.new(:Port => options[:port].to_i, :ServerType => options[:server_type], :BindAddress => options[:ip])
+ server.mount('/', DispatchServlet, options)
+
+ trap("INT") { server.shutdown }
+ server.start
+ end
+
+ def initialize(server, options)
+ @server_options = options
+ @file_handler = WEBrick::HTTPServlet::FileHandler.new(server, options[:server_root], {:FancyIndexing => true })
+ super
+ end
+
+ def do_GET(req, res)
+ begin
+ REQUEST_MUTEX.lock
+
+ unless handle_index(req, res)
+ unless handle_dispatch(req, res)
+ unless handle_file(req, res)
+ unless handle_mapped(req, res)
+ raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
+ end
+ end
+ end
+ end
+ ensure
+ REQUEST_MUTEX.unlock
+ end
+ end
+
+ alias :do_POST :do_GET
+
+ def handle_index(req, res)
+ if req.request_uri.path == "/"
+ if @server_options[:index_controller]
+ res.set_redirect WEBrick::HTTPStatus::MovedPermanently, "/#{@server_options[:index_controller]}/"
+ else
+ res.set_redirect WEBrick::HTTPStatus::MovedPermanently, "/_doc/index.html"
+ end
+
+ return true
+ else
+ return false
+ end
+ end
+
+ def handle_file(req, res)
+ begin
+ @file_handler.send(:do_GET, req, res)
+ return true
+ rescue HTTPStatus::PartialContent, HTTPStatus::NotModified => err
+ res.set_error(err)
+ return true
+ rescue => err
+ p err
+ return false
+ end
+ end
+
+ def handle_mapped(req, res)
+ parsed_ok, controller, action, id = DispatchServlet.parse_uri(req.request_uri.path)
+ if parsed_ok
+ query = "controller=#{controller}&action=#{action}&id=#{id}"
+ query << "{req.request_uri.query}" if req.request_uri.query
+ origin = req.request_uri.path + "?" + query
+ req.request_uri.path = "/dispatch.rb"
+ req.request_uri.query = query
+ handle_dispatch(req, res, origin)
+ else
+ return false
+ end
+ end
+
+ def handle_dispatch(req, res, origin = nil)
+ return false unless /^\/dispatch\.(?:cgi|rb|fcgi)$/.match(req.request_uri.path)
+
+ env = req.meta_vars.clone
+ env["QUERY_STRING"] = req.request_uri.query
+ env["REQUEST_URI"] = origin if origin
+
+ data = nil
+ $old_stdin, $old_stdout = $stdin, $stdout
+ $stdin, $stdout = StringIO.new(req.body || ""), StringIO.new
+
+ begin
+ require 'cgi'
+ CGI.send(:define_method, :env_table) { env }
+
+ load File.join(@server_options[:server_root], "dispatch.rb")
+
+ $stdout.rewind
+ data = $stdout.read
+ ensure
+ $stdin, $stdout = $old_stdin, $old_stdout
+ end
+
+ raw_header, body = *data.split(/^[\xd\xa]+/on, 2)
+ header = WEBrick::HTTPUtils::parse_header(raw_header)
+ if /^(\d+)/ =~ header['status'][0]
+ res.status = $1.to_i
+ header.delete('status')
+ end
+ header.each { |key, val| res[key] = val.join(", ") }
+
+ res.body = body
+ return true
+ rescue => err
+ p err, err.backtrace
+ return false
+ end
+
+ def self.parse_uri(path)
+ component = /([-_a-zA-Z0-9]+)/
+
+ case path.sub(%r{^/(?:fcgi|mruby|cgi)/}, "/")
+ when %r{^/#{component}/?$} then
+ [true, $1, "index", nil]
+ when %r{^/#{component}/#{component}/?$} then
+ [true, $1, $2, nil]
+ when %r{^/#{component}/#{component}/#{component}/?$} then
+ [true, $1, $2, $3]
+ else
+ [false, nil, nil, nil]
+ end
+ end
+end
diff --git a/railties/test/webrick_dispatcher_test.rb b/railties/test/webrick_dispatcher_test.rb
new file mode 100644
index 0000000000..2c6b51ae62
--- /dev/null
+++ b/railties/test/webrick_dispatcher_test.rb
@@ -0,0 +1,30 @@
+#!/bin/env ruby
+
+$:.unshift(File.dirname(__FILE__) + "/../lib")
+
+require 'test/unit'
+require 'webrick_server'
+
+class ParseUriTest < Test::Unit::TestCase
+
+ def test_parse_uri_old_behavior
+ assert_equal [true, 'forum', 'index', '1'], DispatchServlet.parse_uri('/forum/index/1')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum/index')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum/')
+ end
+
+ def test_parse_uri_new_behavior
+ assert_equal [true, 'forum', 'index', '1'], DispatchServlet.parse_uri('/forum/index/1/')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum/index/')
+ assert_equal [true, 'forum', 'index', nil], DispatchServlet.parse_uri('/forum')
+ end
+
+ def test_parse_uri_failures
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('/')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('a')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('/forum//')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('/+forum/')
+ assert_equal [false, nil, nil, nil], DispatchServlet.parse_uri('forum/')
+ end
+
+end
--
cgit v1.2.3