aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG7
-rwxr-xr-xactiverecord/lib/active_record/associations.rb72
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb4
-rwxr-xr-xactiverecord/lib/active_record/base.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb72
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb10
-rw-r--r--activerecord/lib/active_record/session_store.rb4
-rw-r--r--activerecord/lib/active_record/timestamp.rb48
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb6
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb5
-rwxr-xr-xactiverecord/test/cases/base_test.rb4
-rw-r--r--activerecord/test/cases/copy_table_test_sqlite.rb10
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb10
-rw-r--r--activerecord/test/cases/schema_test_postgresql.rb76
-rw-r--r--activerecord/test/cases/timestamp_test.rb75
-rw-r--r--activerecord/test/models/author.rb2
-rw-r--r--activerecord/test/models/company.rb13
-rw-r--r--activerecord/test/models/pet.rb2
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--activerecord/test/schema/schema.rb8
21 files changed, 373 insertions, 86 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index c73ac4649e..d58b44144b 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,3 +1,10 @@
+*Edge*
+
+* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH]
+
+* Added ActiveRecord::Base#touch to update the updated_at/on attributes (or another specified timestamp) with the current time [DHH]
+
+
*2.3.2 [Final] (March 15, 2009)*
* Added ActiveRecord::Base.find_each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 6d25b36aea..2115878e32 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -981,6 +981,9 @@ module ActiveRecord
# If false, don't validate the associated objects when saving the parent object. +false+ by default.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
+ # [:touch]
+ # If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or
+ # destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -990,6 +993,8 @@ module ActiveRecord
# belongs_to :attachable, :polymorphic => true
# belongs_to :project, :readonly => true
# belongs_to :post, :counter_cache => true
+ # belongs_to :company, :touch => true
+ # belongs_to :company, :touch => :employees_last_updated_at
def belongs_to(association_id, options = {})
reflection = create_belongs_to_reflection(association_id, options)
@@ -1001,28 +1006,8 @@ module ActiveRecord
association_constructor_method(:create, reflection, BelongsToAssociation)
end
- # Create the callbacks to update counter cache
- if options[:counter_cache]
- cache_column = reflection.counter_cache_column
-
- method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = send(reflection.name)
- association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
- end
- after_create method_name
-
- method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = send(reflection.name)
- association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
- end
- before_destroy method_name
-
- module_eval(
- "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
- )
- end
+ add_counter_cache_callbacks(reflection) if options[:counter_cache]
+ add_touch_callbacks(reflection, options[:touch]) if options[:touch]
configure_dependency_for_belongs_to(reflection)
end
@@ -1329,6 +1314,43 @@ module ActiveRecord
end
end
+ def add_counter_cache_callbacks(reflection)
+ cache_column = reflection.counter_cache_column
+
+ method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
+ define_method(method_name) do
+ association = send(reflection.name)
+ association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
+ end
+ after_create(method_name)
+
+ method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
+ define_method(method_name) do
+ association = send(reflection.name)
+ association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
+ end
+ before_destroy(method_name)
+
+ module_eval(
+ "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
+ )
+ end
+
+ def add_touch_callbacks(reflection, touch_attribute)
+ method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym
+ define_method(method_name) do
+ association = send(reflection.name)
+
+ if touch_attribute == true
+ association.touch unless association.nil?
+ else
+ association.touch(touch_attribute) unless association.nil?
+ end
+ end
+ after_save(method_name)
+ after_destroy(method_name)
+ end
+
def find_with_associations(options = {})
catch :invalid_query do
join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
@@ -1353,7 +1375,7 @@ module ActiveRecord
dependent_conditions = []
dependent_conditions << "#{reflection.primary_key_name} = \#{record.quoted_id}"
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
- dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions]
+ dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
dependent_conditions << extra_conditions if extra_conditions
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
dependent_conditions = dependent_conditions.gsub('@', '\@')
@@ -1499,7 +1521,7 @@ module ActiveRecord
@@valid_keys_for_belongs_to_association = [
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
- :validate
+ :validate, :touch
]
def create_belongs_to_reflection(association_id, options)
@@ -2101,7 +2123,7 @@ module ActiveRecord
klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref|
- join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]
+ join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))} " if ref && ref.options[:conditions]
end
join
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 676c4ace61..241b9bfee0 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -169,8 +169,8 @@ module ActiveRecord
end
# Forwards the call to the reflection class.
- def sanitize_sql(sql)
- @reflection.klass.send(:sanitize_sql, sql)
+ def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name)
+ @reflection.klass.send(:sanitize_sql, sql, table_name)
end
# Assigns the ID of the owner to the corresponding foreign key in +record+.
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 2a5385119d..251d5000f5 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -810,25 +810,28 @@ module ActiveRecord #:nodoc:
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks.
+ # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
+ # or validations.
#
# ==== Parameters
#
- # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
- # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
+ # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro.
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
#
- # # Update all billing objects with the 3 different attributes given
- # Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" )
+ # # Update all customers with the given attributes
+ # Customer.update_all :wants_email => true
#
- # # Update records that match our conditions
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" )
+ # # Update all books with 'Rails' in their title
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'"
#
- # # Update records that match our conditions but limit it to 5 ordered by date
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'",
- # :order => 'created_at', :limit => 5 )
+ # # Update all avatars migrated more than a week ago
+ # Avatar.update_all ['migrated_at = ?, Time.now.utc], ['migrated_at > ?', 1.week.ago]
+ #
+ # # Update all books that match our conditions, but limit it to 5 ordered by date
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
def update_all(updates, conditions = nil, options = {})
sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} "
@@ -2228,12 +2231,12 @@ module ActiveRecord #:nodoc:
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
# { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition)
+ def sanitize_sql_for_conditions(condition, table_name = quoted_table_name)
return nil if condition.blank?
case condition
when Array; sanitize_sql_array(condition)
- when Hash; sanitize_sql_hash_for_conditions(condition)
+ when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
else condition
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 913bb521ca..4961793866 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -392,9 +392,28 @@ module ActiveRecord
quote_string(s)
end
+ # Checks the following cases:
+ #
+ # - table_name
+ # - "table.name"
+ # - schema_name.table_name
+ # - schema_name."table.name"
+ # - "schema.name".table_name
+ # - "schema.name"."table.name"
+ def quote_table_name(name)
+ schema, name_part = extract_pg_identifier_from_name(name.to_s)
+
+ unless name_part
+ quote_column_name(schema)
+ else
+ table_name, name_part = extract_pg_identifier_from_name(name_part)
+ "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
+ end
+ end
+
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
- %("#{name}")
+ PGconn.quote_ident(name.to_s)
end
# Quote date/time values for use in SQL input. Includes microseconds
@@ -621,33 +640,36 @@ module ActiveRecord
def indexes(table_name, name = nil)
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
result = query(<<-SQL, name)
- SELECT distinct i.relname, d.indisunique, a.attname
- FROM pg_class t, pg_class i, pg_index d, pg_attribute a
+ SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
+ FROM pg_class t, pg_class i, pg_index d
WHERE i.relkind = 'i'
AND d.indexrelid = i.oid
AND d.indisprimary = 'f'
AND t.oid = d.indrelid
AND t.relname = '#{table_name}'
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
- AND a.attrelid = t.oid
- AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum
- OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum
- OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum
- OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum
- OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum )
ORDER BY i.relname
SQL
- current_index = nil
+
indexes = []
- result.each do |row|
- if current_index != row[0]
- indexes << IndexDefinition.new(table_name, row[0], row[1] == "t", [])
- current_index = row[0]
- end
+ indexes = result.map do |row|
+ index_name = row[0]
+ unique = row[1] == 't'
+ indkey = row[2].split(" ")
+ oid = row[3]
+
+ columns = query(<<-SQL, "Columns for index #{row[0]} on #{table_name}").inject({}) {|attlist, r| attlist[r[1]] = r[0]; attlist}
+ SELECT a.attname, a.attnum
+ FROM pg_attribute a
+ WHERE a.attrelid = #{oid}
+ AND a.attnum IN (#{indkey.join(",")})
+ SQL
+
+ column_names = indkey.map {|attnum| columns[attnum] }
+ IndexDefinition.new(table_name, index_name, unique, column_names)
- indexes.last.columns << row[2]
end
indexes
@@ -745,7 +767,7 @@ module ActiveRecord
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
AND cons.contype = 'p'
- AND dep.refobjid = '#{table}'::regclass
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
if result.nil? or result.empty?
@@ -764,7 +786,7 @@ module ActiveRecord
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
- WHERE t.oid = '#{table}'::regclass
+ WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
AND def.adsrc ~* 'nextval'
end_sql
@@ -839,7 +861,7 @@ module ActiveRecord
# Drops an index from a table.
def remove_index(table_name, options = {})
- execute "DROP INDEX #{index_name(table_name, options)}"
+ execute "DROP INDEX #{quote_table_name(index_name(table_name, options))}"
end
# Maps logical Rails types to PostgreSQL-specific data types.
@@ -1040,11 +1062,21 @@ module ActiveRecord
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attrelid = '#{table_name}'::regclass
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
end_sql
end
+
+ def extract_pg_identifier_from_name(name)
+ match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
+
+ if match_data
+ rest = name[match_data[0].length..-1]
+ rest = rest[1..-1] if rest[0,1] == "."
+ [match_data[1], (rest.length > 0 ? rest : nil)]
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index afd6472db8..05334a830a 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -150,6 +150,16 @@ module ActiveRecord
%Q("#{name}")
end
+ # Quote date/time values for use in SQL input. Includes microseconds
+ # if the value is a Time responding to usec.
+ def quoted_date(value) #:nodoc:
+ if value.acts_like?(:time) && value.respond_to?(:usec)
+ "#{super}.#{sprintf("%06d", value.usec)}"
+ else
+ super
+ end
+ end
+
# DATABASE STATEMENTS ======================================
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index 3cc4640f42..21471da419 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -40,7 +40,7 @@ module ActiveRecord
#
# The example SqlBypass class is a generic SQL session store. You may
# use it as a basis for high-performance database-specific stores.
- class SessionStore < ActionController::Session::AbstractStore
+ class SessionStore < ActionDispatch::Session::AbstractStore
# The default Active Record class.
class Session < ActiveRecord::Base
##
@@ -184,7 +184,7 @@ module ActiveRecord
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
- if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
+ if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 8dbe80a01a..d9e1ef351f 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -15,27 +15,57 @@ module ActiveRecord
base.class_inheritable_accessor :record_timestamps, :instance_writer => false
base.record_timestamps = true
end
+
+ # Saves the record with the updated_at/on attributes set to the current time.
+ # If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised.
+ # If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes.
+ #
+ # Examples:
+ #
+ # product.touch # updates updated_at
+ # product.touch(:designed_at) # updates the designed_at attribute
+ def touch(attribute = nil)
+ current_time = current_time_from_proper_timezone
+
+ if attribute
+ write_attribute(attribute, current_time)
+ else
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at)
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on)
+ end
+
+ save!
+ end
+
private
def create_with_timestamps #:nodoc:
if record_timestamps
- t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
- write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
- write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
+ current_time = current_time_from_proper_timezone
- write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil?
- write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil?
+ write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
+ write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
+
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
end
+
create_without_timestamps
end
def update_with_timestamps(*args) #:nodoc:
if record_timestamps && (!partial_updates? || changed?)
- t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
- write_attribute('updated_at', t) if respond_to?(:updated_at)
- write_attribute('updated_on', t) if respond_to?(:updated_on)
+ current_time = current_time_from_proper_timezone
+
+ write_attribute('updated_at', current_time) if respond_to?(:updated_at)
+ write_attribute('updated_on', current_time) if respond_to?(:updated_on)
end
+
update_without_timestamps(*args)
end
+
+ def current_time_from_proper_timezone
+ self.class.default_timezone == :utc ? Time.now.utc : Time.now
+ end
end
-end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 0b6e52c79b..b059eb7f6f 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -175,6 +175,8 @@ module ActiveRecord
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
+ #
+ # Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(options = {}, &block)
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 30edf79a26..5df74fcdcd 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -719,6 +719,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert Client.find(:all, :conditions => "firm_id=#{firm.id}").empty?
end
+ def test_dependence_for_associations_with_hash_condition
+ david = authors(:david)
+ post = posts(:thinking).id
+ assert_difference('Post.count', -1) { assert david.destroy }
+ end
+
def test_destroy_dependent_when_deleted_from_association
firm = Firm.find(:first)
assert_equal 2, firm.clients.size
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index f87c914125..7141531740 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -29,6 +29,11 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?.*TERMINATING_MARKER/, sql
end
+ def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
+ result = Author.find(:all, :joins => [:thinking_posts, :welcome_posts])
+ assert_equal authors(:david), result.first
+ end
+
def test_construct_finder_sql_unpacks_nested_joins
sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]})
assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 99d77961fc..d97cd17d75 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -456,7 +456,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLiteAdapter)
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -1756,7 +1756,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_scoped_find_with_group_and_having
- developers = Developer.with_scope(:find => { :group => 'salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
+ developers = Developer.with_scope(:find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
Developer.find(:all)
end
assert_equal 3, developers.size
diff --git a/activerecord/test/cases/copy_table_test_sqlite.rb b/activerecord/test/cases/copy_table_test_sqlite.rb
index 72bd7e2dab..de8af30997 100644
--- a/activerecord/test/cases/copy_table_test_sqlite.rb
+++ b/activerecord/test/cases/copy_table_test_sqlite.rb
@@ -10,7 +10,7 @@ class CopyTableTest < ActiveRecord::TestCase
end
end
- def test_copy_table(from = 'companies', to = 'companies2', options = {})
+ def test_copy_table(from = 'customers', to = 'customers2', options = {})
assert_nothing_raised {copy_table(from, to, options)}
assert_equal row_count(from), row_count(to)
@@ -24,11 +24,11 @@ class CopyTableTest < ActiveRecord::TestCase
end
def test_copy_table_renaming_column
- test_copy_table('companies', 'companies2',
- :rename => {'client_of' => 'fan_of'}) do |from, to, options|
- expected = column_values(from, 'client_of')
+ test_copy_table('customers', 'customers2',
+ :rename => {'name' => 'person_name'}) do |from, to, options|
+ expected = column_values(from, 'name')
assert expected.any?, 'only nils in resultset; real values are needed'
- assert_equal expected, column_values(to, 'fan_of')
+ assert_equal expected, column_values(to, 'person_name')
end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 17e4c755ce..f9ad7f3ba3 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -22,6 +22,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_no_match %r{create_table "sqlite_sequence"}, output
end
+ def test_schema_dump_includes_camelcase_table_name
+ output = standard_dump
+ assert_match %r{create_table "CamelCase"}, output
+ end
+
def assert_line_up(lines, pattern, required = false)
return assert(true) if lines.empty?
matches = lines.map { |line| line.match(pattern) }
@@ -147,6 +152,11 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
+ def test_schema_dumps_index_columns_in_right_order
+ index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
+ assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition
+ end
+
if current_adapter?(:MysqlAdapter)
def test_schema_dump_should_not_add_default_value_for_mysql_text_field
output = standard_dump
diff --git a/activerecord/test/cases/schema_test_postgresql.rb b/activerecord/test/cases/schema_test_postgresql.rb
index 336a38765c..a294848fa3 100644
--- a/activerecord/test/cases/schema_test_postgresql.rb
+++ b/activerecord/test/cases/schema_test_postgresql.rb
@@ -6,6 +6,7 @@ class SchemaTest < ActiveRecord::TestCase
SCHEMA_NAME = 'test_schema'
SCHEMA2_NAME = 'test_schema2'
TABLE_NAME = 'things'
+ CAPITALIZED_TABLE_NAME = 'Things'
INDEX_A_NAME = 'a_index_things_on_name'
INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema'
INDEX_A_COLUMN = 'name'
@@ -18,9 +19,27 @@ class SchemaTest < ActiveRecord::TestCase
'moment timestamp without time zone default now()'
]
+ class Thing1 < ActiveRecord::Base
+ set_table_name "test_schema.things"
+ end
+
+ class Thing2 < ActiveRecord::Base
+ set_table_name "test_schema2.things"
+ end
+
+ class Thing3 < ActiveRecord::Base
+ set_table_name 'test_schema."things.table"'
+ end
+
+ class Thing4 < ActiveRecord::Base
+ set_table_name 'test_schema."Things"'
+ end
+
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
+ @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})"
+ @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{CAPITALIZED_TABLE_NAME}\" (#{COLUMNS.join(',')})"
@connection.execute "CREATE SCHEMA #{SCHEMA2_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});"
@@ -39,6 +58,12 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+ def test_with_schema_prefixed_capitalized_table_name
+ assert_nothing_raised do
+ assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{CAPITALIZED_TABLE_NAME}")
+ end
+ end
+
def test_with_schema_search_path
assert_nothing_raised do
with_schema_search_path(SCHEMA_NAME) do
@@ -47,6 +72,47 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+
+ def test_proper_encoding_of_table_name
+ assert_equal '"table_name"', @connection.quote_table_name('table_name')
+ assert_equal '"table.name"', @connection.quote_table_name('"table.name"')
+ assert_equal '"schema_name"."table_name"', @connection.quote_table_name('schema_name.table_name')
+ assert_equal '"schema_name"."table.name"', @connection.quote_table_name('schema_name."table.name"')
+ assert_equal '"schema.name"."table_name"', @connection.quote_table_name('"schema.name".table_name')
+ assert_equal '"schema.name"."table.name"', @connection.quote_table_name('"schema.name"."table.name"')
+ end
+
+ def test_classes_with_qualified_schema_name
+ assert_equal 0, Thing1.count
+ assert_equal 0, Thing2.count
+ assert_equal 0, Thing3.count
+ assert_equal 0, Thing4.count
+
+ Thing1.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 0, Thing2.count
+ assert_equal 0, Thing3.count
+ assert_equal 0, Thing4.count
+
+ Thing2.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 1, Thing2.count
+ assert_equal 0, Thing3.count
+ assert_equal 0, Thing4.count
+
+ Thing3.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 1, Thing2.count
+ assert_equal 1, Thing3.count
+ assert_equal 0, Thing4.count
+
+ Thing4.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 1, Thing2.count
+ assert_equal 1, Thing3.count
+ assert_equal 1, Thing4.count
+ end
+
def test_raise_on_unquoted_schema_name
assert_raise(ActiveRecord::StatementInvalid) do
with_schema_search_path '$user,public'
@@ -69,6 +135,16 @@ class SchemaTest < ActiveRecord::TestCase
do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2)
end
+ def test_with_uppercase_index_name
+ ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_nothing_raised { ActiveRecord::Base.connection.remove_index :things, :name => "#{SCHEMA_NAME}.things_Index"}
+
+ ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ ActiveRecord::Base.connection.schema_search_path = SCHEMA_NAME
+ assert_nothing_raised { ActiveRecord::Base.connection.remove_index :things, :name => "things_Index"}
+ ActiveRecord::Base.connection.schema_search_path = "public"
+ end
+
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
new file mode 100644
index 0000000000..24b237a72b
--- /dev/null
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -0,0 +1,75 @@
+require 'cases/helper'
+require 'models/developer'
+require 'models/owner'
+require 'models/pet'
+
+class TimestampTest < ActiveRecord::TestCase
+ fixtures :developers, :owners, :pets
+
+ def setup
+ @developer = Developer.first
+ @previously_updated_at = @developer.updated_at
+ end
+
+ def test_saving_a_changed_record_updates_its_timestamp
+ @developer.name = "Jack Bauer"
+ @developer.save!
+
+ assert @previously_updated_at != @developer.updated_at
+ end
+
+ def test_saving_a_unchanged_record_doesnt_update_its_timestamp
+ @developer.save!
+
+ assert @previously_updated_at == @developer.updated_at
+ end
+
+ def test_touching_a_record_updates_its_timestamp
+ @developer.touch
+
+ assert @previously_updated_at != @developer.updated_at
+ end
+
+ def test_touching_a_different_attribute
+ previously_created_at = @developer.created_at
+ @developer.touch(:created_at)
+
+ assert previously_created_at != @developer.created_at
+ end
+
+ def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
+ pet = Pet.first
+ owner = pet.owner
+ previously_owner_updated_at = owner.updated_at
+
+ pet.name = "Fluffy the Third"
+ pet.save
+
+ assert previously_owner_updated_at != pet.owner.updated_at
+ end
+
+ def test_destroying_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
+ pet = Pet.first
+ owner = pet.owner
+ previously_owner_updated_at = owner.updated_at
+
+ pet.destroy
+
+ assert previously_owner_updated_at != pet.owner.updated_at
+ end
+
+ def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
+ Pet.belongs_to :owner, :touch => :happy_at
+
+ pet = Pet.first
+ owner = pet.owner
+ previously_owner_happy_at = owner.happy_at
+
+ pet.name = "Fluffy the Third"
+ pet.save
+
+ assert previously_owner_happy_at != pet.owner.happy_at
+ ensure
+ Pet.belongs_to :owner, :touch => true
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 4ffac4fe32..0d9ee36b20 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -25,6 +25,8 @@ class Author < ActiveRecord::Base
has_many :comments_with_order_and_conditions, :through => :posts, :source => :comments, :order => 'comments.body', :conditions => "comments.body like 'Thank%'"
has_many :comments_with_include, :through => :posts, :source => :comments, :include => :post
+ has_many :thinking_posts, :class_name => 'Post', :conditions => { :title => 'So I was thinking' }, :dependent => :delete_all
+ has_many :welcome_posts, :class_name => 'Post', :conditions => { :title => 'Welcome to the weblog' }
has_many :comments_desc, :through => :posts, :source => :comments, :order => 'comments.id DESC'
has_many :limited_comments, :through => :posts, :source => :comments, :limit => 1
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 9c9d03dd5b..4c2514b0b3 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -78,13 +78,6 @@ class DependentFirm < Company
has_many :companies, :foreign_key => 'client_of', :order => "id", :dependent => :nullify
end
-class ExclusivelyDependentFirm < Company
- has_one :account, :foreign_key => "firm_id", :dependent => :delete
- has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'"
- has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.']
- has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'}
-end
-
class Client < Company
belongs_to :firm, :foreign_key => "client_of"
belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id"
@@ -125,6 +118,12 @@ class Client < Company
end
end
+class ExclusivelyDependentFirm < Company
+ has_one :account, :foreign_key => "firm_id", :dependent => :delete
+ has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'"
+ has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.']
+ has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'}
+end
class SpecialClient < Client
end
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index dc1a3c5e94..a8bf94dd86 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -1,5 +1,5 @@
class Pet < ActiveRecord::Base
set_primary_key :pet_id
- belongs_to :owner
+ belongs_to :owner, :touch => true
has_many :toys
end
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index 550d4ae23c..f25b2ddf77 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -13,7 +13,7 @@ class Project < ActiveRecord::Base
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
:after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"}
- has_and_belongs_to_many :well_payed_salary_groups, :class_name => "Developer", :group => "salary", :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary"
+ has_and_belongs_to_many :well_payed_salary_groups, :class_name => "Developer", :group => "developers.salary", :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary"
attr_accessor :developers_log
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index ea848a2940..98e6d192a5 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -68,6 +68,10 @@ ActiveRecord::Schema.define do
t.boolean :value
end
+ create_table "CamelCase", :force => true do |t|
+ t.string :name
+ end
+
create_table :categories, :force => true do |t|
t.string :name, :null => false
t.string :type
@@ -114,6 +118,8 @@ ActiveRecord::Schema.define do
t.integer :rating, :default => 1
end
+ add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index"
+
create_table :computers, :force => true do |t|
t.integer :developer, :null => false
t.integer :extendedWarranty, :null => false
@@ -281,6 +287,8 @@ ActiveRecord::Schema.define do
create_table :owners, :primary_key => :owner_id ,:force => true do |t|
t.string :name
+ t.column :updated_at, :datetime
+ t.column :happy_at, :datetime
end