git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
David Heinemeier Hansson
2004-11-24 01:04:44 +00:00
commit db045dbbf6
296 changed files with 30881 additions and 0 deletions

757
activerecord/CHANGELOG Normal file
View File

@@ -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]

20
activerecord/MIT-LICENSE Normal file
View File

@@ -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.

361
activerecord/README Executable file
View File

@@ -0,0 +1,361 @@
= Active Record -- Object-relation mapping put on rails
Active Record connects business objects and database tables to create a persistable
domain model where logic and data is presented in one wrapping. It's an implementation
of the object-relational mapping (ORM) pattern[http://www.martinfowler.com/eaaCatalog/activeRecord.html]
by the same name as described by Martin Fowler:
"An object that wraps a row in a database table or view, encapsulates
the database access, and adds domain logic on that data."
Active Records main contribution to the pattern is to relieve the original of two stunting problems:
lack of associations and inheritance. By adding a simple domain language-like set of macros to describe
the former and integrating the Single Table Inheritance pattern for the latter, Active Record narrows the
gap of functionality between the data mapper and active record approach.
A short rundown of the major features:
* Automated mapping between classes and tables, attributes and columns.
class Product < ActiveRecord::Base; end
...is automatically mapped to the table named "products", such as:
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
...which again gives Product#name and Product#name=(new_name)
Learn more in link:classes/ActiveRecord/Base.html
* Associations between objects controlled by simple meta-programming macros.
class Firm < ActiveRecord::Base
has_many :clients
has_one :account
belongs_to :conglomorate
end
Learn more in link:classes/ActiveRecord/Associations/ClassMethods.html
* Aggregations of value objects controlled by simple meta-programming macros.
class Account < ActiveRecord::Base
composed_of :balance, :class_name => "Money",
:mapping => %w(balance amount)
composed_of :address,
:mapping => [%w(address_street street), %w(address_city city)]
end
Learn more in link:classes/ActiveRecord/Aggregations/ClassMethods.html
* Validation rules that can differ for new or existing objects.
class Post < ActiveRecord::Base
def validate # validates on both creates and updates
errors.add_on_empty "title"
end
def validate_on_update
errors.add_on_empty "password"
end
end
Learn more in link:classes/ActiveRecord/Validations.html
* Callbacks as methods or queues on the entire lifecycle (instantiation, saving, destroying, validating, etc).
class Person < ActiveRecord::Base
def before_destroy # is called just before Person#destroy
CreditCard.find(credit_card_id).destroy
end
end
class Account < ActiveRecord::Base
after_find :eager_load, 'self.class.announce(#{id})'
end
Learn more in link:classes/ActiveRecord/Callbacks.html
* Observers for the entire lifecycle
class CommentObserver < ActiveRecord::Observer
def after_create(comment) # is called just after Comment#save
NotificationService.send_email("david@loudthinking.com", comment)
end
end
Learn more in link:classes/ActiveRecord/Observer.html
* Inheritance hierarchies
class Company < ActiveRecord::Base; end
class Firm < Company; end
class Client < Company; end
class PriorityClient < Client; end
Learn more in link:classes/ActiveRecord/Base.html
* Transaction support on both a database and object level. The latter is implemented
by using Transaction::Simple[http://www.halostatue.ca/ruby/Transaction__Simple.html]
# Just database transaction
Account.transaction do
david.withdrawal(100)
mary.deposit(100)
end
# Database and object transaction
Account.transaction(david, mary) do
david.withdrawal(100)
mary.deposit(100)
end
Learn more in link:classes/ActiveRecord/Transactions/ClassMethods.html
* Reflections on columns, associations, and aggregations
reflection = Firm.reflect_on_association(:clients)
reflection.klass # => Client (class)
Firm.columns # Returns an array of column descriptors for the firms table
Learn more in link:classes/ActiveRecord/Reflection/ClassMethods.html
* Direct manipulation (instead of service invocation)
So instead of (Hibernate[http://www.hibernate.org/] example):
long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
// something interesting involving a cat...
sess.save(cat);
sess.flush(); // force the SQL INSERT
Active Record lets you:
pkId = 1234
cat = Cat.find(pkId)
# something even more interesting involving a the same cat...
cat.save
Learn more in link:classes/ActiveRecord/Base.html
* Database abstraction through simple adapters (~100 lines) with a shared connector
ActiveRecord::Base.establish_connection(:adapter => "sqlite", :dbfile => "dbfile")
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "me",
:password => "secret",
:database => "activerecord"
)
Learn more in link:classes/ActiveRecord/Base.html#M000081
* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc]
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
== Simple example (1/2): Defining tables and classes (using MySQL)
Data definitions are specified only in the database. Active Record queries the database for
the column names (that then serves to determine which attributes are valid) on regular
objects instantiation through the new constructor and relies on the column names in the rows
with the finders.
# CREATE TABLE companies (
# id int(11) unsigned NOT NULL auto_increment,
# client_of int(11),
# name varchar(255),
# type varchar(100),
# PRIMARY KEY (id)
# )
Active Record automatically links the "Company" object to the "companies" table
class Company < ActiveRecord::Base
has_many :people, :class_name => "Person"
end
class Firm < Company
has_many :clients
def people_with_all_clients
clients.inject([]) { |people, client| people + client.people }
end
end
The foreign_key is only necessary because we didn't use "firm_id" in the data definition
class Client < Company
belongs_to :firm, :foreign_key => "client_of"
end
# CREATE TABLE people (
# id int(11) unsigned NOT NULL auto_increment,
# name text,
# company_id text,
# PRIMARY KEY (id)
# )
Active Record will also automatically link the "Person" object to the "people" table
class Person < ActiveRecord::Base
belongs_to :company
end
== Simple example (2/2): Using the domain
Picking a database connection for all the active records
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:host => "localhost",
:username => "me",
:password => "secret",
:database => "activerecord"
)
Create some fixtures
firm = Firm.new("name" => "Next Angle")
# SQL: INSERT INTO companies (name, type) VALUES("Next Angle", "Firm")
firm.save
client = Client.new("name" => "37signals", "client_of" => firm.id)
# SQL: INSERT INTO companies (name, client_of, type) VALUES("37signals", 1, "Firm")
client.save
Lots of different finders
# SQL: SELECT * FROM companies WHERE id = 1
next_angle = Company.find(1)
# SQL: SELECT * FROM companies WHERE id = 1 AND type = 'Firm'
next_angle = Firm.find(1)
# SQL: SELECT * FROM companies WHERE id = 1 AND name = 'Next Angle'
next_angle = Company.find_first "name = 'Next Angle'"
next_angle = Firm.find_by_sql("SELECT * FROM companies WHERE id = 1").first
The supertype, Company, will return subtype instances
Firm === next_angle
All the dynamic methods added by the has_many macro
next_angle.clients.empty? # true
next_angle.clients.size # total number of clients
all_clients = next_angle.clients
Constrained finds makes access security easier when ID comes from a web-app
# SQL: SELECT * FROM companies WHERE client_of = 1 AND type = 'Client' AND id = 2
thirty_seven_signals = next_angle.clients.find(2)
Bi-directional associations thanks to the "belongs_to" macro
thirty_seven_signals.firm.nil? # true
== Examples
Active Record ships with a couple of examples that should give you a good feel for
operating usage. Be sure to edit the <tt>examples/shared_setup.rb</tt> file for your
own database before running the examples. Possibly also the table definition SQL in
the examples themselves.
It's also highly recommended to have a look at the unit tests. Read more in link:files/RUNNING_UNIT_TESTS.html
== Database support
Active Record ships with adapters for MySQL/Ruby[http://www.tmtm.org/en/mysql/ruby/]
(compatible with Ruby/MySQL[http://www.tmtm.org/ruby/mysql/README_en.html]),
PostgreSQL[http://www.postgresql.jp/interfaces/ruby/], and
SQLite[http://rubyforge.org/projects/sqlite-ruby/] (needs SQLite 2.8.13+ and SQLite-Ruby 1.1.2+).
The adapters are around 100 lines of code fulfilling the interface specified by
ActiveRecord::ConnectionAdapters::AbstractAdapter. Writing a new adapter should be a small task --
especially considering the extensive test suite that'll make sure you're fulfilling the contract.
== Philosophy
Active Record attempts to provide a coherent wrapping for the inconvenience that is
object-relational mapping. The prime directive for this mapping has been to minimize
the amount of code needed to built a real-world domain model. This is made possible
by relying on a number of conventions that make it easy for Active Record to infer
complex relations and structures from a minimal amount of explicit direction.
Convention over Configuration:
* No XML-files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word
Admit the Database:
* Lets you drop down to SQL for odd cases and performance
* Doesn't attempt to duplicate or replace data definitions
== Download
The latest version of Active Record can be found at
* http://rubyforge.org/project/showfiles.php?group_id=182
Documentation can be found at
* http://ar.rubyonrails.org
== Installation
The prefered method of installing Active Record is through its GEM file. You'll need to have
RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl] installed for that, though. If you have,
then use:
% [sudo] gem install activerecord-0.9.0.gem
You can also install Active Record the old-fashion way with the following command:
% [sudo] ruby install.rb
from its distribution directory.
== License
Active Record is released under the same license as Ruby.
== Support
The Active Record homepage is http://activerecord.rubyonrails.org. You can find the Active Record
RubyForge page at http://rubyforge.org/projects/activerecord. And as Jim from Rake says:
Feel free to submit commits or feature requests. If you send a patch,
remember to update the corresponding unit tests. If fact, I prefer
new feature to be submitted in the form of new unit tests.
For other information, feel free to ask on the ruby-talk mailing list
(which is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.

View File

@@ -0,0 +1,36 @@
== Creating the test database
The default names for the test databases are "activerecord_unittest" and
"activerecord_unittest2". If you want to use another database name then be sure
to update the connection adapter setups you want to test with in
test/connections/<your database>/connection.rb.
When you have the database online, you can import the fixture tables with
the test/fixtures/db_definitions/*.sql files.
Make sure that you create database objects with the same user that you specified in i
connection.rb otherwise (on Postgres, at least) tests for default values will fail
(see http://dev.rubyonrails.org/trac.cgi/ticket/118)
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all the adapters. You can also run the suite on just
one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
Rake can be found at http://rake.rubyforge.org
== Running by hand
Unit tests are located in test directory. If you only want to run a single test suite,
or don't want to bother with Rake, you can do so with something like:
cd test; ruby -I "connections/native_mysql" base_test.rb
That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
and test suite name as needed.
You can also run all the suites on a specific adapter with:
cd test; all.sh "connections/native_mysql"

126
activerecord/Rakefile Executable file
View File

@@ -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

View File

@@ -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}"

View File

@@ -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}"

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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

60
activerecord/install.rb Normal file
View File

@@ -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)
}

View File

@@ -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'

View File

@@ -0,0 +1,165 @@
module ActiveRecord
module Aggregations # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
# as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
# composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
# and how it can be turned back into attributes (when the entity is saved to the database). Example:
#
# class Customer < ActiveRecord::Base
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
# end
#
# The customer class now has the following methods to manipulate the value objects:
# * <tt>Customer#balance, Customer#balance=(money)</tt>
# * <tt>Customer#address, Customer#address=(address)</tt>
#
# These methods will operate with value objects like the ones described below:
#
# class Money
# include Comparable
# attr_reader :amount, :currency
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
#
# def initialize(amount, currency = "USD")
# @amount, @currency = amount, currency
# end
#
# def exchange_to(other_currency)
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
# Money.new(exchanged_amount, other_currency)
# end
#
# def ==(other_money)
# amount == other_money.amount && currency == other_money.currency
# end
#
# def <=>(other_money)
# if currency == other_money.currency
# amount <=> amount
# else
# amount <=> other_money.exchange_to(currency).amount
# end
# end
# end
#
# class Address
# attr_reader :street, :city
# def initialize(street, city)
# @street, @city = street, city
# end
#
# def close_to?(other_address)
# city == other_address.city
# end
#
# def ==(other_address)
# city == other_address.city && street == other_address.street
# end
# end
#
# Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
# composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
# +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
# customer.balance # => Money value object
# customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
# customer.balance > Money.new(10) # => true
# customer.balance == Money.new(20) # => true
# customer.balance < Money.new(5) # => false
#
# Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
# determine the order of the parameters. Example:
#
# customer.address_street = "Hyancintvej"
# customer.address_city = "Copenhagen"
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
# customer.address = Address.new("May Street", "Chicago")
# customer.address_street # => "May Street"
# customer.address_city # => "Chicago"
#
# == Writing value objects
#
# Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
# $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
# sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
# easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
# creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
# changed through other means than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
# change it afterwards will result in a TypeError.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
# immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
module ClassMethods
# Adds the a reader and writer method for manipulating a value object, so
# <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
# if the real class name is +CompanyAddress+, you'll have to specify it with this option.
# * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
# to a constructor parameter on the value class.
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
def composed_of(part_id, options = {})
validate_options([ :class_name, :mapping ], options.keys)
name = part_id.id2name
class_name = options[:class_name] || name_to_class_name(name)
mapping = options[:mapping]
reader_method(name, class_name, mapping)
writer_method(name, class_name, mapping)
end
private
# Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
def validate_options(valid_option_keys, supplied_option_keys)
unknown_option_keys = supplied_option_keys - valid_option_keys
raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
end
def name_to_class_name(name)
name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
end
def reader_method(name, class_name, mapping)
module_eval <<-end_eval
def #{name}(force_reload = false)
if @#{name}.nil? || force_reload
@#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
end
return @#{name}
end
end_eval
end
def writer_method(name, class_name, mapping)
module_eval <<-end_eval
def #{name}=(part)
@#{name} = part.freeze
#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
end
end_eval
end
end
end
end

View File

@@ -0,0 +1,576 @@
require 'active_record/associations/association_collection'
require 'active_record/associations/has_many_association'
require 'active_record/associations/has_and_belongs_to_many_association'
require 'active_record/deprecated_associations'
module ActiveRecord
module Associations # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
# specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
# methods. Example:
#
# class Project < ActiveRecord::Base
# belongs_to :portfolio
# has_one :project_manager
# has_many :milestones
# has_and_belongs_to_many :categories
# end
#
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
# <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
# <tt>Project#milestones.build, Project#milestones.create</tt>
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1)</tt>
#
# == Example
#
# link:../examples/associations.png
#
# == Is it belongs_to or has_one?
#
# Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
# saying belongs_to. Example:
#
# class Post < ActiveRecord::Base
# has_one :author
# end
#
# class Author < ActiveRecord::Base
# belongs_to :post
# end
#
# The tables for these classes could look something like:
#
# CREATE TABLE posts (
# id int(11) NOT NULL auto_increment,
# title varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE authors (
# id int(11) NOT NULL auto_increment,
# post_id int(11) default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# == Caching
#
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
# instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
# worrying too much about performance at the first go. Example:
#
# project.milestones # fetches milestones from the database
# project.milestones.size # uses the milestone cache
# project.milestones.empty? # uses the milestone cache
# project.milestones(true).size # fetches milestones from the database
# project.milestones # uses the milestone cache
#
# == Modules
#
# By default, associations will look for objects within the current module scope. Consider:
#
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base
# has_many :clients
# end
#
# class Company < ActiveRecord::Base; end
# end
# end
#
# When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
# with a class in another module scope this can be done by specifying the complete class name, such as:
#
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base; end
# end
#
# module Billing
# class Account < ActiveRecord::Base
# belongs_to :firm, :class_name => "MyApplication::Business::Firm"
# end
# end
# end
#
# == Type safety with ActiveRecord::AssociationTypeMismatch
#
# If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
# get a ActiveRecord::AssociationTypeMismatch.
#
# == Options
#
# All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
# possible.
module ClassMethods
# Adds the following methods for retrival and query of collections of associated objects.
# +collection+ is replaced with the symbol passed as the first argument, so
# <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
# An empty array is returned if none are found.
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL. This does not destroy the objects.
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
# * <tt>collection.size</tt> - returns the number of associated objects.
# * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
# * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
# criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
# * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
# * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
#
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
# * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
# from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
# if the real class name is +SpecialProduct+, you'll have to specify it with this option.
# * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
# sql fragment, such as "price > 5 AND name LIKE 'B%'".
# * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
# such as "last_name, first_name DESC"
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
# as the default foreign_key.
# * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object.
# May not be set if :exclusively_dependent is also set.
# * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
# before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
# clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
# May not be set if :dependent is also set.
# * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
# associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
#
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
# has_many :tracks, :order => "position", :dependent => true
# has_many :subscribers, :class_name => "Person", :finder_sql =>
# 'SELECT DISTINCT people.* ' +
# 'FROM people p, post_subscriptions ps ' +
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
# 'ORDER BY p.first_name'
def has_many(association_id, options = {})
validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
association_name, association_class_name, association_class_primary_key_name =
associate_identification(association_id, options[:class_name], options[:foreign_key])
require_association_class(association_class_name)
if options[:dependent] and options[:exclusively_dependent]
raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode
elsif options[:dependent]
module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
elsif options[:exclusively_dependent]
module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
end
define_method(association_name) do |*params|
force_reload = params.first unless params.empty?
association = instance_variable_get("@#{association_name}")
if association.nil?
association = HasManyAssociation.new(self,
association_name, association_class_name,
association_class_primary_key_name, options)
instance_variable_set("@#{association_name}", association)
end
association.reload if force_reload
association
end
# deprecated api
deprecated_collection_count_method(association_name)
deprecated_add_association_relation(association_name)
deprecated_remove_association_relation(association_name)
deprecated_has_collection_method(association_name)
deprecated_find_in_collection_method(association_name)
deprecated_find_all_in_collection_method(association_name)
deprecated_create_method(association_name)
deprecated_build_method(association_name)
end
# Adds the following methods for retrival and query of a single associated object.
# +association+ is replaced with the symbol passed as the first argument, so
# <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
# same id as the associated object.
# * <tt>association.nil?</tt> - returns true if there is no associated object.
# * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key but has not yet been saved.
# * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
#
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
# * <tt>Account#beneficiary.nil?</tt>
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
# if the real class name is +Person+, you'll have to specify it with this option.
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
# sql fragment, such as "rank = 5".
# * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
# an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
# * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
# as the default foreign_key.
#
# Option examples:
# has_one :credit_card, :dependent => true
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
def has_one(association_id, options = {})
options.merge!({ :remote => true })
belongs_to(association_id, options)
association_name, association_class_name, class_primary_key_name =
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
require_association_class(association_class_name)
has_one_writer_method(association_name, association_class_name, class_primary_key_name)
build_method("build_", association_name, association_class_name, class_primary_key_name)
create_method("create_", association_name, association_class_name, class_primary_key_name)
module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
end
# Adds the following methods for retrival and query for a single associated object that this object holds an id to.
# +association+ is replaced with the symbol passed as the first argument, so
# <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
# * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
# same id as the associated object.
# * <tt>association.nil?</tt> - returns true if there is no associated object.
#
# Example: An Post class declares <tt>has_one :author</tt>, which will add:
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
# * <tt>Post#author.nil?</tt>
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
# from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
# if the real class name is +Person+, you'll have to specify it with this option.
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
# sql fragment, such as "authorized = 1".
# * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
# an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
# of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
# +Boss+ class will use "boss_id" as the default foreign_key.
# * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
# and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
# destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
# is used on the associate class (such as a Post class).
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
# :conditions => 'discounts > #{payments_count}'
def belongs_to(association_id, options = {})
validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
association_name, association_class_name, class_primary_key_name =
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
require_association_class(association_class_name)
association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
if options[:remote]
association_finder = <<-"end_eval"
#{association_class_name}.find_first(
"#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
#{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
)
end_eval
else
association_finder = options[:conditions] ?
"#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
"#{association_class_name}.find(#{association_class_primary_key_name})"
end
has_association_method(association_name)
association_reader_method(association_name, association_finder)
belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
association_comparison_method(association_name, association_class_name)
if options[:counter_cache]
module_eval(
"after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
" if has_#{association_name}?'"
)
module_eval(
"before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
" if has_#{association_name}?'"
)
end
end
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
# an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
# will give the default join table name of "developers_projects" because "D" outranks "P".
#
# Any additional fields added to the join table will be placed as attributes when pulling records out through
# has_and_belongs_to_many associations. This is helpful when have information about the association itself
# that you want available on retrival.
#
# Adds the following methods for retrival and query.
# +collection+ is replaced with the symbol passed as the first argument, so
# <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
# An empty array is returned if none is found.
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
# (collection.push and collection.concat are aliases to this method).
# * <tt>collection.push_with_attributes(object, join_attributes)</tt> - adds one to the collection by creating an association in the join table that
# also holds the attributes from <tt>join_attributes</tt> (should be a hash with the column names as keys). This can be used to have additional
# attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
# (collection.concat_with_attributes is an alias to this method).
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
# * <tt>collection.size</tt> - returns the number of associated objects.
#
# Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
# * <tt>Developer#projects</tt>
# * <tt>Developer#projects<<</tt>
# * <tt>Developer#projects.delete</tt>
# * <tt>Developer#projects.clear</tt>
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
# The declaration may include an options hash to specialize the behavior of the association.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
# +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
# * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
# WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
# has_and_belongs_to_many declaration in order to work.
# * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
# will use "person_id" as the default foreign_key.
# * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
# that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
# * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
# sql fragment, such as "authorized = 1".
# * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
# * <tt>:uniq</tt> - if set to true, duplicate associated objects will be ignored by accessors and query methods
# * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
# * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
# classes with a manual one
# * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
# with a manual one
#
# Option examples:
# has_and_belongs_to_many :projects
# has_and_belongs_to_many :nations, :class_name => "Country"
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
def has_and_belongs_to_many(association_id, options = {})
validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
:join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
association_name, association_class_name, association_class_primary_key_name =
associate_identification(association_id, options[:class_name], options[:foreign_key])
require_association_class(association_class_name)
join_table = options[:join_table] ||
join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
define_method(association_name) do |*params|
force_reload = params.first unless params.empty?
association = instance_variable_get("@#{association_name}")
if association.nil?
association = HasAndBelongsToManyAssociation.new(self,
association_name, association_class_name,
association_class_primary_key_name, join_table, options)
instance_variable_set("@#{association_name}", association)
end
association.reload if force_reload
association
end
before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\\\#{self.id}'"
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
# deprecated api
deprecated_collection_count_method(association_name)
deprecated_add_association_relation(association_name)
deprecated_remove_association_relation(association_name)
deprecated_has_collection_method(association_name)
end
private
# Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
def validate_options(valid_option_keys, supplied_option_keys)
unknown_option_keys = supplied_option_keys - valid_option_keys
raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
end
def join_table_name(first_table_name, second_table_name)
if first_table_name < second_table_name
join_table = "#{first_table_name}_#{second_table_name}"
else
join_table = "#{second_table_name}_#{first_table_name}"
end
table_name_prefix + join_table + table_name_suffix
end
def associate_identification(association_id, association_class_name, foreign_key, plural = true)
if association_class_name !~ /::/
association_class_name = type_name_with_module(
association_class_name ||
Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
)
end
primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
return association_id.id2name, association_class_name, primary_key_name
end
def association_comparison_method(association_name, association_class_name)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}?(comparison_object, force_reload = false)
if comparison_object.kind_of?(#{association_class_name})
#{association_name}(force_reload) == comparison_object
else
raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
end
end
end_eval
end
def association_reader_method(association_name, association_finder)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}(force_reload = false)
if @#{association_name}.nil? || force_reload
begin
@#{association_name} = #{association_finder}
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
nil
end
end
return @#{association_name}
end
end_eval
end
def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}=(association)
if association.nil?
@#{association_name}.#{class_primary_key_name} = nil
@#{association_name}.save(false)
@#{association_name} = nil
else
raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
association.#{class_primary_key_name} = id
association.save(false)
@#{association_name} = association
end
end
end_eval
end
def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}=(association)
if association.nil?
@#{association_name} = self.#{association_class_primary_key_name} = nil
else
raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
@#{association_name} = association
self.#{association_class_primary_key_name} = association.id
end
end
end_eval
end
def has_association_method(association_name)
module_eval <<-"end_eval", __FILE__, __LINE__
def has_#{association_name}?(force_reload = false)
!#{association_name}(force_reload).nil?
end
end_eval
end
def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{method_prefix + collection_name}(attributes = {})
association = #{collection_class_name}.new
association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
association
end
end_eval
end
def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
module_eval <<-"end_eval", __FILE__, __LINE__
def #{method_prefix + collection_name}(attributes = nil)
#{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
end
end_eval
end
def require_association_class(class_name)
begin
require(Inflector.underscore(class_name))
rescue LoadError
if logger
logger.info "#{self.to_s} failed to require #{class_name}"
else
STDERR << "#{self.to_s} failed to require #{class_name}\n"
end
end
end
end
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,170 @@
# postgresql_adaptor.rb
# author: Luke Holden <lholden@cablelan.net>
# notes: Currently this adaptor does not pass the test_zero_date_fields
# and test_zero_datetime_fields unit tests in the BasicsTest test
# group.
#
# This is due to the fact that, in postgresql you can not have a
# totally zero timestamp. Instead null/nil should be used to
# represent no value.
#
require 'active_record/connection_adapters/abstract_adapter'
require 'parsedate'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.postgresql_connection(config) # :nodoc:
require_library_or_gem 'postgres' unless self.class.const_defined?(:PGconn)
symbolize_strings_in_hash(config)
host = config[:host]
port = config[:port] || 5432 unless host.nil?
username = config[:username].to_s
password = config[:password].to_s
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
ConnectionAdapters::PostgreSQLAdapter.new(
PGconn.connect(host, port, "", "", database, username, password), logger
)
end
end
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter # :nodoc:
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
result = select(sql, name)
result.nil? ? nil : result.first
end
def columns(table_name, name = nil)
table_structure(table_name).inject([]) do |columns, field|
columns << Column.new(field[0], field[2], field[1])
columns
end
end
def insert(sql, name = nil, pk = nil, id_value = nil)
execute(sql, name = nil)
table = sql.split(" ", 4)[2]
return id_value || last_insert_id(table, pk)
end
def execute(sql, name = nil)
log(sql, name, @connection) { |connection| connection.query(sql) }
end
alias_method :update, :execute
alias_method :delete, :execute
def begin_db_transaction() execute "BEGIN" end
def commit_db_transaction() execute "COMMIT" end
def rollback_db_transaction() execute "ROLLBACK" end
def quote_column_name(name)
return "\"#{name}\""
end
private
def last_insert_id(table, column = "id")
sequence_name = "#{table}_#{column || 'id'}_seq"
@connection.exec("SELECT currval('#{sequence_name}')")[0][0].to_i
end
def select(sql, name = nil)
res = nil
log(sql, name, @connection) { |connection| res = connection.exec(sql) }
results = res.result
rows = []
if results.length > 0
fields = res.fields
results.each do |row|
hashed_row = {}
row.each_index { |cel_index| hashed_row[fields[cel_index]] = row[cel_index] }
rows << hashed_row
end
end
return rows
end
def split_table_schema(table_name)
schema_split = table_name.split('.')
schema_name = "public"
if schema_split.length > 1
schema_name = schema_split.first.strip
table_name = schema_split.last.strip
end
return [schema_name, table_name]
end
def table_structure(table_name)
database_name = @connection.db
schema_name, table_name = split_table_schema(table_name)
# Grab a list of all the default values for the columns.
sql = "SELECT column_name, column_default, character_maximum_length, data_type "
sql << " FROM information_schema.columns "
sql << " WHERE table_catalog = '#{database_name}' "
sql << " AND table_schema = '#{schema_name}' "
sql << " AND table_name = '#{table_name}';"
column_defaults = nil
log(sql, nil, @connection) { |connection| column_defaults = connection.query(sql) }
column_defaults.collect do |row|
field = row[0]
type = type_as_string(row[3], row[2])
default = default_value(row[1])
length = row[2]
[field, type, default, length]
end
end
def type_as_string(field_type, field_length)
type = case field_type
when 'numeric', 'real', 'money' then 'float'
when 'character varying', 'interval' then 'string'
when 'timestamp without time zone' then 'datetime'
else field_type
end
size = field_length.nil? ? "" : "(#{field_length})"
return type + size
end
def default_value(value)
# Boolean types
return "t" if value =~ /true/i
return "f" if value =~ /false/i
# Char/String type values
return $1 if value =~ /^'(.*)'::(bpchar|text|character varying)$/
# Numeric values
return value if value =~ /^[0-9]+(\.[0-9]*)?/
# Date / Time magic values
return Time.now.to_s if value =~ /^\('now'::text\)::(date|timestamp)/
# Fixed dates / times
return $1 if value =~ /^'(.+)'::(date|timestamp)/
# Anything else is blank, some user type, or some function
# and we can't know the value of that, so return nil.
return nil
end
end
end
end

View File

@@ -0,0 +1,105 @@
# sqlite_adapter.rb
# author: Luke Holden <lholden@cablelan.net>
require 'active_record/connection_adapters/abstract_adapter'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.sqlite_connection(config) # :nodoc:
require_library_or_gem('sqlite') unless self.class.const_defined?(:SQLite)
symbolize_strings_in_hash(config)
unless config.has_key?(:dbfile)
raise ArgumentError, "No database file specified. Missing argument: dbfile"
end
db = SQLite::Database.new(config[:dbfile], 0)
db.show_datatypes = "ON" if !defined? SQLite::Version
db.results_as_hash = true if defined? SQLite::Version
db.type_translation = false
ConnectionAdapters::SQLiteAdapter.new(db, logger)
end
end
module ConnectionAdapters
class SQLiteAdapter < AbstractAdapter # :nodoc:
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
result = select(sql, name)
result.nil? ? nil : result.first
end
def columns(table_name, name = nil)
table_structure(table_name).inject([]) do |columns, field|
columns << Column.new(field['name'], field['dflt_value'], field['type'])
columns
end
end
def insert(sql, name = nil, pk = nil, id_value = nil)
execute(sql, name = nil)
id_value || @connection.send( defined?( SQLite::Version ) ? :last_insert_row_id : :last_insert_rowid )
end
def execute(sql, name = nil)
log(sql, name, @connection) do |connection|
if defined?( SQLite::Version )
case sql
when "BEGIN" then connection.transaction
when "COMMIT" then connection.commit
when "ROLLBACK" then connection.rollback
else connection.execute(sql)
end
else
connection.execute( sql )
end
end
end
alias_method :update, :execute
alias_method :delete, :execute
def begin_db_transaction() execute "BEGIN" end
def commit_db_transaction() execute "COMMIT" end
def rollback_db_transaction() execute "ROLLBACK" end
def quote_string(s)
SQLite::Database.quote(s)
end
def quote_column_name(name)
return "'#{name}'"
end
private
def select(sql, name = nil)
results = nil
log(sql, name, @connection) { |connection| results = connection.execute(sql) }
rows = []
results.each do |row|
hash_only_row = {}
row.each_key do |key|
hash_only_row[key.sub(/\w+\./, "")] = row[key] unless key.class == Fixnum
end
rows << hash_only_row
end
return rows
end
def table_structure(table_name)
sql = "PRAGMA table_info(#{table_name});"
results = nil
log(sql, nil, @connection) { |connection| results = connection.execute(sql) }
return results
end
end
end
end

View File

@@ -0,0 +1,298 @@
require 'active_record/connection_adapters/abstract_adapter'
# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server
#
# Author: Joey Gibson <joey@joeygibson.com>
# Date: 10/14/2004
#
# REQUIREMENTS:
#
# This adapter will ONLY work on Windows systems, since it relies on Win32OLE, which,
# to my knowledge, is only available on Window.
#
# It relies on the ADO support in the DBI module. If you are using the
# one-click installer of Ruby, then you already have DBI installed, but
# the ADO module is *NOT* installed. You will need to get the latest
# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
# unzip it, and copy the file src/lib/dbd_ado/ADO.rb to
# X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb (you will need to create
# the ADO directory). Once you've installed that file, you are ready to go.
#
# This module uses the ADO-style DSNs for connection. For example:
# "DBI:ADO:Provider=SQLOLEDB;Data Source=(local);Initial Catalog=test;User Id=sa;Password=password;"
# with User Id replaced with your proper login, and Password with your
# password.
#
# I have tested this code on a WindowsXP Pro SP1 system,
# ruby 1.8.2 (2004-07-29) [i386-mswin32], SQL Server 2000.
#
module ActiveRecord
class Base
def self.sqlserver_connection(config)
require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI)
class_eval { include ActiveRecord::SQLServerBaseExtensions }
symbolize_strings_in_hash(config)
if config.has_key? :dsn
dsn = config[:dsn]
else
raise ArgumentError, "No DSN specified"
end
conn = DBI.connect(dsn)
conn["AutoCommit"] = true
ConnectionAdapters::SQLServerAdapter.new(conn, logger)
end
end
module SQLServerBaseExtensions #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
module ClassMethods
def find_first(conditions = nil, orderings = nil)
sql = "SELECT TOP 1 * FROM #{table_name} "
add_conditions!(sql, conditions)
sql << "ORDER BY #{orderings} " unless orderings.nil?
record = connection.select_one(sql, "#{name} Load First")
instantiate(record) unless record.nil?
end
def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)
sql = "SELECT "
sql << "TOP #{limit} " unless limit.nil?
sql << " * FROM #{table_name} "
sql << "#{joins} " if joins
add_conditions!(sql, conditions)
sql << "ORDER BY #{orderings} " unless orderings.nil?
find_by_sql(sql)
end
end
def attributes_with_quotes
columns_hash = self.class.columns_hash
attrs = @attributes.dup
attrs = attrs.reject do |name, value|
columns_hash[name].identity
end
attrs.inject({}) do |attrs_quoted, pair|
attrs_quoted[pair.first] = quote(pair.last, columns_hash[pair.first])
attrs_quoted
end
end
end
module ConnectionAdapters
class ColumnWithIdentity < Column
attr_reader :identity
def initialize(name, default, sql_type = nil, is_identity = false)
super(name, default, sql_type)
@identity = is_identity
end
end
class SQLServerAdapter < AbstractAdapter # :nodoc:
def quote_column_name(name)
" [#{name}] "
end
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
result = select(sql, name)
result.nil? ? nil : result.first
end
def columns(table_name, name = nil)
sql = <<EOL
SELECT s.name AS TableName, c.id AS ColId, c.name AS ColName, t.name AS ColType, c.length AS Length,
c.AutoVal AS IsIdentity,
c.cdefault AS DefaultId, com.text AS DefaultValue
FROM syscolumns AS c
JOIN systypes AS t ON (c.xtype = t.xtype AND c.usertype = t.usertype)
JOIN sysobjects AS s ON (c.id = s.id)
LEFT OUTER JOIN syscomments AS com ON (c.cdefault = com.id)
WHERE s.name = '#{table_name}'
EOL
columns = []
log(sql, name, @connection) do |conn|
conn.select_all(sql) do |row|
default_value = row[:DefaultValue]
if default_value =~ /null/i
default_value = nil
else
default_value =~ /\(([^)]+)\)/
default_value = $1
end
col = ColumnWithIdentity.new(row[:ColName], default_value, "#{row[:ColType]}(#{row[:Length]})", row[:IsIdentity] != nil)
columns << col
end
end
columns
end
def insert(sql, name = nil, pk = nil, id_value = nil)
begin
table_name = get_table_name(sql)
col = get_identity_column(table_name)
ii_enabled = false
if col != nil
if query_contains_identity_column(sql, col)
begin
execute enable_identity_insert(table_name, true)
ii_enabled = true
rescue Exception => e
# Coulnd't turn on IDENTITY_INSERT
end
end
end
log(sql, name, @connection) do |conn|
conn.execute(sql)
select_one("SELECT @@IDENTITY AS Ident")["Ident"]
end
ensure
if ii_enabled
begin
execute enable_identity_insert(table_name, false)
rescue Exception => e
# Couldn't turn off IDENTITY_INSERT
end
end
end
end
def execute(sql, name = nil)
if sql =~ /^INSERT/i
insert(sql, name)
else
log(sql, name, @connection) do |conn|
conn.execute(sql)
end
end
end
alias_method :update, :execute
alias_method :delete, :execute
def begin_db_transaction
begin
@connection["AutoCommit"] = false
rescue Exception => e
@connection["AutoCommit"] = true
end
end
def commit_db_transaction
begin
@connection.commit
ensure
@connection["AutoCommit"] = true
end
end
def rollback_db_transaction
begin
@connection.rollback
ensure
@connection["AutoCommit"] = true
end
end
def recreate_database(name)
drop_database(name)
create_database(name)
end
def drop_database(name)
execute "DROP DATABASE #{name}"
end
def create_database(name)
execute "CREATE DATABASE #{name}"
end
private
def select(sql, name = nil)
rows = []
log(sql, name, @connection) do |conn|
conn.select_all(sql) do |row|
record = {}
row.column_names.each do |col|
record[col] = row[col]
end
rows << record
end
end
rows
end
def enable_identity_insert(table_name, enable = true)
if has_identity_column(table_name)
"SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
end
end
def get_table_name(sql)
if sql =~ /into\s*([^\s]+)\s*/i or
sql =~ /update\s*([^\s]+)\s*/i
$1
else
nil
end
end
def has_identity_column(table_name)
return get_identity_column(table_name) != nil
end
def get_identity_column(table_name)
if not @table_columns
@table_columns = {}
end
if @table_columns[table_name] == nil
@table_columns[table_name] = columns(table_name)
end
@table_columns[table_name].each do |col|
return col.name if col.identity
end
return nil
end
def query_contains_identity_column(sql, col)
return sql =~ /[\(\.\,]\s*#{col}/
end
end
end
end

View File

@@ -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

View File

@@ -0,0 +1,208 @@
require 'erb'
require 'yaml'
require 'active_record/support/class_inheritable_attributes'
require 'active_record/support/inflector'
# Fixtures are a way of organizing data that you want to test against. You normally have one YAML file with fixture
# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep
# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash
# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml:
#
# david:
# id: 1
# name: David Heinemeier Hansson
# birthday: 1979-10-15
# profession: Systems development
#
# steve:
# id: 2
# name: Steve Ross Kellock
# birthday: 1974-09-27
# profession: guy with keyboard
#
# So this YAML file includes two fixtures. T
#
# Now when we call <tt>@developers = Fixtures.create_fixtures(".", "developers")</tt> both developers will get inserted into
# the "developers" table through the active Active Record connection (that must be setup before-hand). And we can now query
# the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt> will return
# <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
#
# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then
# be used for comparison in a unit test. Something like:
#
# def test_find
# assert_equal @developers["david"]["name"], @developers["david"].find.name
# end
#
# Comparing that the data we have on the name is also what the object returns when we ask for it.
#
# == Automatic fixture setup and instance variable availability
#
# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
#
# class FixturesTest < Test::Unit::TestCase
# fixtures :developers # you can add more with comma separation
#
# def test_developers
# assert_equal 3, @developers.size # the container for all the fixtures is automatically set
# assert_kind_of Developer, @david # works like @developers["david"].find
# assert_equal "David Heinemeier Hansson", @david.name
# end
# end
class Fixtures < Hash
def self.instantiate_fixtures(object, fixtures_directory, *table_names)
[ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
object.instance_variable_set "@#{table_names[idx]}", fixtures
fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
end
end
def self.create_fixtures(fixtures_directory, *table_names)
connection = block_given? ? yield : ActiveRecord::Base.connection
old_logger_level = ActiveRecord::Base.logger.level
begin
ActiveRecord::Base.logger.level = Logger::ERROR
fixtures = connection.transaction do
table_names.flatten.map do |table_name|
Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
end
end
return fixtures.size > 1 ? fixtures : fixtures.first
ensure
ActiveRecord::Base.logger.level = old_logger_level
end
end
def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/)
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
@class_name = Inflector.classify(@table_name)
read_fixture_files
delete_existing_fixtures
insert_fixtures
end
private
def read_fixture_files
if File.exists?(yaml_file_path)
YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data|
self[name] = Fixture.new(data, @class_name)
end
else
Dir.entries(@fixture_path).each do |file|
self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter
end
end
end
def delete_existing_fixtures
@connection.delete "DELETE FROM #{@table_name}"
end
def insert_fixtures
values.each do |fixture|
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
end
end
def yaml_file_path
@fixture_path + ".yml"
end
def yaml_fixtures_key(path)
File.basename(@fixture_path).split(".").first
end
def erb_render(fixture_content)
ERB.new(fixture_content).result
end
end
class Fixture #:nodoc:
include Enumerable
class FixtureError < StandardError; end
class FormatError < FixtureError; end
def initialize(fixture, class_name)
@fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
@class_name = class_name
end
def each
@fixture.each { |item| yield item }
end
def [](key)
@fixture[key]
end
def to_hash
@fixture
end
def key_list
@fixture.keys.join(", ")
end
def value_list
@fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
end
def find
Object.const_get(@class_name).find(self["id"])
end
private
def read_fixture_file(fixture_file_path)
IO.readlines(fixture_file_path).inject({}) do |fixture, line|
# Mercifully skip empty lines.
next if line.empty?
# Use the same regular expression for attributes as Active Record.
unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
raise FormatError, "#{path}: fixture format error at '#{line}'. Expecting 'key => value'."
end
key, value = md.captures
# Disallow duplicate keys to catch typos.
raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
fixture[key] = value.strip
fixture
end
end
end
class Test::Unit::TestCase #:nodoc:
include ClassInheritableAttributes
cattr_accessor :fixture_path
cattr_accessor :fixture_table_names
def self.fixtures(*table_names)
write_inheritable_attribute("fixture_table_names", table_names)
end
def setup
instantiate_fixtures(*fixture_table_names) if fixture_table_names
end
def self.method_added(method_symbol)
if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
alias_method :setup_without_fixtures, :setup
define_method(:setup) do
instantiate_fixtures(*fixture_table_names) if fixture_table_names
setup_without_fixtures
end
end
end
private
def instantiate_fixtures(*table_names)
Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
end
def fixture_table_names
self.class.read_inheritable_attribute("fixture_table_names")
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,205 @@
module ActiveRecord
# Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
#
# Example:
#
# class Person < ActiveRecord::Base
# protected
# def validate
# errors.add_on_empty %w( first_name last_name )
# errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
# end
#
# def validate_on_create # is only run the first time a new object is saved
# unless valid_discount?(membership_discount)
# errors.add("membership_discount", "has expired")
# end
# end
#
# def validate_on_update
# errors.add_to_base("No changes have occured") if unchanged_attributes?
# end
# end
#
# person = Person.new("first_name" => "David", "phone_number" => "what?")
# person.save # => false (and doesn't do the save)
# person.errors.empty? # => false
# person.count # => 2
# person.errors.on "last_name" # => "can't be empty"
# person.errors.on "phone_number" # => "has invalid format"
# person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
# "Phone number has invalid format"
#
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
# person.save # => true (and person is now saved in the database)
#
# An +Errors+ object is automatically created for every Active Record.
module Validations
def self.append_features(base) # :nodoc:
super
base.class_eval do
alias_method :save_without_validation, :save
alias_method :save, :save_with_validation
alias_method :update_attribute_without_validation_skipping, :update_attribute
alias_method :update_attribute, :update_attribute_with_validation_skipping
end
end
# The validation process on save can be skipped by passing false. The regular Base#save method is
# replaced with this when the validations module is mixed in, which it is by default.
def save_with_validation(perform_validation = true)
if perform_validation && valid? || !perform_validation then save_without_validation else false end
end
# Updates a single attribute and saves the record without going through the normal validation procedure.
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
# in Base is replaced with this when the validations module is mixed in, which it is by default.
def update_attribute_with_validation_skipping(name, value)
@attributes[name] = value
save(false)
end
# Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
def valid?
errors.clear
validate
if new_record? then validate_on_create else validate_on_update end
errors.empty?
end
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors = Errors.new(self) if @errors.nil?
@errors
end
protected
# Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
def validate #:doc:
end
# Overwrite this method for validation checks used only on creation.
def validate_on_create #:doc:
end
# Overwrite this method for validation checks used only on updates.
def validate_on_update # :doc:
end
end
# Active Record validation is reported to and from this object, which is used by Base#save to
# determine whether the object in a valid state to be saved. See usage example in Validations.
class Errors
def initialize(base) # :nodoc:
@base, @errors = base, {}
end
# Adds an error to the base object instead of any particular attribute. This is used
# to report errors that doesn't tie to any specific attribute, but rather to the object
# as a whole. These error messages doesn't get prepended with any field name when iterating
# with each_full, so they should be complete sentences.
def add_to_base(msg)
add(:base, msg)
end
# Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
# If no +msg+ is supplied, "invalid" is assumed.
def add(attribute, msg = "invalid")
@errors[attribute] = [] if @errors[attribute].nil?
@errors[attribute] << msg
end
# Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
def add_on_empty(attributes, msg = "can't be empty")
[attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr) }
end
# Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
# If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
for attr in [attributes].flatten
add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr) && @base.send(attr).length < range.begin
add(attr, too_long_msg % range.end) if @base.attribute_present?(attr) && @base.send(attr).length > range.end
end
end
alias :add_on_boundry_breaking :add_on_boundary_breaking
# Returns true if the specified +attribute+ has errors associated with it.
def invalid?(attribute)
!@errors[attribute].nil?
end
# * Returns nil, if no errors are associated with the specified +attribute+.
# * Returns the error message, if one error is associated with the specified +attribute+.
# * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
def on(attribute)
if @errors[attribute].nil?
nil
elsif @errors[attribute].length == 1
@errors[attribute].first
else
@errors[attribute]
end
end
alias :[] :on
# Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
def on_base
on(:base)
end
# Yields each attribute and associated message per error added.
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
# through iteration as "First name can't be empty".
def each_full
full_messages.each { |msg| yield msg }
end
# Returns all the full error messages in an array.
def full_messages
full_messages = []
@errors.each_key do |attr|
@errors[attr].each do |msg|
if attr == :base
full_messages << msg
else
full_messages << @base.class.human_attribute_name(attr) + " " + msg
end
end
end
return full_messages
end
# Returns true if no errors have been added.
def empty?
return @errors.empty?
end
# Removes all the errors that have been added.
def clear
@errors = {}
end
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
# with this as well.
def count
error_count = 0
@errors.each_value { |attribute| error_count += attribute.length }
error_count
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,702 @@
# :title: Transaction::Simple
#
# == Licence
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#--
# Transaction::Simple
# Simple object transaction support for Ruby
# Version 1.11
#
# Copyright (c) 2003 Austin Ziegler
#
# $Id: simple.rb,v 1.2 2004/08/20 13:56:37 webster132 Exp $
#
# ==========================================================================
# Revision History ::
# YYYY.MM.DD Change ID Developer
# Description
# --------------------------------------------------------------------------
# 2003.07.29 Austin Ziegler
# Added debugging capabilities and VERSION string.
# 2003.08.21 Austin Ziegler
# Added named transactions.
#
# ==========================================================================
#++
require 'thread'
# The "Transaction" namespace can be used for additional transactional
# support objects and modules.
module Transaction
# A standard exception for transactional errors.
class TransactionError < StandardError; end
# A standard exception for transactional errors involving the acquisition
# of locks for Transaction::Simple::ThreadSafe.
class TransactionThreadError < StandardError; end
# = Transaction::Simple for Ruby
# Simple object transaction support for Ruby
#
# == Introduction
#
# Transaction::Simple provides a generic way to add active transactional
# support to objects. The transaction methods added by this module will
# work with most objects, excluding those that cannot be <i>Marshal</i>ed
# (bindings, procedure objects, IO instances, or singleton objects).
#
# The transactions supported by Transaction::Simple are not backed
# transactions; that is, they have nothing to do with any sort of data
# store. They are "live" transactions occurring in memory and in the
# object itself. This is to allow "test" changes to be made to an object
# before making the changes permanent.
#
# Transaction::Simple can handle an "infinite" number of transactional
# levels (limited only by memory). If I open two transactions, commit the
# first, but abort the second, the object will revert to the original
# version.
#
# Transaction::Simple supports "named" transactions, so that multiple
# levels of transactions can be committed, aborted, or rewound by
# referring to the appropriate name of the transaction. Names may be any
# object *except* +nil+.
#
# Copyright:: Copyright © 2003 by Austin Ziegler
# Version:: 1.1
# Licence:: MIT-Style
#
# Thanks to David Black for help with the initial concept that led to this
# library.
#
# == Usage
# include 'transaction/simple'
#
# v = "Hello, you." # => "Hello, you."
# v.extend(Transaction::Simple) # => "Hello, you."
#
# v.start_transaction # => ... (a Marshal string)
# v.transaction_open? # => true
# v.gsub!(/you/, "world") # => "Hello, world."
#
# v.rewind_transaction # => "Hello, you."
# v.transaction_open? # => true
#
# v.gsub!(/you/, "HAL") # => "Hello, HAL."
# v.abort_transaction # => "Hello, you."
# v.transaction_open? # => false
#
# v.start_transaction # => ... (a Marshal string)
# v.start_transaction # => ... (a Marshal string)
#
# v.transaction_open? # => true
# v.gsub!(/you/, "HAL") # => "Hello, HAL."
#
# v.commit_transaction # => "Hello, HAL."
# v.transaction_open? # => true
# v.abort_transaction # => "Hello, you."
# v.transaction_open? # => false
#
# == Named Transaction Usage
# v = "Hello, you." # => "Hello, you."
# v.extend(Transaction::Simple) # => "Hello, you."
#
# v.start_transaction(:first) # => ... (a Marshal string)
# v.transaction_open? # => true
# v.transaction_open?(:first) # => true
# v.transaction_open?(:second) # => false
# v.gsub!(/you/, "world") # => "Hello, world."
#
# v.start_transaction(:second) # => ... (a Marshal string)
# v.gsub!(/world/, "HAL") # => "Hello, HAL."
# v.rewind_transaction(:first) # => "Hello, you."
# v.transaction_open? # => true
# v.transaction_open?(:first) # => true
# v.transaction_open?(:second) # => false
#
# v.gsub!(/you/, "world") # => "Hello, world."
# v.start_transaction(:second) # => ... (a Marshal string)
# v.gsub!(/world/, "HAL") # => "Hello, HAL."
# v.transaction_name # => :second
# v.abort_transaction(:first) # => "Hello, you."
# v.transaction_open? # => false
#
# v.start_transaction(:first) # => ... (a Marshal string)
# v.gsub!(/you/, "world") # => "Hello, world."
# v.start_transaction(:second) # => ... (a Marshal string)
# v.gsub!(/world/, "HAL") # => "Hello, HAL."
#
# v.commit_transaction(:first) # => "Hello, HAL."
# v.transaction_open? # => false
#
# == Contraindications
#
# While Transaction::Simple is very useful, it has some severe limitations
# that must be understood. Transaction::Simple:
#
# * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed cannot
# use Transaction::Simple.
# * does not manage resources. Resources external to the object and its
# instance variables are not managed at all. However, all instance
# variables and objects "belonging" to those instance variables are
# managed. If there are object reference counts to be handled,
# Transaction::Simple will probably cause problems.
# * is not inherently thread-safe. In the ACID ("atomic, consistent,
# isolated, durable") test, Transaction::Simple provides CD, but it is
# up to the user of Transaction::Simple to provide isolation and
# atomicity. Transactions should be considered "critical sections" in
# multi-threaded applications. If thread safety and atomicity is
# absolutely required, use Transaction::Simple::ThreadSafe, which uses a
# Mutex object to synchronize the accesses on the object during the
# transactional operations.
# * does not necessarily maintain Object#__id__ values on rewind or abort.
# This may change for future versions that will be Ruby 1.8 or better
# *only*. Certain objects that support #replace will maintain
# Object#__id__.
# * Can be a memory hog if you use many levels of transactions on many
# objects.
#
module Simple
VERSION = '1.1.1.0';
# Sets the Transaction::Simple debug object. It must respond to #<<.
# Sets the transaction debug object. Debugging will be performed
# automatically if there's a debug object. The generic transaction error
# class.
def self.debug_io=(io)
raise TransactionError, "Transaction Error: the transaction debug object must respond to #<<" unless io.respond_to?(:<<)
@tdi = io
end
# Returns the Transaction::Simple debug object. It must respond to #<<.
def self.debug_io
@tdi
end
# If +name+ is +nil+ (default), then returns +true+ if there is
# currently a transaction open.
#
# If +name+ is specified, then returns +true+ if there is currently a
# transaction that responds to +name+ open.
def transaction_open?(name = nil)
if name.nil?
Transaction::Simple.debug_io << "Transaction [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
return (not @__transaction_checkpoint__.nil?)
else
Transaction::Simple.debug_io << "Transaction(#{name.inspect}) [#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n" unless Transaction::Simple.debug_io.nil?
return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
end
end
# Returns the current name of the transaction. Transactions not
# explicitly named are named +nil+.
def transaction_name
raise TransactionError, "Transaction Error: No transaction open." if @__transaction_checkpoint__.nil?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Transaction Name: #{@__transaction_names__[-1].inspect}\n" unless Transaction::Simple.debug_io.nil?
@__transaction_names__[-1]
end
# Starts a transaction. Stores the current object state. If a
# transaction name is specified, the transaction will be named.
# Transaction names must be unique. Transaction names of +nil+ will be
# treated as unnamed transactions.
def start_transaction(name = nil)
@__transaction_level__ ||= 0
@__transaction_names__ ||= []
if name.nil?
@__transaction_names__ << nil
s = ""
else
raise TransactionError, "Transaction Error: Named transactions must be unique." if @__transaction_names__.include?(name)
@__transaction_names__ << name
s = "(#{name.inspect})"
end
@__transaction_level__ += 1
Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} Start Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
@__transaction_checkpoint__ = Marshal.dump(self)
end
# Rewinds the transaction. If +name+ is specified, then the intervening
# transactions will be aborted and the named transaction will be
# rewound. Otherwise, only the current transaction is rewound.
def rewind_transaction(name = nil)
raise TransactionError, "Transaction Error: Cannot rewind. There is no current transaction." if @__transaction_checkpoint__.nil?
if name.nil?
__rewind_this_transaction
s = ""
else
raise TransactionError, "Transaction Error: Cannot rewind to transaction #{name.inspect} because it does not exist." unless @__transaction_names__.include?(name)
s = "(#{name})"
while @__transaction_names__[-1] != name
@__transaction_checkpoint__ = __rewind_this_transaction
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
@__transaction_level__ -= 1
@__transaction_names__.pop
end
__rewind_this_transaction
end
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} Rewind Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
self
end
# Aborts the transaction. Resets the object state to what it was before
# the transaction was started and closes the transaction. If +name+ is
# specified, then the intervening transactions and the named transaction
# will be aborted. Otherwise, only the current transaction is aborted.
def abort_transaction(name = nil)
raise TransactionError, "Transaction Error: Cannot abort. There is no current transaction." if @__transaction_checkpoint__.nil?
if name.nil?
__abort_transaction(name)
else
raise TransactionError, "Transaction Error: Cannot abort nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
__abort_transaction(name) while @__transaction_names__.include?(name)
end
self
end
# If +name+ is +nil+ (default), the current transaction level is closed
# out and the changes are committed.
#
# If +name+ is specified and +name+ is in the list of named
# transactions, then all transactions are closed and committed until the
# named transaction is reached.
def commit_transaction(name = nil)
raise TransactionError, "Transaction Error: Cannot commit. There is no current transaction." if @__transaction_checkpoint__.nil?
if name.nil?
s = ""
__commit_transaction
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
else
raise TransactionError, "Transaction Error: Cannot commit nonexistant transaction #{name.inspect}." unless @__transaction_names__.include?(name)
s = "(#{name})"
while @__transaction_names__[-1] != name
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
__commit_transaction
end
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Commit Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
__commit_transaction
end
self
end
# Alternative method for calling the transaction methods. An optional
# name can be specified for named transaction support.
#
# #transaction(:start):: #start_transaction
# #transaction(:rewind):: #rewind_transaction
# #transaction(:abort):: #abort_transaction
# #transaction(:commit):: #commit_transaction
# #transaction(:name):: #transaction_name
# #transaction:: #transaction_open?
def transaction(action = nil, name = nil)
case action
when :start
start_transaction(name)
when :rewind
rewind_transaction(name)
when :abort
abort_transaction(name)
when :commit
commit_transaction(name)
when :name
transaction_name
when nil
transaction_open?(name)
end
end
def __abort_transaction(name = nil) #:nodoc:
@__transaction_checkpoint__ = __rewind_this_transaction
if name.nil?
s = ""
else
s = "(#{name.inspect})"
end
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} Abort Transaction#{s}\n" unless Transaction::Simple.debug_io.nil?
@__transaction_level__ -= 1
@__transaction_names__.pop
if @__transaction_level__ < 1
@__transaction_level__ = 0
@__transaction_names__ = []
end
end
TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
def __rewind_this_transaction #:nodoc:
r = Marshal.restore(@__transaction_checkpoint__)
begin
self.replace(r) if respond_to?(:replace)
rescue
nil
end
r.instance_variables.each do |i|
next if SKIP_TRANSACTION_VARS.include?(i)
if respond_to?(:instance_variable_get)
instance_variable_set(i, r.instance_variable_get(i))
else
instance_eval(%q|#{i} = r.instance_eval("#{i}")|)
end
end
if respond_to?(:instance_variable_get)
return r.instance_variable_get(TRANSACTION_CHECKPOINT)
else
return r.instance_eval(TRANSACTION_CHECKPOINT)
end
end
def __commit_transaction #:nodoc:
if respond_to?(:instance_variable_get)
@__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
else
@__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
end
@__transaction_level__ -= 1
@__transaction_names__.pop
if @__transaction_level__ < 1
@__transaction_level__ = 0
@__transaction_names__ = []
end
end
private :__abort_transaction, :__rewind_this_transaction, :__commit_transaction
# = Transaction::Simple::ThreadSafe
# Thread-safe simple object transaction support for Ruby.
# Transaction::Simple::ThreadSafe is used in the same way as
# Transaction::Simple. Transaction::Simple::ThreadSafe uses a Mutex
# object to ensure atomicity at the cost of performance in threaded
# applications.
#
# Transaction::Simple::ThreadSafe will not wait to obtain a lock; if the
# lock cannot be obtained immediately, a
# Transaction::TransactionThreadError will be raised.
#
# Thanks to Mauricio Fernández for help with getting this part working.
module ThreadSafe
VERSION = '1.1.1.0';
include Transaction::Simple
SKIP_TRANSACTION_VARS = Transaction::Simple::SKIP_TRANSACTION_VARS.dup #:nodoc:
SKIP_TRANSACTION_VARS << "@__transaction_mutex__"
Transaction::Simple.instance_methods(false) do |meth|
next if meth == "transaction"
arg = "(name = nil)" unless meth == "transaction_name"
module_eval <<-EOS
def #{meth}#{arg}
if (@__transaction_mutex__ ||= Mutex.new).try_lock
result = super
@__transaction_mutex__.unlock
return result
else
raise TransactionThreadError, "Transaction Error: Cannot obtain lock for ##{meth}"
end
ensure
@__transaction_mutex__.unlock
end
EOS
end
end
end
end
if $0 == __FILE__
require 'test/unit'
class Test__Transaction_Simple < Test::Unit::TestCase #:nodoc:
VALUE = "Now is the time for all good men to come to the aid of their country."
def setup
@value = VALUE.dup
@value.extend(Transaction::Simple)
end
def test_extended
assert_respond_to(@value, :start_transaction)
end
def test_started
assert_equal(false, @value.transaction_open?)
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
end
def test_rewind
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_not_equal(VALUE, @value)
assert_nothing_raised { @value.rewind_transaction }
assert_equal(true, @value.transaction_open?)
assert_equal(VALUE, @value)
end
def test_abort
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.abort_transaction }
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_not_equal(VALUE, @value)
assert_nothing_raised { @value.abort_transaction }
assert_equal(false, @value.transaction_open?)
assert_equal(VALUE, @value)
end
def test_commit
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.commit_transaction }
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_not_equal(VALUE, @value)
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.commit_transaction }
assert_equal(false, @value.transaction_open?)
assert_not_equal(VALUE, @value)
end
def test_multilevel
assert_equal(false, @value.transaction_open?)
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_equal(VALUE.gsub(/men/, 'women'), @value)
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.start_transaction }
assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
assert_nothing_raised { @value.commit_transaction }
assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.abort_transaction }
assert_equal(VALUE, @value)
end
def test_multilevel_named
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.transaction_name }
assert_nothing_raised { @value.start_transaction(:first) } # 1
assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
assert_equal(true, @value.transaction_open?)
assert_equal(true, @value.transaction_open?(:first))
assert_equal(:first, @value.transaction_name)
assert_nothing_raised { @value.start_transaction } # 2
assert_not_equal(:first, @value.transaction_name)
assert_equal(nil, @value.transaction_name)
assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
assert_nothing_raised { @value.abort_transaction(:first) }
assert_equal(false, @value.transaction_open?)
assert_nothing_raised do
@value.start_transaction(:first)
@value.gsub!(/men/, 'women')
@value.start_transaction(:second)
@value.gsub!(/women/, 'people')
@value.start_transaction
@value.gsub!(/people/, 'sentients')
end
assert_nothing_raised { @value.abort_transaction(:second) }
assert_equal(true, @value.transaction_open?(:first))
assert_equal(VALUE.gsub(/men/, 'women'), @value)
assert_nothing_raised do
@value.start_transaction(:second)
@value.gsub!(/women/, 'people')
@value.start_transaction
@value.gsub!(/people/, 'sentients')
end
assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
assert_nothing_raised { @value.rewind_transaction(:second) }
assert_equal(VALUE.gsub(/men/, 'women'), @value)
assert_nothing_raised do
@value.gsub!(/women/, 'people')
@value.start_transaction
@value.gsub!(/people/, 'sentients')
end
assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
assert_nothing_raised { @value.commit_transaction(:first) }
assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
assert_equal(false, @value.transaction_open?)
end
def test_array
assert_nothing_raised do
@orig = ["first", "second", "third"]
@value = ["first", "second", "third"]
@value.extend(Transaction::Simple)
end
assert_equal(@orig, @value)
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
assert_not_equal(@orig, @value)
assert_nothing_raised { @value.abort_transaction }
assert_equal(@orig, @value)
end
end
class Test__Transaction_Simple_ThreadSafe < Test::Unit::TestCase #:nodoc:
VALUE = "Now is the time for all good men to come to the aid of their country."
def setup
@value = VALUE.dup
@value.extend(Transaction::Simple::ThreadSafe)
end
def test_extended
assert_respond_to(@value, :start_transaction)
end
def test_started
assert_equal(false, @value.transaction_open?)
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
end
def test_rewind
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.rewind_transaction }
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_not_equal(VALUE, @value)
assert_nothing_raised { @value.rewind_transaction }
assert_equal(true, @value.transaction_open?)
assert_equal(VALUE, @value)
end
def test_abort
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.abort_transaction }
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_not_equal(VALUE, @value)
assert_nothing_raised { @value.abort_transaction }
assert_equal(false, @value.transaction_open?)
assert_equal(VALUE, @value)
end
def test_commit
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.commit_transaction }
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_not_equal(VALUE, @value)
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.commit_transaction }
assert_equal(false, @value.transaction_open?)
assert_not_equal(VALUE, @value)
end
def test_multilevel
assert_equal(false, @value.transaction_open?)
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.gsub!(/men/, 'women') }
assert_equal(VALUE.gsub(/men/, 'women'), @value)
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.start_transaction }
assert_nothing_raised { @value.gsub!(/country/, 'nation-state') }
assert_nothing_raised { @value.commit_transaction }
assert_equal(VALUE.gsub(/men/, 'women').gsub(/country/, 'nation-state'), @value)
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value.abort_transaction }
assert_equal(VALUE, @value)
end
def test_multilevel_named
assert_equal(false, @value.transaction_open?)
assert_raises(Transaction::TransactionError) { @value.transaction_name }
assert_nothing_raised { @value.start_transaction(:first) } # 1
assert_raises(Transaction::TransactionError) { @value.start_transaction(:first) }
assert_equal(true, @value.transaction_open?)
assert_equal(true, @value.transaction_open?(:first))
assert_equal(:first, @value.transaction_name)
assert_nothing_raised { @value.start_transaction } # 2
assert_not_equal(:first, @value.transaction_name)
assert_equal(nil, @value.transaction_name)
assert_raises(Transaction::TransactionError) { @value.abort_transaction(:second) }
assert_nothing_raised { @value.abort_transaction(:first) }
assert_equal(false, @value.transaction_open?)
assert_nothing_raised do
@value.start_transaction(:first)
@value.gsub!(/men/, 'women')
@value.start_transaction(:second)
@value.gsub!(/women/, 'people')
@value.start_transaction
@value.gsub!(/people/, 'sentients')
end
assert_nothing_raised { @value.abort_transaction(:second) }
assert_equal(true, @value.transaction_open?(:first))
assert_equal(VALUE.gsub(/men/, 'women'), @value)
assert_nothing_raised do
@value.start_transaction(:second)
@value.gsub!(/women/, 'people')
@value.start_transaction
@value.gsub!(/people/, 'sentients')
end
assert_raises(Transaction::TransactionError) { @value.rewind_transaction(:foo) }
assert_nothing_raised { @value.rewind_transaction(:second) }
assert_equal(VALUE.gsub(/men/, 'women'), @value)
assert_nothing_raised do
@value.gsub!(/women/, 'people')
@value.start_transaction
@value.gsub!(/people/, 'sentients')
end
assert_raises(Transaction::TransactionError) { @value.commit_transaction(:foo) }
assert_nothing_raised { @value.commit_transaction(:first) }
assert_equal(VALUE.gsub(/men/, 'sentients'), @value)
assert_equal(false, @value.transaction_open?)
end
def test_array
assert_nothing_raised do
@orig = ["first", "second", "third"]
@value = ["first", "second", "third"]
@value.extend(Transaction::Simple::ThreadSafe)
end
assert_equal(@orig, @value)
assert_nothing_raised { @value.start_transaction }
assert_equal(true, @value.transaction_open?)
assert_nothing_raised { @value[1].gsub!(/second/, "fourth") }
assert_not_equal(@orig, @value)
assert_nothing_raised { @value.abort_transaction }
assert_equal(@orig, @value)
end
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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/"

View File

@@ -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

8
activerecord/test/all.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
if [ -z "$1" ]; then
echo "Usage: $0 connections/<db_library>" 1>&2
exit 1
fi
ruby -I $1 -e 'Dir.foreach(".") { |file| require file if file =~ /_test.rb$/ }'

View File

@@ -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

544
activerecord/test/base_test.rb Executable file
View File

@@ -0,0 +1,544 @@
require 'abstract_unit'
require 'fixtures/topic'
require 'fixtures/reply'
require 'fixtures/company'
require 'fixtures/default'
require 'fixtures/auto_id'
require 'fixtures/column_name'
class Category < ActiveRecord::Base; end
class Smarts < ActiveRecord::Base; end
class CreditCard < ActiveRecord::Base; end
class MasterCreditCard < ActiveRecord::Base; end
class LoosePerson < ActiveRecord::Base
attr_protected :credit_rating, :administrator
end
class TightPerson < ActiveRecord::Base
attr_accessible :name, :address
end
class TightDescendent < TightPerson
attr_accessible :phone_number
end
class Booleantest < ActiveRecord::Base; end
class BasicsTest < Test::Unit::TestCase
def setup
@topic_fixtures, @companies = create_fixtures "topics", "companies"
end
def test_set_attributes
topic = Topic.find(1)
topic.attributes = { "title" => "Budget", "author_name" => "Jason" }
topic.save
assert_equal("Budget", topic.title)
assert_equal("Jason", topic.author_name)
assert_equal(@topic_fixtures["first"]["author_email_address"], Topic.find(1).author_email_address)
end
def test_integers_as_nil
Topic.update(1, "approved" => "")
assert_nil Topic.find(1).approved
end
def test_set_attributes_with_block
topic = Topic.new do |t|
t.title = "Budget"
t.author_name = "Jason"
end
assert_equal("Budget", topic.title)
assert_equal("Jason", topic.author_name)
end
def test_respond_to?
topic = Topic.find(1)
assert topic.respond_to?("title")
assert topic.respond_to?("title?")
assert topic.respond_to?("title=")
assert topic.respond_to?(:title)
assert topic.respond_to?(:title?)
assert topic.respond_to?(:title=)
assert topic.respond_to?("author_name")
assert topic.respond_to?("attribute_names")
assert !topic.respond_to?("nothingness")
assert !topic.respond_to?(:nothingness)
end
def test_array_content
topic = Topic.new
topic.content = %w( one two three )
topic.save
assert_equal(%w( one two three ), Topic.find(topic.id).content)
end
def test_hash_content
topic = Topic.new
topic.content = { "one" => 1, "two" => 2 }
topic.save
assert_equal 2, Topic.find(topic.id).content["two"]
topic.content["three"] = 3
topic.save
assert_equal 3, Topic.find(topic.id).content["three"]
end
def test_update_array_content
topic = Topic.new
topic.content = %w( one two three )
topic.content.push "four"
assert_equal(%w( one two three four ), topic.content)
topic.save
topic = Topic.find(topic.id)
topic.content << "five"
assert_equal(%w( one two three four five ), topic.content)
end
def test_create
topic = Topic.new
topic.title = "New Topic"
topic.save
id = topic.id
topicReloaded = Topic.find(id)
assert_equal("New Topic", topicReloaded.title)
end
def test_create_through_factory
topic = Topic.create("title" => "New Topic")
topicReloaded = Topic.find(topic.id)
assert_equal(topic, topicReloaded)
end
def test_update
topic = Topic.new
topic.title = "Another New Topic"
topic.written_on = "2003-12-12 23:23"
topic.save
id = topic.id
assert_equal(id, topic.id)
topicReloaded = Topic.find(id)
assert_equal("Another New Topic", topicReloaded.title)
topicReloaded.title = "Updated topic"
topicReloaded.save
topicReloadedAgain = Topic.find(id)
assert_equal("Updated topic", topicReloadedAgain.title)
end
def test_preserving_date_objects
# SQL Server doesn't have a separate column type just for dates, so all are returned as time
if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
end
assert_kind_of(
Date, Topic.find(1).last_read,
"The last_read attribute should be of the Date class"
)
end
def test_preserving_time_objects
assert_kind_of(
Time, Topic.find(1).written_on,
"The written_on attribute should be of the Time class"
)
end
def test_destroy
topic = Topic.new
topic.title = "Yet Another New Topic"
topic.written_on = "2003-12-12 23:23"
topic.save
id = topic.id
topic.destroy
assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(id) }
end
def test_record_not_found_exception
assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(id) }
end
def test_initialize_with_attributes
topic = Topic.new({
"title" => "initialized from attributes", "written_on" => "2003-12-12 23:23"
})
assert_equal("initialized from attributes", topic.title)
end
def test_load
topics = Topic.find_all nil, "id"
assert_equal(2, topics.size)
assert_equal(@topic_fixtures["first"]["title"], topics.first.title)
end
def test_load_with_condition
topics = Topic.find_all "author_name = 'Mary'"
assert_equal(1, topics.size)
assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
end
def test_table_name_guesses
assert_equal "topics", Topic.table_name
assert_equal "categories", Category.table_name
assert_equal "smarts", Smarts.table_name
assert_equal "credit_cards", CreditCard.table_name
assert_equal "master_credit_cards", MasterCreditCard.table_name
ActiveRecord::Base.pluralize_table_names = false
assert_equal "category", Category.table_name
assert_equal "smarts", Smarts.table_name
assert_equal "credit_card", CreditCard.table_name
assert_equal "master_credit_card", MasterCreditCard.table_name
ActiveRecord::Base.pluralize_table_names = true
ActiveRecord::Base.table_name_prefix = "test_"
assert_equal "test_categories", Category.table_name
ActiveRecord::Base.table_name_suffix = "_test"
assert_equal "test_categories_test", Category.table_name
ActiveRecord::Base.table_name_prefix = ""
assert_equal "categories_test", Category.table_name
ActiveRecord::Base.table_name_suffix = ""
assert_equal "categories", Category.table_name
ActiveRecord::Base.pluralize_table_names = false
ActiveRecord::Base.table_name_prefix = "test_"
assert_equal "test_category", Category.table_name
ActiveRecord::Base.table_name_suffix = "_test"
assert_equal "test_category_test", Category.table_name
ActiveRecord::Base.table_name_prefix = ""
assert_equal "category_test", Category.table_name
ActiveRecord::Base.table_name_suffix = ""
assert_equal "category", Category.table_name
ActiveRecord::Base.pluralize_table_names = true
end
def test_destroy_all
assert_equal(2, Topic.find_all.size)
Topic.destroy_all "author_name = 'Mary'"
assert_equal(1, Topic.find_all.size)
end
def test_boolean_attributes
assert ! Topic.find(1).approved?
assert Topic.find(2).approved?
end
def test_increment_counter
Topic.increment_counter("replies_count", 1)
assert_equal 1, Topic.find(1).replies_count
Topic.increment_counter("replies_count", 1)
assert_equal 2, Topic.find(1).replies_count
end
def test_decrement_counter
Topic.decrement_counter("replies_count", 2)
assert_equal 1, Topic.find(2).replies_count
Topic.decrement_counter("replies_count", 2)
assert_equal 0, Topic.find(1).replies_count
end
def test_update_all
Topic.update_all "content = 'bulk updated!'"
assert_equal "bulk updated!", Topic.find(1).content
assert_equal "bulk updated!", Topic.find(2).content
end
def test_update_by_condition
Topic.update_all "content = 'bulk updated!'", "approved = 1"
assert_equal "Have a nice day", Topic.find(1).content
assert_equal "bulk updated!", Topic.find(2).content
end
def test_attribute_present
t = Topic.new
t.title = "hello there!"
t.written_on = Time.now
assert t.attribute_present?("title")
assert t.attribute_present?("written_on")
assert !t.attribute_present?("content")
end
def test_attribute_keys_on_new_instance
t = Topic.new
assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
assert_raises(NoMethodError) { t.title2 }
end
def test_class_name
assert_equal "Firm", ActiveRecord::Base.class_name("firms")
assert_equal "Category", ActiveRecord::Base.class_name("categories")
assert_equal "AccountHolder", ActiveRecord::Base.class_name("account_holder")
ActiveRecord::Base.pluralize_table_names = false
assert_equal "Firms", ActiveRecord::Base.class_name( "firms" )
ActiveRecord::Base.pluralize_table_names = true
ActiveRecord::Base.table_name_prefix = "test_"
assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms" )
ActiveRecord::Base.table_name_suffix = "_tests"
assert_equal "Firm", ActiveRecord::Base.class_name( "test_firms_tests" )
ActiveRecord::Base.table_name_prefix = ""
assert_equal "Firm", ActiveRecord::Base.class_name( "firms_tests" )
ActiveRecord::Base.table_name_suffix = ""
assert_equal "Firm", ActiveRecord::Base.class_name( "firms" )
end
def test_null_fields
assert_nil Topic.find(1).parent_id
assert_nil Topic.create("title" => "Hey you").parent_id
end
def test_default_values
topic = Topic.new
assert_equal 1, topic.approved
assert_nil topic.written_on
assert_nil topic.last_read
topic.save
topic = Topic.find(topic.id)
assert_equal 1, topic.approved
assert_nil topic.last_read
end
def test_default_values_on_empty_strings
topic = Topic.new
topic.approved = nil
topic.last_read = nil
topic.save
topic = Topic.find(topic.id)
assert_nil topic.last_read
assert_nil topic.approved
end
def test_equality
assert_equal Topic.find(1), Topic.find(2).parent
end
def test_hashing
assert_equal [ Topic.find(1) ], [ Topic.find(2).parent ] & [ Topic.find(1) ]
end
def test_destroy_new_record
client = Client.new
client.destroy
assert client.frozen?
end
def test_update_attribute
assert !Topic.find(1).approved?
Topic.find(1).update_attribute("approved", true)
assert Topic.find(1).approved?
end
def test_mass_assignment_protection
firm = Firm.new
firm.attributes = { "name" => "Next Angle", "rating" => 5 }
assert_equal 1, firm.rating
end
def test_mass_assignment_accessible
reply = Reply.new("title" => "hello", "content" => "world", "approved" => 0)
reply.save
assert_equal 1, reply.approved
reply.approved = 0
reply.save
assert_equal 0, reply.approved
end
def test_mass_assignment_protection_inheritance
assert_equal [ :credit_rating, :administrator ], LoosePerson.protected_attributes
assert_nil TightPerson.protected_attributes
end
def test_multiparameter_attributes_on_date
# SQL Server doesn't have a separate column type just for dates, so all are returned as time
if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
end
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Date.new(2004, 6, 24).to_s, topic.last_read.to_s
end
def test_multiparameter_attributes_on_date_with_empty_date
# SQL Server doesn't have a separate column type just for dates, so all are returned as time
if ActiveRecord::ConnectionAdapters.const_defined? :SQLServerAdapter
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
end
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" }
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Date.new(2004, 6, 1).to_s, topic.last_read.to_s
end
def test_multiparameter_attributes_on_date_with_all_empty
attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" }
topic = Topic.find(1)
topic.attributes = attributes
assert_nil topic.last_read
end
def test_multiparameter_attributes_on_time
attributes = {
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
}
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
def test_multiparameter_attributes_on_time_with_empty_seconds
attributes = {
"written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
"written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
}
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
def test_boolean
b_false = Booleantest.create({ "value" => false })
false_id = b_false.id
b_true = Booleantest.create({ "value" => true })
true_id = b_true.id
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
assert b_true.value?
end
def test_clone
topic = Topic.find(1)
cloned_topic = topic.clone
assert_equal topic.title, cloned_topic.title
assert cloned_topic.new_record?
# test if the attributes have been cloned
topic.title = "a"
cloned_topic.title = "b"
assert_equal "a", topic.title
assert_equal "b", cloned_topic.title
# test if the attribute values have been cloned
topic.title = {"a" => "b"}
cloned_topic = topic.clone
cloned_topic.title["a"] = "c"
assert_equal "b", topic.title["a"]
end
def test_bignum
company = Company.find(1)
company.rating = 2147483647
company.save
company = Company.find(1)
assert_equal 2147483647, company.rating
end
def test_default
if Default.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
default = Default.new
# dates / timestampts
time_format = "%m/%d/%Y %H:%M"
assert_equal Time.now.strftime(time_format), default.modified_time.strftime(time_format)
assert_equal Date.today, default.modified_date
# fixed dates / times
assert_equal Date.new(2004, 1, 1), default.fixed_date
assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
# char types
assert_equal 'Y', default.char1
assert_equal 'a varchar field', default.char2
assert_equal 'a text field', default.char3
end
end
def test_auto_id
auto = AutoId.new
auto.save
assert (auto.id > 0)
end
def quote_column_name(name)
"<#{name}>"
end
def test_quote_keys
ar = AutoId.new
source = {"foo" => "bar", "baz" => "quux"}
actual = ar.send(:quote_columns, self, source)
inverted = actual.invert
assert_equal("<foo>", inverted["bar"])
assert_equal("<baz>", inverted["quux"])
end
def test_column_name_properly_quoted
col_record = ColumnName.new
col_record.references = 40
col_record.save
col_record.references = 41
col_record.save
c2 = ColumnName.find(col_record.id)
assert_equal(41, c2.references)
end
MyObject = Struct.new :attribute1, :attribute2
def test_serialized_attribute
myobj = MyObject.new('value1', 'value2')
topic = Topic.create("content" => myobj)
Topic.serialize("content", MyObject)
assert_equal(myobj, topic.content)
end
def test_serialized_attribute_with_class_constraint
myobj = MyObject.new('value1', 'value2')
topic = Topic.create("content" => myobj)
Topic.serialize(:content, Hash)
assert_raises(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
settings = { "color" => "blue" }
Topic.find(topic.id).update_attribute("content", settings)
assert_equal(settings, Topic.find(topic.id).content)
Topic.serialize(:content)
end
def test_quote
content = "\\ \001 ' \n \\n \""
topic = Topic.create('content' => content)
assert_equal content, Topic.find(topic.id).content
end
end

View File

@@ -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

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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')

View File

@@ -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;"
)

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,8 @@
signals37:
id: 1
firm_id: 1
credit_limit: 50
unknown:
id: 2
credit_limit: 50

4
activerecord/test/fixtures/auto_id.rb vendored Normal file
View File

@@ -0,0 +1,4 @@
class AutoId < ActiveRecord::Base
def self.table_name () "auto_id_tests" end
def self.primary_key () "auto_id" end
end

View File

@@ -0,0 +1 @@
1b => 1

View File

@@ -0,0 +1 @@
a b => 1

View File

@@ -0,0 +1,3 @@
a => 1
b => 2

View File

@@ -0,0 +1,3 @@
a => 1
b => 2
a => 3

View File

@@ -0,0 +1 @@
a =>

View File

@@ -0,0 +1,3 @@
class ColumnName < ActiveRecord::Base
def self.table_name () "colnametests" end
end

View File

@@ -0,0 +1,6 @@
id => 2
type => Client
firm_id => 1
client_of => 2
name => Summit
ruby_type => Client

View File

@@ -0,0 +1,4 @@
id => 1
type => Firm
name => 37signals
ruby_type => Firm

View File

@@ -0,0 +1,6 @@
id => 3
type => Client
firm_id => 1
client_of => 1
name => Microsoft
ruby_type => Client

37
activerecord/test/fixtures/company.rb vendored Executable file
View File

@@ -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

View File

@@ -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

3
activerecord/test/fixtures/course.rb vendored Normal file
View File

@@ -0,0 +1,3 @@
class Course < ActiveRecord::Base
has_many :entrants
end

View File

@@ -0,0 +1,2 @@
id => 2
name => Java Development

View File

@@ -0,0 +1,2 @@
id => 1
name => Ruby Development

30
activerecord/test/fixtures/customer.rb vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,6 @@
id => 1
name => David
balance => 50
address_street => Funny Street
address_city => Scary Town
address_country => Loony Land

View File

@@ -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`)
);

View File

@@ -0,0 +1,4 @@
CREATE TABLE `courses` (
`id` INTEGER NOT NULL PRIMARY KEY,
`name` VARCHAR(255) NOT NULL
);

View File

@@ -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
);

View File

@@ -0,0 +1,4 @@
CREATE TABLE courses (
id serial,
name text
);

View File

@@ -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
);

View File

@@ -0,0 +1,4 @@
CREATE TABLE 'courses' (
'id' INTEGER NOT NULL PRIMARY KEY,
'name' VARCHAR(255) NOT NULL
);

View File

@@ -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)
);

View File

@@ -0,0 +1,4 @@
CREATE TABLE courses (
id int NOT NULL PRIMARY KEY,
name varchar(255) NOT NULL
);

2
activerecord/test/fixtures/default.rb vendored Normal file
View File

@@ -0,0 +1,2 @@
class Default < ActiveRecord::Base
end

View File

@@ -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

View File

@@ -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 %>

View File

@@ -0,0 +1,3 @@
developer_id => 1
project_id => 2
joined_on => 2004-10-10

View File

@@ -0,0 +1,3 @@
developer_id => 1
project_id => 1
joined_on => 2004-10-10

View File

@@ -0,0 +1,2 @@
developer_id => 2
project_id => 1

3
activerecord/test/fixtures/entrant.rb vendored Normal file
View File

@@ -0,0 +1,3 @@
class Entrant < ActiveRecord::Base
belongs_to :course
end

View File

@@ -0,0 +1,3 @@
id => 1
course_id => 1
name => Ruby Developer

View File

@@ -0,0 +1,3 @@
id => 2
course_id => 1
name => Ruby Guru

View File

@@ -0,0 +1,3 @@
id => 3
course_id => 2
name => Java Lover

5
activerecord/test/fixtures/movie.rb vendored Normal file
View File

@@ -0,0 +1,5 @@
class Movie < ActiveRecord::Base
def self.primary_key
"movieid"
end
end

View File

@@ -0,0 +1,2 @@
movieid => 1
name => Terminator

View File

@@ -0,0 +1,2 @@
movieid => 2
name => Gladiator

4
activerecord/test/fixtures/project.rb vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,2 @@
id => 2
name => Active Controller

View File

@@ -0,0 +1,2 @@
id => 1
name => Active Record

21
activerecord/test/fixtures/reply.rb vendored Executable file
View File

@@ -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

View File

@@ -0,0 +1,5 @@
class Subscriber < ActiveRecord::Base
def self.primary_key
"nick"
end
end

View File

@@ -0,0 +1,2 @@
nick => alterself
name => Luke Holden

View File

@@ -0,0 +1,2 @@
nick => webster132
name => David Heinemeier Hansson

20
activerecord/test/fixtures/topic.rb vendored Executable file
View File

@@ -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

9
activerecord/test/fixtures/topics/first vendored Executable file
View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More