TeX and CSS tweaks.

Sync with latest Instiki Trunk
(Updates Rails to 1.2.2)
This commit is contained in:
Jacques Distler 2007-02-09 02:04:31 -06:00
parent 0ac586ee25
commit c358389f25
443 changed files with 24218 additions and 9823 deletions

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004 David Heinemeier Hansson
# Copyright (c) 2004-2006 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -30,7 +30,7 @@ unless defined?(ActiveSupport)
require 'active_support'
rescue LoadError
require 'rubygems'
require_gem 'activesupport'
gem 'activesupport'
end
end
@ -46,14 +46,18 @@ require 'active_record/timestamp'
require 'active_record/acts/list'
require 'active_record/acts/tree'
require 'active_record/acts/nested_set'
require 'active_record/locking'
require 'active_record/locking/optimistic'
require 'active_record/locking/pessimistic'
require 'active_record/migration'
require 'active_record/schema'
require 'active_record/calculations'
require 'active_record/xml_serialization'
require 'active_record/attribute_methods'
ActiveRecord::Base.class_eval do
include ActiveRecord::Validations
include ActiveRecord::Locking
include ActiveRecord::Locking::Optimistic
include ActiveRecord::Locking::Pessimistic
include ActiveRecord::Callbacks
include ActiveRecord::Observing
include ActiveRecord::Timestamp
@ -65,10 +69,12 @@ ActiveRecord::Base.class_eval do
include ActiveRecord::Acts::List
include ActiveRecord::Acts::NestedSet
include ActiveRecord::Calculations
include ActiveRecord::XmlSerialization
include ActiveRecord::AttributeMethods
end
unless defined?(RAILS_CONNECTION_ADAPTERS)
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase )
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase )
end
RAILS_CONNECTION_ADAPTERS.each do |adapter|

View file

@ -1,8 +1,7 @@
module ActiveRecord
module Acts #:nodoc:
module List #:nodoc:
def self.append_features(base)
super
def self.included(base)
base.extend(ClassMethods)
end
@ -78,7 +77,8 @@ module ActiveRecord
def insert_at(position = 1)
insert_at_position(position)
end
# Swap positions with the next lower item, if one exists.
def move_lower
return unless lower_item
@ -88,6 +88,7 @@ module ActiveRecord
end
end
# Swap positions with the next higher item, if one exists.
def move_higher
return unless higher_item
@ -97,6 +98,8 @@ module ActiveRecord
end
end
# Move to the bottom of the list. If the item is already in the list, the items below it have their
# position adjusted accordingly.
def move_to_bottom
return unless in_list?
acts_as_list_class.transaction do
@ -105,6 +108,8 @@ module ActiveRecord
end
end
# Move to the top of the list. If the item is already in the list, the items above it have their
# position adjusted accordingly.
def move_to_top
return unless in_list?
acts_as_list_class.transaction do
@ -112,31 +117,36 @@ module ActiveRecord
assume_top_position
end
end
def remove_from_list
decrement_positions_on_lower_items if in_list?
end
# Increase the position of this item without adjusting the rest of the list.
def increment_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i + 1
end
# Decrease the position of this item without adjusting the rest of the list.
def decrement_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i - 1
end
# Return true if this object is the first in the list.
def first?
return false unless in_list?
self.send(position_column) == 1
end
# Return true if this object is the last in the list.
def last?
return false unless in_list?
self.send(position_column) == bottom_position_in_list
end
# Return the next higher item in the list.
def higher_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
@ -144,6 +154,7 @@ module ActiveRecord
)
end
# Return the next lower item in the list.
def lower_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>

View file

@ -1,8 +1,7 @@
module ActiveRecord
module Acts #:nodoc:
module NestedSet #:nodoc:
def self.append_features(base)
super
def self.included(base)
base.extend(ClassMethods)
end
@ -164,9 +163,9 @@ module ActiveRecord
child[left_col_name] = right_bound
child[right_col_name] = right_bound + 1
self[right_col_name] += 2
self.class.transaction {
self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
self.class.base_class.transaction {
self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" )
self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" )
self.save
child.save
}
@ -181,17 +180,17 @@ module ActiveRecord
# Returns a set of itself and all of its nested children
def full_set
self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
end
# Returns a set of all of its children and nested children
def all_children
self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
self.class.base_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
end
# Returns a set of only this entry's immediate children
def direct_children
self.class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}")
self.class.base_class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}")
end
# Prunes a branch off of the tree, shifting all of the elements on the right
@ -200,10 +199,10 @@ module ActiveRecord
return if self[right_col_name].nil? || self[left_col_name].nil?
dif = self[right_col_name] - self[left_col_name] + 1
self.class.transaction {
self.class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" )
self.class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" )
self.class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" )
self.class.base_class.transaction {
self.class.base_class.delete_all( "#{scope_condition} and #{left_col_name} > #{self[left_col_name]} and #{right_col_name} < #{self[right_col_name]}" )
self.class.base_class.update_all( "#{left_col_name} = (#{left_col_name} - #{dif})", "#{scope_condition} AND #{left_col_name} >= #{self[right_col_name]}" )
self.class.base_class.update_all( "#{right_col_name} = (#{right_col_name} - #{dif} )", "#{scope_condition} AND #{right_col_name} >= #{self[right_col_name]}" )
}
end
end

View file

@ -1,21 +1,20 @@
module ActiveRecord
module Acts #:nodoc:
module Tree #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
def self.included(base)
base.extend(ClassMethods)
end
# Specify this act if you want to model a tree structure by providing a parent association and a children
# Specify this act if you want to model a tree structure by providing a parent association and a children
# association. This act requires that you have a foreign key column, which by default is called parent_id.
#
#
# class Category < ActiveRecord::Base
# acts_as_tree :order => "name"
# end
#
# Example :
#
# Example:
# root
# \_ child1
# \_ child1
# \_ subchild1
# \_ subchild2
#
@ -28,7 +27,7 @@ module ActiveRecord
# root.children # => [child1]
# root.children.first.children.first # => subchild1
#
# In addition to the parent and children associations, the following instance methods are added to the class
# In addition to the parent and children associations, the following instance methods are added to the class
# after specifying the act:
# * siblings : Returns all the children of the parent, excluding the current node ([ subchild2 ] when called from subchild1)
# * self_and_siblings : Returns all the children of the parent, including the current node ([ subchild1, subchild2 ] when called from subchild1)
@ -49,7 +48,7 @@ module ActiveRecord
class_eval <<-EOV
include ActiveRecord::Acts::Tree::InstanceMethods
def self.roots
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
end
@ -67,13 +66,13 @@ module ActiveRecord
# subchild1.ancestors # => [child1, root]
def ancestors
node, nodes = self, []
nodes << node = node.parent until not node.has_parent?
nodes << node = node.parent while node.parent
nodes
end
def root
node = self
node = node.parent until not node.has_parent?
node = node.parent while node.parent
node
end
@ -82,7 +81,7 @@ module ActiveRecord
end
def self_and_siblings
has_parent? ? parent.children : self.class.roots
parent ? parent.children : self.class.roots
end
end
end

View file

@ -109,8 +109,8 @@ module ActiveRecord
# 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>.
# Adds reader and writer methods for manipulating a value object:
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
#
# Options are:
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
@ -118,49 +118,73 @@ module ActiveRecord
# 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.
# * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
# attributes are nil. Setting the aggregate class to nil has the effect of writing nil to all mapped attributes.
# This defaults to false.
#
# 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) ]
# composed_of :gps_location
# composed_of :gps_location, :allow_nil => true
#
def composed_of(part_id, options = {})
options.assert_valid_keys(:class_name, :mapping)
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
name = part_id.id2name
class_name = options[:class_name] || name_to_class_name(name)
mapping = options[:mapping] || [ name, name ]
class_name = options[:class_name] || name.camelize
mapping = options[:mapping] || [ name, name ]
allow_nil = options[:allow_nil] || false
reader_method(name, class_name, mapping)
writer_method(name, class_name, mapping)
reader_method(name, class_name, mapping, allow_nil)
writer_method(name, class_name, mapping, allow_nil)
create_reflection(:composed_of, part_id, options, self)
end
private
def name_to_class_name(name)
name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
end
def reader_method(name, class_name, mapping)
def reader_method(name, class_name, mapping, allow_nil)
mapping = (Array === mapping.first ? mapping : [ mapping ])
allow_nil_condition = if allow_nil
mapping.collect { |pair| "!read_attribute(\"#{pair.first}\").nil?"}.join(" && ")
else
"true"
end
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(", ")})
if (@#{name}.nil? || force_reload) && #{allow_nil_condition}
@#{name} = #{class_name}.new(#{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
def writer_method(name, class_name, mapping, allow_nil)
mapping = (Array === mapping.first ? mapping : [ mapping ])
if allow_nil
module_eval <<-end_eval
def #{name}=(part)
if part.nil?
#{mapping.collect { |pair| "@attributes[\"#{pair.first}\"] = nil" }.join("\n")}
else
@#{name} = part.freeze
#{mapping.collect { |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
end
end
end_eval
else
module_eval <<-end_eval
def #{name}=(part)
@#{name} = part.freeze
#{mapping.collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
end
end_eval
end
end
end
end

View file

@ -10,65 +10,54 @@ require 'active_record/deprecated_associations'
module ActiveRecord
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
@reflection = reflection
end
def message
"Could not find the association #{@reflection.options[:through].inspect} in model #{@reflection.klass}"
def initialize(owner_class_name, reflection)
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
end
end
class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
@owner_class_name = owner_class_name
@reflection = reflection
@source_reflection = source_reflection
end
def message
"Cannot have a has_many :through association '#{@owner_class_name}##{@reflection.name}' on the polymorphic object '#{@source_reflection.class_name}##{@source_reflection.name}'."
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
end
end
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
@reflection = reflection
@through_reflection = reflection.through_reflection
@source_reflection_names = reflection.source_reflection_names
@source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
end
def message
"Could not find the source association(s) #{@source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{@through_reflection.klass}. Try 'has_many #{@reflection.name.inspect}, :through => #{@through_reflection.name.inspect}, :source => <name>'. Is it one of #{@source_associations.to_sentence :connector => 'or'}?"
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :connector => 'or'}?")
end
end
class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc
class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
def initialize(reflection)
@reflection = reflection
@through_reflection = reflection.through_reflection
@source_reflection = reflection.source_reflection
through_reflection = reflection.through_reflection
source_reflection = reflection.source_reflection
super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
end
def message
"Invalid source reflection macro :#{@source_reflection.macro}#{" :through" if @source_reflection.options[:through]} for has_many #{@reflection.name.inspect}, :through => #{@through_reflection.name.inspect}. Use :source to specify the source reflection."
end
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
def initialize(owner, reflection)
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
end
end
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
@reflection = reflection
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
end
def message
"Can not eagerly load the polymorphic association #{@reflection.name.inspect}"
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
end
end
module Associations # :nodoc:
def self.append_features(base)
super
def self.included(base)
base.extend(ClassMethods)
end
@ -95,7 +84,7 @@ module ActiveRecord
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</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.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find(:all, options),</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>
@ -109,25 +98,27 @@ module ActiveRecord
# 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
# class User < ActiveRecord::Base
# # I reference an account.
# belongs_to :account
# end
#
# class Author < ActiveRecord::Base
# belongs_to :post
# class Account < ActiveRecord::Base
# # One user references me.
# has_one :user
# end
#
# The tables for these classes could look something like:
#
# CREATE TABLE posts (
# CREATE TABLE users (
# id int(11) NOT NULL auto_increment,
# title varchar default NULL,
# account_id int(11) default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE authors (
# CREATE TABLE accounts (
# id int(11) NOT NULL auto_increment,
# post_id int(11) default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
@ -215,6 +206,21 @@ module ActiveRecord
# has_many :people, :extend => FindOrCreateByNameExtension
# end
#
# If you need to use multiple named extension modules, you can specify an array of modules with the :extend option.
# In the case of name conflicts between methods in the modules, methods in modules later in the array supercede
# those earlier in the array. Example:
#
# class Account < ActiveRecord::Base
# has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
# end
#
# Some extensions can only be made to work with knowledge of the association proxy's internals.
# Extensions can access relevant state using accessors on the association proxy:
#
# * +proxy_owner+ - Returns the object the association is part of.
# * +proxy_reflection+ - Returns the reflection object that describes the association.
# * +proxy_target+ - Returns the associated object for belongs_to and has_one, or the collection of associated objects for has_many and has_and_belongs_to_many.
#
# === Association Join Models
#
# Has Many associations can be configured with the :through option to use an explicit join model to retrieve the data. This
@ -273,6 +279,30 @@ module ActiveRecord
# This works by using a type column in addition to a foreign key to specify the associated record. In the Asset example, you'd need
# an attachable_id integer column and an attachable_type string column.
#
# Using polymorphic associations in combination with single table inheritance (STI) is a little tricky. In order
# for the associations to work as expected, ensure that you store the base model for the STI models in the
# type column of the polymorphic association. To continue with the asset example above, suppose there are guest posts
# and member posts that use the posts table for STI. So there will be an additional 'type' column in the posts table.
#
# class Asset < ActiveRecord::Base
# belongs_to :attachable, :polymorphic => true
#
# def attachable_type=(sType)
# super(sType.to_s.classify.constantize.base_class.to_s)
# end
# end
#
# class Post < ActiveRecord::Base
# # because we store "Post" in attachable_type now :dependent => :destroy will work
# has_many :assets, :as => :attachable, :dependent => :destroy
# end
#
# class GuestPost < ActiveRecord::Base
# end
#
# class MemberPost < ActiveRecord::Base
# end
#
# == Caching
#
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
@ -319,22 +349,17 @@ module ActiveRecord
# But that shouldn't fool you to think that you can pull out huge amounts of data with no performance penalty just because you've reduced
# the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
# catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
#
# Since the eager loading pulls from multiple tables, you'll have to disambiguate any column references in both conditions and orders. So
# :order => "posts.id DESC" will work while :order => "id DESC" will not. Because eager loading generates the SELECT statement too, the
# :select option is ignored.
#
# Please note that limited eager loading with has_many and has_and_belongs_to_many associations is not compatible with describing conditions
# on these eager tables. This will work:
#
# Post.find(:all, :include => :comments, :conditions => "posts.title = 'magic forest'", :limit => 2)
#
# ...but this will not (and an ArgumentError will be raised):
#
# Post.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%'", :limit => 2)
#
# Also have in mind that since the eager loading is pulling from multiple tables, you'll have to disambiguate any column references
# in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that
# you alter the :order and :conditions on the association definitions themselves.
#
# It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will not pull
# additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading.
# You can use eager loading on multiple associations from the same table, but you cannot use those associations in orders and conditions
# as there is currently not any way to disambiguate them. Eager loading will not pull additional attributes on join tables, so "rich
# associations" with has_and_belongs_to_many are not a good fit for eager loading.
#
# When eager loaded, conditions are interpolated in the context of the model class, not the model instance. Conditions are lazily interpolated
# before the actual model exists.
#
# == Table Aliasing
#
@ -432,6 +457,7 @@ module ActiveRecord
# * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL.
# This will also destroy the objects if they're declared as belongs_to and dependent on this model.
# * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
# * <tt>collection_singular_ids</tt> - returns an array of the associated objects ids
# * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
# * <tt>collection.clear</tt> - removes every object from the collection. This destroys the associated objects if they
# are <tt>:dependent</tt>, deletes them directly from the database if they are <tt>:dependent => :delete_all</tt>,
@ -451,6 +477,7 @@ module ActiveRecord
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients=</tt>
# * <tt>Firm#client_ids</tt>
# * <tt>Firm#client_ids=</tt>
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
@ -502,6 +529,7 @@ module ActiveRecord
# * <tt>:source</tt>: Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either +:subscribers+ or
# +:subscriber+ on +Subscription+, unless a +:source+ is given.
# * <tt>:uniq</tt> - if set to true, duplicates will be omitted from the collection. Useful in conjunction with :through.
#
# Option examples:
# has_many :comments, :order => "posted_on"
@ -562,25 +590,28 @@ module ActiveRecord
# 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 :destroy (or true) all the associated objects are destroyed when this object is. Also,
# association is assigned.
# * <tt>:dependent</tt> - if set to :destroy (or true) the associated object is destroyed when this object is. If set to
# :delete the associated object is deleted *without* calling its destroy method. If set to :nullify the associated
# object's foreign key is set to NULL. Also, association is assigned.
# * <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.
# * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded.
#
# * <tt>:as</tt>: Specifies a polymorphic interface (See #belongs_to).
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
# has_one :credit_card, :dependent => :nullify # updates the associated records foriegn key value to null rather than destroying it
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
# has_one :attachment, :as => :attachable
def has_one(association_id, options = {})
reflection = create_has_one_reflection(association_id, options)
module_eval do
after_save <<-EOF
association = instance_variable_get("@#{reflection.name}")
unless association.nil?
if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
association["#{reflection.primary_key_name}"] = id
association.save(true)
end
@ -644,6 +675,12 @@ module ActiveRecord
# :conditions => 'discounts > #{payments_count}'
# belongs_to :attachable, :polymorphic => true
def belongs_to(association_id, options = {})
if options.include?(:class_name) && !options.include?(:foreign_key)
::ActiveSupport::Deprecation.warn(
"The inferred foreign_key name will change in Rails 2.0 to use the association name instead of its class name when they differ. When using :class_name in belongs_to, use the :foreign_key option to explicitly set the key name to avoid problems in the transition.",
caller)
end
reflection = create_belongs_to_reflection(association_id, options)
if reflection.options[:polymorphic]
@ -652,7 +689,7 @@ module ActiveRecord
module_eval do
before_save <<-EOF
association = instance_variable_get("@#{reflection.name}")
if !association.nil?
if association && association.target
if association.new_record?
association.save(true)
end
@ -708,7 +745,13 @@ module ActiveRecord
# 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".
# will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence
# is calculated using the <tt><</tt> operator for <tt>String</tt>. This means that if the strings are of different lengths,
# and the strings are equal when compared up to the shortest length, then the longer string is considered of higher
# lexical precedence than the shorter one. For example, one would expect the tables <tt>paper_boxes</tt> and <tt>papers</tt>
# to generate a join table name of <tt>papers_paper_boxes</tt> because of the length of the name <tt>paper_boxes</tt>,
# but it in fact generates a join table name of <tt>paper_boxes_papers</tt>. Be aware of this caveat, and use the
# custom <tt>join_table</tt> option if you need to.
#
# Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through
# has_and_belongs_to_many associations. Records returned from join tables with additional attributes will be marked as
@ -729,24 +772,31 @@ module ActiveRecord
# * <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=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
# * <tt>collection_singular_ids</tt> - returns an array of the associated objects ids
# * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
# * <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.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table 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 the join table and that has already been saved (if it passed the validation).
#
# 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.push_with_attributes</tt>
# * <tt>Developer#projects.delete</tt>
# * <tt>Developer#projects=</tt>
# * <tt>Developer#project_ids</tt>
# * <tt>Developer#project_ids=</tt>
# * <tt>Developer#projects.clear</tt>
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
# The declaration may include an options hash to specialize the behavior of the association.
#
# Options are:
@ -831,14 +881,14 @@ module ActiveRecord
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
retval = association.reload
unless retval.nil?
instance_variable_set("@#{reflection.name}", association)
else
if retval.nil? and association_proxy_class == BelongsToAssociation
instance_variable_set("@#{reflection.name}", nil)
return nil
end
instance_variable_set("@#{reflection.name}", association)
end
association
association.target.nil? ? nil : association
end
define_method("#{reflection.name}=") do |new_value|
@ -860,7 +910,7 @@ module ActiveRecord
end
define_method("set_#{reflection.name}_target") do |target|
return if target.nil?
return if target.nil? and association_proxy_class == BelongsToAssociation
association = association_proxy_class.new(self, reflection)
association.target = target
instance_variable_set("@#{reflection.name}", association)
@ -887,22 +937,20 @@ module ActiveRecord
collection_reader_method(reflection, association_proxy_class)
define_method("#{reflection.name}=") do |new_value|
association = instance_variable_get("@#{reflection.name}")
unless association.respond_to?(:loaded?)
association = association_proxy_class.new(self, reflection)
instance_variable_set("@#{reflection.name}", association)
end
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
association = send(reflection.name)
association.replace(new_value)
association
end
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
send("#{reflection.name}=", reflection.class_name.constantize.find(new_value))
define_method("#{reflection.name.to_s.singularize}_ids") do
send(reflection.name).map(&:id)
end
end
def require_association_class(class_name)
require_association(Inflector.underscore(class_name)) if class_name
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
ids = (new_value || []).reject { |nid| nid.blank? }
send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
end
end
def add_multiple_associated_save_callbacks(association_name)
@ -961,14 +1009,6 @@ module ActiveRecord
end
end
def count_with_associations(options = {})
catch :invalid_query do
join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
return count_by_sql(construct_counter_sql_with_included_associations(options, join_dependency))
end
0
end
def find_with_associations(options = {})
catch :invalid_query do
join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
@ -979,13 +1019,17 @@ module ActiveRecord
end
def configure_dependency_for_has_many(reflection)
if reflection.options[:dependent] == true
::ActiveSupport::Deprecation.warn("The :dependent => true option is deprecated and will be removed from Rails 2.0. Please use :dependent => :destroy instead. See http://www.rubyonrails.org/deprecation for details.", caller)
end
if reflection.options[:dependent] && reflection.options[:exclusively_dependent]
raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
end
if reflection.options[:exclusively_dependent]
reflection.options[:dependent] = :delete_all
#warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
::ActiveSupport::Deprecation.warn("The :exclusively_dependent option is deprecated and will be removed from Rails 2.0. Please use :dependent => :delete_all instead. See http://www.rubyonrails.org/deprecation for details.", caller)
end
# See HasManyAssociation#delete_records. Dependent associations
@ -998,7 +1042,7 @@ module ActiveRecord
end
case reflection.options[:dependent]
when :destroy, true
when :destroy, true
module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
when :delete_all
module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }"
@ -1007,20 +1051,22 @@ module ActiveRecord
when nil, false
# pass
else
raise ArgumentError, 'The :dependent option expects either :destroy, :delete_all, or :nullify'
raise ArgumentError, 'The :dependent option expects either :destroy, :delete_all, or :nullify'
end
end
def configure_dependency_for_has_one(reflection)
case reflection.options[:dependent]
when :destroy, true
module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'"
when :delete
module_eval "before_destroy '#{reflection.class_name}.delete(#{reflection.name}.id) unless #{reflection.name}.nil?'"
when :nullify
module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil)'"
module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil) unless #{reflection.name}.nil?'"
when nil, false
# pass
else
raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
raise ArgumentError, "The :dependent option expects either :destroy, :delete or :nullify."
end
end
@ -1042,6 +1088,7 @@ module ActiveRecord
:exclusively_dependent, :dependent,
:select, :conditions, :include, :order, :group, :limit, :offset,
:as, :through, :source,
:uniq,
:finder_sql, :counter_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend
@ -1079,7 +1126,8 @@ module ActiveRecord
options.assert_valid_keys(
:class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
:select, :conditions, :include, :order, :group, :limit, :offset,
:finder_sql, :delete_sql, :insert_sql, :uniq,
:uniq,
:finder_sql, :delete_sql, :insert_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend
)
@ -1112,31 +1160,6 @@ module ActiveRecord
"#{name} Load Including Associations"
)
end
def construct_counter_sql_with_included_associations(options, join_dependency)
scope = scope(:find)
sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key})"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
if !Base.connection.supports_count_distinct?
sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{table_name}.#{primary_key}"
end
sql << " FROM #{table_name} "
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
if !Base.connection.supports_count_distinct?
sql << ")"
end
return sanitize_sql(sql)
end
def construct_finder_sql_with_included_associations(options, join_dependency)
scope = scope(:find)
@ -1145,11 +1168,13 @@ module ActiveRecord
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
sql << "ORDER BY #{options[:order]} " if options[:order]
sql << "GROUP BY #{options[:group]} " if options[:group]
add_order!(sql, options[:order], scope)
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
add_lock!(sql, options, scope)
return sanitize_sql(sql)
end
@ -1168,26 +1193,35 @@ module ActiveRecord
"#{name} Load IDs For Limited Eager Loading"
).collect { |row| connection.quote(row[primary_key]) }.join(", ")
end
def construct_finder_sql_for_association_limiting(options, join_dependency)
scope = scope(:find)
scope = scope(:find)
is_distinct = include_eager_conditions?(options) || include_eager_order?(options)
sql = "SELECT "
sql << "DISTINCT #{table_name}." if include_eager_conditions?(options) || include_eager_order?(options)
sql << primary_key
sql << ", #{options[:order].split(',').collect { |s| s.split.first } * ', '}" if options[:order] && (include_eager_conditions?(options) || include_eager_order?(options))
if is_distinct
sql << connection.distinct("#{table_name}.#{primary_key}", options[:order])
else
sql << primary_key
end
sql << " FROM #{table_name} "
if include_eager_conditions?(options) || include_eager_order?(options)
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
if is_distinct
sql << join_dependency.join_associations.collect(&:association_join).join
add_joins!(sql, options, scope)
end
add_conditions!(sql, options[:conditions], scope)
sql << "ORDER BY #{options[:order]} " if options[:order]
if options[:order]
if is_distinct
connection.add_order_by_for_association_limiting!(sql, options)
else
sql << "ORDER BY #{options[:order]}"
end
end
add_limit!(sql, options, scope)
return sanitize_sql(sql)
end
# Checks if the conditions reference a table other than the current model table
def include_eager_conditions?(options)
# look in both sets of conditions
@ -1199,7 +1233,7 @@ module ActiveRecord
end
end
return false unless conditions.any?
conditions.join(' ').scan(/(\w+)\.\w+/).flatten.any? do |condition_table_name|
conditions.join(' ').scan(/([\.\w]+)\.\w+/).flatten.any? do |condition_table_name|
condition_table_name != table_name
end
end
@ -1208,7 +1242,7 @@ module ActiveRecord
def include_eager_order?(options)
order = options[:order]
return false unless order
order.scan(/(\w+)\.\w+/).flatten.any? do |order_table_name|
order.scan(/([\.\w]+)\.\w+/).flatten.any? do |order_table_name|
order_table_name != table_name
end
end
@ -1225,7 +1259,7 @@ module ActiveRecord
def add_association_callbacks(association_name, options)
callbacks = %w(before_add after_add before_remove after_remove)
callbacks.each do |callback_name|
full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}"
full_callback_name = "#{callback_name}_for_#{association_name}"
defined_callbacks = options[callback_name.to_sym]
if options.has_key?(callback_name.to_sym)
class_inheritable_reader full_callback_name.to_sym
@ -1248,7 +1282,7 @@ module ActiveRecord
extension_module_name.constantize
end
class JoinDependency
class JoinDependency # :nodoc:
attr_reader :joins, :reflections, :table_aliases
def initialize(base, associations, joins)
@ -1334,11 +1368,15 @@ module ActiveRecord
when :has_many, :has_and_belongs_to_many
collection = record.send(join.reflection.name)
collection.loaded
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
association = join.instantiate(row)
collection.target.push(association) unless collection.target.include?(association)
when :has_one, :belongs_to
when :has_one
return if record.id.to_s != join.parent.record_id(row).to_s
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
record.send("set_#{join.reflection.name}_target", association)
when :belongs_to
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
association = join.instantiate(row)
record.send("set_#{join.reflection.name}_target", association)
@ -1348,7 +1386,7 @@ module ActiveRecord
return association
end
class JoinBase
class JoinBase # :nodoc:
attr_reader :active_record, :table_joins
delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record
@ -1393,7 +1431,7 @@ module ActiveRecord
end
end
class JoinAssociation < JoinBase
class JoinAssociation < JoinBase # :nodoc:
attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name
delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
@ -1407,7 +1445,7 @@ module ActiveRecord
@parent = parent
@reflection = reflection
@aliased_prefix = "t#{ join_dependency.joins.size }"
@aliased_table_name = table_name # start with the table name
@aliased_table_name = table_name #.tr('.', '_') # start with the table name, sub out any .'s
@parent_table_name = parent.active_record.table_name
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\son}
@ -1418,18 +1456,22 @@ module ActiveRecord
# if the table name has been used, then use an alias
@aliased_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}"
table_index = join_dependency.table_aliases[aliased_table_name]
join_dependency.table_aliases[aliased_table_name] += 1
@aliased_table_name = @aliased_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
else
join_dependency.table_aliases[aliased_table_name] += 1
end
join_dependency.table_aliases[aliased_table_name] += 1
if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through])
@aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : reflection.through_reflection.klass.table_name
unless join_dependency.table_aliases[aliased_join_table_name].zero?
@aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join"
table_index = join_dependency.table_aliases[aliased_join_table_name]
join_dependency.table_aliases[aliased_join_table_name] += 1
@aliased_join_table_name = @aliased_join_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
else
join_dependency.table_aliases[aliased_join_table_name] += 1
end
join_dependency.table_aliases[aliased_join_table_name] += 1
end
end
@ -1440,7 +1482,7 @@ module ActiveRecord
table_alias_for(options[:join_table], aliased_join_table_name),
aliased_join_table_name,
options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key,
reflection.active_record.table_name, reflection.active_record.primary_key] +
parent.aliased_table_name, reflection.active_record.primary_key] +
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
table_name_and_alias, aliased_table_name, klass.primary_key,
aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key
@ -1457,7 +1499,7 @@ module ActiveRecord
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
aliased_join_table_name, polymorphic_foreign_key,
parent.aliased_table_name, parent.primary_key,
aliased_join_table_name, polymorphic_foreign_type, klass.quote(parent.active_record.base_class.name)] +
aliased_join_table_name, polymorphic_foreign_type, klass.quote_value(parent.active_record.base_class.name)] +
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [table_name_and_alias,
aliased_table_name, primary_key, aliased_join_table_name, options[:foreign_key] || reflection.klass.to_s.classify.foreign_key
]
@ -1472,23 +1514,28 @@ module ActiveRecord
aliased_table_name, "#{source_reflection.options[:as]}_id",
aliased_join_table_name, options[:foreign_key] || primary_key,
aliased_table_name, "#{source_reflection.options[:as]}_type",
klass.quote(source_reflection.active_record.base_class.name)
klass.quote_value(source_reflection.active_record.base_class.name)
]
else
case source_reflection.macro
when :belongs_to
first_key = primary_key
second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
second_key = source_reflection.options[:foreign_key] || klass.to_s.classify.foreign_key
extra = nil
when :has_many
first_key = through_reflection.klass.to_s.classify.foreign_key
first_key = through_reflection.klass.base_class.to_s.classify.foreign_key
second_key = options[:foreign_key] || primary_key
extra = through_reflection.klass.descends_from_active_record? ? nil :
" AND %s.%s = %s" % [
aliased_join_table_name,
reflection.active_record.connection.quote_column_name(through_reflection.active_record.inheritance_column),
through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
end
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), aliased_join_table_name,
through_reflection.primary_key_name,
parent.aliased_table_name, parent.primary_key] +
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s%s) " % [
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
aliased_join_table_name, through_reflection.primary_key_name,
parent.aliased_table_name, parent.primary_key, extra] +
" LEFT OUTER JOIN %s ON (%s.%s = %s.%s) " % [
table_name_and_alias,
aliased_table_name, first_key,
aliased_join_table_name, second_key
@ -1502,7 +1549,7 @@ module ActiveRecord
aliased_table_name, "#{reflection.options[:as]}_id",
parent.aliased_table_name, parent.primary_key,
aliased_table_name, "#{reflection.options[:as]}_type",
klass.quote(parent.active_record.base_class.name)
klass.quote_value(parent.active_record.base_class.name)
]
when reflection.macro == :has_one && reflection.options[:as]
" LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s " % [
@ -1510,7 +1557,7 @@ module ActiveRecord
aliased_table_name, "#{reflection.options[:as]}_id",
parent.aliased_table_name, parent.primary_key,
aliased_table_name, "#{reflection.options[:as]}_type",
klass.quote(reflection.active_record.base_class.name)
klass.quote_value(reflection.active_record.base_class.name)
]
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
@ -1530,9 +1577,13 @@ module ActiveRecord
end || ''
join << %(AND %s.%s = %s ) % [
aliased_table_name,
reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
klass.quote(klass.name)] unless klass.descends_from_active_record?
join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
reflection.active_record.connection.quote_column_name(klass.inheritance_column),
klass.quote_value(klass.name.demodulize)] 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]
end
join
end
@ -1550,8 +1601,8 @@ module ActiveRecord
end
def interpolate_sql(sql)
instance_eval("%@#{sql.gsub('@', '\@')}@")
end
instance_eval("%@#{sql.gsub('@', '\@')}@")
end
end
end
end

View file

@ -9,7 +9,7 @@ module ActiveRecord
end
def reset
@target = []
reset_target!
@loaded = false
end
@ -28,7 +28,7 @@ module ActiveRecord
callback(:after_add, record)
end
end
result && self
end
@ -39,7 +39,12 @@ module ActiveRecord
def delete_all
load_target
delete(@target)
@target = []
reset_target!
end
# Calculate sum using SQL, not Enumerable
def sum(*args, &block)
calculate(:sum, *args, &block)
end
# Remove +records+ from this association. Does not destroy +records+.
@ -77,9 +82,9 @@ module ActiveRecord
each { |record| record.destroy }
end
@target = []
reset_target!
end
def create(attributes = {})
# Can't use Base.create since the foreign key may be a protected attribute.
if attributes.is_a?(Array)
@ -95,21 +100,35 @@ module ActiveRecord
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
def size
if loaded? then @target.size else count_records end
if loaded? && !@reflection.options[:uniq]
@target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = Array(@target.detect { |r| r.new_record? })
unsaved_records.size + count_records
else
count_records
end
end
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
def length
load_target.size
end
def empty?
size.zero?
end
def uniq(collection = self)
collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
seen = Set.new
collection.inject([]) do |kept, record|
unless seen.include?(record.id)
kept << record
seen << record.id
end
kept
end
end
# Replace this collection with +other_array+
@ -127,12 +146,23 @@ module ActiveRecord
end
end
private
# Array#flatten has problems with recursive 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
protected
def reset_target!
@target = Array.new
end
def find_target
records =
if @reflection.options[:finder_sql]
@reflection.klass.find_by_sql(@finder_sql)
else
find(:all)
end
@reflection.options[:uniq] ? uniq(records) : records
end
private
def callback(method, record)
callbacks_for(method).each do |callback|
case callback

View file

@ -4,14 +4,27 @@ module ActiveRecord
attr_reader :reflection
alias_method :proxy_respond_to?, :respond_to?
alias_method :proxy_extend, :extend
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ }
delegate :to_param, :to => :proxy_target
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
def initialize(owner, reflection)
@owner, @reflection = owner, reflection
proxy_extend(reflection.options[:extend]) if reflection.options[:extend]
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
reset
end
def proxy_owner
@owner
end
def proxy_reflection
@reflection
end
def proxy_target
@target
end
def respond_to?(symbol, include_priv = false)
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
end
@ -28,7 +41,7 @@ module ActiveRecord
end
def conditions
@conditions ||= eval("%(#{@reflection.active_record.send :sanitize_sql, @reflection.options[:conditions]})") if @reflection.options[:conditions]
@conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
end
alias :sql_conditions :conditions
@ -106,21 +119,22 @@ module ActiveRecord
private
def method_missing(method, *args, &block)
load_target
@target.send(method, *args, &block)
if load_target
@target.send(method, *args, &block)
end
end
def load_target
if !@owner.new_record? || foreign_key_present
begin
@target = find_target if !loaded?
rescue ActiveRecord::RecordNotFound
reset
end
return nil unless defined?(@loaded)
if !loaded? and (!@owner.new_record? || foreign_key_present)
@target = find_target
end
loaded if target
target
@loaded = true
@target
rescue ActiveRecord::RecordNotFound
reset
end
# Can be overwritten by associations that might have the foreign key available for an association without
@ -134,6 +148,11 @@ module ActiveRecord
raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
end
end
# Array#flatten has problems with recursive 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

@ -13,6 +13,17 @@ module ActiveRecord
record
end
def create(attributes = {})
# Can't use Base.create since the foreign key may be a protected attribute.
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr) }
else
record = build(attributes)
insert_record(record) unless @owner.new_record?
record
end
end
def find_first
load_target.first
end
@ -56,7 +67,9 @@ module ActiveRecord
@reflection.klass.find(*args)
end
end
# Deprecated as of Rails 1.2. If your associations require attributes
# you should be using has_many :through
def push_with_attributes(record, join_attributes = {})
raise_on_type_mismatch(record)
join_attributes.each { |key, value| record[key.to_s] = value }
@ -68,13 +81,10 @@ module ActiveRecord
self
end
deprecate :push_with_attributes => "consider using has_many :through instead"
alias :concat_with_attributes :push_with_attributes
def size
@reflection.options[:uniq] ? count_records : super
end
protected
def method_missing(method, *args, &block)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
@ -85,17 +95,7 @@ module ActiveRecord
end
end
end
def find_target
if @reflection.options[:finder_sql]
records = @reflection.klass.find_by_sql(@finder_sql)
else
records = find(:all)
end
@reflection.options[:uniq] ? uniq(records) : records
end
def count_records
load_target.size
end
@ -118,7 +118,7 @@ module ActiveRecord
attributes[column.name] = record.quoted_id
else
if record.attributes.has_key?(column.name)
value = @owner.send(:quote, record[column.name], column)
value = @owner.send(:quote_value, record[column.name], column)
attributes[column.name] = value unless value.nil?
end
end

View file

@ -10,10 +10,12 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr) }
else
load_target
record = @reflection.klass.new(attributes)
set_belongs_to_association_for(record)
@target ||= [] unless loaded?
@target << record
record
end
end
@ -29,22 +31,28 @@ module ActiveRecord
@reflection.klass.find_all(conditions, orderings, limit, joins)
end
end
deprecate :find_all => "use find(:all, ...) instead"
# DEPRECATED. Find the first associated record. All arguments are optional.
def find_first(conditions = nil, orderings = nil)
find_all(conditions, orderings, 1).first
end
deprecate :find_first => "use find(:first, ...) instead"
# Count the number of associated records. All arguments are optional.
def count(runtime_conditions = nil)
def count(*args)
if @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
elsif @reflection.options[:finder_sql]
@reflection.klass.count_by_sql(@finder_sql)
else
sql = @finder_sql
sql += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions
@reflection.klass.count(sql)
column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
options[:conditions] = options[:conditions].nil? ?
@finder_sql :
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
options[:include] = @reflection.options[:include]
@reflection.klass.count(column_name, options)
end
end
@ -83,33 +91,45 @@ module ActiveRecord
@reflection.klass.find(*args)
end
end
protected
def method_missing(method, *args, &block)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
else
create_scoping = {}
set_belongs_to_association_for(create_scoping)
@reflection.klass.with_scope(
:create => create_scoping,
:find => {
:conditions => @finder_sql,
:joins => @join_sql,
:readonly => false
},
:create => {
@reflection.primary_key_name => @owner.id
}
) do
@reflection.klass.send(method, *args, &block)
end
end
end
def find_target
if @reflection.options[:finder_sql]
@reflection.klass.find_by_sql(@finder_sql)
else
find(:all)
def load_target
if !@owner.new_record? || foreign_key_present
begin
if !loaded?
if @target.is_a?(Array) && @target.any?
@target = (find_target + @target).uniq
else
@target = find_target
end
end
rescue ActiveRecord::RecordNotFound
reset
end
end
loaded if target
target
end
def count_records
@ -118,7 +138,7 @@ module ActiveRecord
elsif @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
else
@reflection.klass.count(@counter_sql)
@reflection.klass.count(:conditions => @counter_sql)
end
@target = [] and loaded if count == 0
@ -167,7 +187,7 @@ module ActiveRecord
when @reflection.options[:as]
@finder_sql =
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
@finder_sql << " AND (#{conditions})" if conditions
else

View file

@ -8,7 +8,6 @@ module ActiveRecord
construct_sql
end
def find(*args)
options = Base.send(:extract_options_from_args!, args)
@ -23,12 +22,12 @@ module ActiveRecord
elsif @reflection.options[:order]
options[:order] = @reflection.options[:order]
end
options[:select] = construct_select(options[:select])
options[:from] ||= construct_from
options[:joins] = construct_joins(options[:joins])
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
merge_options_from_reflection!(options)
# Pass through args exactly as we received them.
@ -41,6 +40,68 @@ module ActiveRecord
@loaded = false
end
# Adds records to the association. The source record and its associates
# must have ids in order to create records associating them, so this
# will raise ActiveRecord::HasManyThroughCantAssociateNewRecords if
# either is a new record. Calls create! so you can rescue errors.
#
# The :before_add and :after_add callbacks are not yet supported.
def <<(*records)
return if records.empty?
through = @reflection.through_reflection
raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) if @owner.new_record?
load_target
klass = through.klass
klass.transaction do
flatten_deeper(records).each do |associate|
raise_on_type_mismatch(associate)
raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
@owner.send(@reflection.through_reflection.name).proxy_target << klass.with_scope(:create => construct_join_attributes(associate)) { klass.create! }
@target << associate
end
end
self
end
[:push, :concat].each { |method| alias_method method, :<< }
# Remove +records+ from this association. Does not destroy +records+.
def delete(*records)
records = flatten_deeper(records)
records.each { |associate| raise_on_type_mismatch(associate) }
records.reject! { |associate| @target.delete(associate) if associate.new_record? }
return if records.empty?
@delete_join_finder ||= "find_all_by_#{@reflection.source_reflection.association_foreign_key}"
through = @reflection.through_reflection
through.klass.transaction do
records.each do |associate|
joins = @owner.send(through.name).send(@delete_join_finder, associate.id)
@owner.send(through.name).delete(joins)
@target.delete(associate)
end
end
end
def build(attrs = nil)
raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection)
end
def create!(attrs = nil)
@reflection.klass.transaction do
self << @reflection.klass.with_scope(:create => attrs) { @reflection.klass.create! }
end
end
# Calculate sum using SQL, not Enumerable
def sum(*args, &block)
calculate(:sum, *args, &block)
end
protected
def method_missing(method, *args, &block)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
@ -49,41 +110,68 @@ module ActiveRecord
@reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) }
end
end
def find_target
@reflection.klass.find(:all,
records = @reflection.klass.find(:all,
:select => construct_select,
:conditions => construct_conditions,
:from => construct_from,
:joins => construct_joins,
:order => @reflection.options[:order],
:order => @reflection.options[:order],
:limit => @reflection.options[:limit],
:group => @reflection.options[:group],
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include]
)
@reflection.options[:uniq] ? records.to_set.to_a : records
end
def construct_conditions
conditions = if @reflection.through_reflection.options[:as]
"#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_id = #{@owner.quoted_id} " +
"AND #{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
# Construct attributes for associate pointing to owner.
def construct_owner_attributes(reflection)
if as = reflection.options[:as]
{ "#{as}_id" => @owner.id,
"#{as}_type" => @owner.class.base_class.name.to_s }
else
"#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.primary_key_name} = #{@owner.quoted_id}"
{ reflection.primary_key_name => @owner.id }
end
conditions << " AND (#{sql_conditions})" if sql_conditions
return conditions
end
# Construct attributes for :through pointing to owner and associate.
def construct_join_attributes(associate)
construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.association_foreign_key => associate.id)
end
# Associate attributes pointing to owner, quoted.
def construct_quoted_owner_attributes(reflection)
if as = reflection.options[:as]
{ "#{as}_id" => @owner.quoted_id,
"#{as}_type" => reflection.klass.quote_value(
@owner.class.base_class.name.to_s,
reflection.klass.columns_hash["#{as}_type"]) }
else
{ reflection.primary_key_name => @owner.quoted_id }
end
end
# Build SQL conditions from attributes, qualified by table name.
def construct_conditions
table_name = @reflection.through_reflection.table_name
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
"#{table_name}.#{attr} = #{value}"
end
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
end
def construct_from
@reflection.table_name
end
def construct_select(custom_select = nil)
selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*"
selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*"
end
def construct_joins(custom_joins = nil)
def construct_joins(custom_joins = nil)
polymorphic_join = nil
if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
reflection_primary_key = @reflection.klass.primary_key
@ -94,7 +182,7 @@ module ActiveRecord
if @reflection.source_reflection.options[:as]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type",
@owner.class.quote(@reflection.through_reflection.klass.name)
@owner.class.quote_value(@reflection.through_reflection.klass.name)
]
end
end
@ -106,14 +194,15 @@ module ActiveRecord
polymorphic_join
]
end
def construct_scope
{
:find => { :from => construct_from, :conditions => construct_conditions, :joins => construct_joins, :select => construct_select },
:create => { @reflection.primary_key_name => @owner.id }
}
{ :create => construct_owner_attributes(@reflection),
:find => { :from => construct_from,
:conditions => construct_conditions,
:joins => construct_joins,
:select => construct_select } }
end
def construct_sql
case
when @reflection.options[:finder_sql]
@ -133,14 +222,15 @@ module ActiveRecord
@counter_sql = @finder_sql
end
end
def conditions
@conditions ||= [
(interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
(interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions])
].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions])
(interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
(interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]),
("#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.name.demodulize)}" unless @reflection.through_reflection.klass.descends_from_active_record?)
].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
end
alias_method :sql_conditions, :conditions
end
end

View file

@ -69,7 +69,7 @@ module ActiveRecord
when @reflection.options[:as]
@finder_sql =
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
else
@finder_sql = "#{@reflection.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
end

View file

@ -1,3 +1,4 @@
require 'base64'
require 'yaml'
require 'set'
require 'active_record/deprecated_finders'
@ -80,11 +81,12 @@ module ActiveRecord #:nodoc:
#
# == Conditions
#
# Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement.
# Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement.
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
# be used for statements that don't involve tainted data. Examples:
# be used for statements that don't involve tainted data. The hash form works much like the array form, except
# only equality and range is possible. Examples:
#
# User < ActiveRecord::Base
# class User < ActiveRecord::Base
# def self.authenticate_unsafely(user_name, password)
# find(:first, :conditions => "user_name = '#{user_name}' AND password = '#{password}'")
# end
@ -92,12 +94,16 @@ module ActiveRecord #:nodoc:
# def self.authenticate_safely(user_name, password)
# find(:first, :conditions => [ "user_name = ? AND password = ?", user_name, password ])
# end
#
# def self.authenticate_safely_simply(user_name, password)
# find(:first, :conditions => { :user_name => user_name, :password => password })
# end
# end
#
# The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection
# attacks if the <tt>user_name</tt> and +password+ parameters come directly from a HTTP request. The <tt>authenticate_safely</tt> method,
# on the other hand, will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query, which will ensure that
# an attacker can't escape the query and fake the login (or worse).
# attacks if the <tt>user_name</tt> and +password+ parameters come directly from a HTTP request. The <tt>authenticate_safely</tt> and
# <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query,
# which will ensure that an attacker can't escape the query and fake the login (or worse).
#
# When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth
# question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing
@ -108,6 +114,16 @@ module ActiveRecord #:nodoc:
# { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' }
# ])
#
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
# operator. For instance:
#
# Student.find(:all, :conditions => { :first_name => "Harvey", :status => 1 })
# Student.find(:all, :conditions => params[:student])
#
# A range may be used in the hash to use the SQL BETWEEN operator:
#
# Student.find(:all, :conditions => { :grade => 9..12 })
#
# == Overwriting default accessors
#
# All column values are automatically available through basic accessors on the Active Record object, but some times you
@ -166,6 +182,12 @@ module ActiveRecord #:nodoc:
# # Now the 'Summer' tag does exist
# Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
#
# Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Example:
#
# # No 'Winter' tag exists
# winter = Tag.find_or_initialize_by_name("Winter")
# winter.new_record? # true
#
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
# Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
@ -243,9 +265,9 @@ module ActiveRecord #:nodoc:
class Base
# Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
# on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
cattr_accessor :logger
cattr_accessor :logger, :instance_writer => false
include Reloadable::Subclasses
include Reloadable::Deprecated
def self.inherited(child) #:nodoc:
@@subclasses[self] ||= []
@ -256,7 +278,7 @@ module ActiveRecord #:nodoc:
def self.reset_subclasses #:nodoc:
nonreloadables = []
subclasses.each do |klass|
unless klass.reloadable?
unless Dependencies.autoloaded? klass
nonreloadables << klass
next
end
@ -269,54 +291,54 @@ module ActiveRecord #:nodoc:
@@subclasses = {}
cattr_accessor :configurations
cattr_accessor :configurations, :instance_writer => false
@@configurations = {}
# Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and
# :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as
# the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember
# that this is a global setting for all Active Records.
cattr_accessor :primary_key_prefix_type
cattr_accessor :primary_key_prefix_type, :instance_writer => false
@@primary_key_prefix_type = nil
# Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
# table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
# for tables in a shared database. By default, the prefix is the empty string.
cattr_accessor :table_name_prefix
cattr_accessor :table_name_prefix, :instance_writer => false
@@table_name_prefix = ""
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
# "people_basecamp"). By default, the suffix is the empty string.
cattr_accessor :table_name_suffix
cattr_accessor :table_name_suffix, :instance_writer => false
@@table_name_suffix = ""
# Indicates whether or not table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a +Product+ class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
cattr_accessor :pluralize_table_names
cattr_accessor :pluralize_table_names, :instance_writer => false
@@pluralize_table_names = true
# Determines whether or not to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
# make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
# may complicate matters if you use software like syslog. This is true, by default.
cattr_accessor :colorize_logging
cattr_accessor :colorize_logging, :instance_writer => false
@@colorize_logging = true
# Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
# This is set to :local by default.
cattr_accessor :default_timezone
cattr_accessor :default_timezone, :instance_writer => false
@@default_timezone = :local
# Determines whether or not to use a connection for each thread, or a single shared connection for all threads.
# Defaults to false. Set to true if you're writing a threaded application.
cattr_accessor :allow_concurrency
cattr_accessor :allow_concurrency, :instance_writer => false
@@allow_concurrency = false
# Determines whether to speed up access by generating optimized reader
# methods to avoid expensive calls to method_missing when accessing
# attributes by name. You might want to set this to false in development
# mode, because the methods would be regenerated on each request.
cattr_accessor :generate_read_methods
cattr_accessor :generate_read_methods, :instance_writer => false
@@generate_read_methods = true
# Specifies the format to use when dumping the database schema with Rails'
@ -325,7 +347,7 @@ module ActiveRecord #:nodoc:
# ActiveRecord::Schema file which can be loaded into any database that
# supports migrations. Use :ruby if you want to have different database
# adapters for, e.g., your development and test environments.
cattr_accessor :schema_format
cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby
class << self # Class methods
@ -351,7 +373,11 @@ module ActiveRecord #:nodoc:
# to already defined associations. See eager loading under Associations.
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
# include the joined columns.
# * <tt>:from</tt>: By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
# of a database view).
# * <tt>:readonly</tt>: Mark the returned records read-only so they cannot be saved or updated.
# * <tt>:lock</tt>: An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE".
# :lock => true gives connection's default exclusive lock, usually "FOR UPDATE".
#
# Examples for find by id:
# Person.find(1) # returns the object for ID = 1
@ -371,6 +397,17 @@ module ActiveRecord #:nodoc:
# Person.find(:all, :offset => 10, :limit => 10)
# Person.find(:all, :include => [ :account, :friends ])
# Person.find(:all, :group => "category")
#
# Example for find with a lock. Imagine two concurrent transactions:
# each will read person.visits == 2, add 1 to it, and save, resulting
# in two saves of person.visits = 3. By locking the row, the second
# transaction has to wait until the first is finished; we get the
# expected person.visits == 4.
# Person.transaction do
# person = Person.find(1, :lock => true)
# person.visits += 1
# person.save!
# end
def find(*args)
options = extract_options_from_args!(args)
validate_find_options(options)
@ -391,10 +428,16 @@ module ActiveRecord #:nodoc:
end
# Returns true if the given +id+ represents the primary key of a record in the database, false otherwise.
# You can also pass a set of SQL conditions.
# Example:
# Person.exists?(5)
def exists?(id)
!find(:first, :conditions => ["#{primary_key} = ?", id]).nil? rescue false
# Person.exists?('5')
# Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
def exists?(id_or_conditions)
!find(:first, :conditions => expand_id_conditions(id_or_conditions)).nil?
rescue ActiveRecord::ActiveRecordError
false
end
# Creates an object, instantly saves it as a record (if the validation permits it), and returns it. If the save
@ -483,12 +526,12 @@ module ActiveRecord #:nodoc:
# for looping over a collection where each element require a number of aggregate values. Like the DiscussionBoard
# that needs to list both the number of posts and comments.
def increment_counter(counter_name, id)
update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{quote(id)}"
update_all "#{counter_name} = #{counter_name} + 1", "#{primary_key} = #{quote_value(id)}"
end
# Works like increment_counter, but decrements instead.
def decrement_counter(counter_name, id)
update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{quote(id)}"
update_all "#{counter_name} = #{counter_name} - 1", "#{primary_key} = #{quote_value(id)}"
end
@ -548,21 +591,44 @@ module ActiveRecord #:nodoc:
# to guess the table name from even when called on Reply. The rules used to do the guess are handled by the Inflector class
# in Active Support, which knows almost all common English inflections (report a bug if your inflection isn't covered).
#
# 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".
# Nested classes are given table names prefixed by the singular form of
# the parent's table name. Example:
# file class table_name
# invoice.rb Invoice invoices
# invoice/lineitem.rb Invoice::Lineitem invoice_lineitems
#
# 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:
# Additionally, the class-level table_name_prefix is prepended and the
# table_name_suffix is appended. So if you have "myapp_" as a prefix,
# the table name guess for an Invoice class becomes "myapp_invoices".
# Invoice::Lineitem becomes "myapp_invoice_lineitems".
#
# 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
# set_table_name "mice"
# set_table_name "mice"
# end
def table_name
reset_table_name
end
def reset_table_name #:nodoc:
name = "#{table_name_prefix}#{undecorated_table_name(base_class.name)}#{table_name_suffix}"
base = base_class
name =
# STI subclasses always use their superclass' table.
unless self == base
base.table_name
else
# Nested classes are prefixed with singular parent table name.
if parent < ActiveRecord::Base && !parent.abstract_class?
contained = parent.table_name
contained = contained.singularize if parent.pluralize_table_names
contained << '_'
end
name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
end
set_table_name(name)
name
end
@ -585,9 +651,10 @@ module ActiveRecord #:nodoc:
key
end
# Defines the column name for use with single table inheritance -- can be overridden in subclasses.
# Defines the column name for use with single table inheritance
# -- can be set in subclasses like so: self.inheritance_column = "type_id"
def inheritance_column
"type"
@inheritance_column ||= "type".freeze
end
# Lazy-set the sequence name to the connection's default. This method
@ -699,7 +766,7 @@ module ActiveRecord #:nodoc:
@columns
end
# Returns an array of column objects for the table associated with this class.
# Returns a hash of column objects for the table associated with this class.
def columns_hash
@columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
end
@ -737,7 +804,7 @@ module ActiveRecord #:nodoc:
# Resets all the cached information about columns, which will cause them to be reloaded on the next request.
def reset_column_information
read_methods.each { |name| undef_method(name) }
@column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @read_methods = nil
@column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @read_methods = @inheritance_column = nil
end
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
@ -755,10 +822,16 @@ module ActiveRecord #:nodoc:
superclass == Base || !columns_hash.include?(inheritance_column)
end
def quote(object) #:nodoc:
connection.quote(object)
def quote_value(value, column = nil) #:nodoc:
connection.quote(value,column)
end
def quote(value, column = nil) #:nodoc:
connection.quote(value, column)
end
deprecate :quote => :quote_value
# Used to sanitize objects before they're used in an SELECT SQL-statement. Delegates to <tt>connection.quote</tt>.
def sanitize(object) #:nodoc:
connection.quote(object)
@ -837,7 +910,7 @@ module ActiveRecord #:nodoc:
method_scoping.assert_valid_keys([ :find, :create ])
if f = method_scoping[:find]
f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :readonly ])
f.assert_valid_keys([ :conditions, :joins, :select, :include, :from, :offset, :limit, :order, :readonly, :lock ])
f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly)
end
@ -917,7 +990,7 @@ module ActiveRecord #:nodoc:
options.update(:limit => 1) unless options[:include]
find_every(options).first
end
def find_every(options)
records = scoped?(:find, :include) || options[:include] ?
find_with_associations(options) :
@ -927,11 +1000,11 @@ module ActiveRecord #:nodoc:
records
end
def find_from_ids(ids, options)
expects_array = ids.first.kind_of?(Array)
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
ids = ids.flatten.compact.uniq
case ids.size
@ -947,9 +1020,12 @@ module ActiveRecord #:nodoc:
def find_one(id, options)
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
options.update :conditions => "#{table_name}.#{primary_key} = #{sanitize(id)}#{conditions}"
options.update :conditions => "#{table_name}.#{primary_key} = #{quote_value(id,columns_hash[primary_key])}#{conditions}"
if result = find_initial(options)
# Use find_every(options).first since the primary key condition
# already ensures we have a single record. Using find_initial adds
# a superfluous :limit => 1.
if result = find_every(options).first
result
else
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
@ -958,7 +1034,7 @@ module ActiveRecord #:nodoc:
def find_some(ids, options)
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
ids_list = ids.map { |id| sanitize(id) }.join(',')
ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
options.update :conditions => "#{table_name}.#{primary_key} IN (#{ids_list})#{conditions}"
result = find_every(options)
@ -970,23 +1046,32 @@ module ActiveRecord #:nodoc:
end
end
# Finder methods must instantiate through this method to work with the single-table inheritance model
# that makes it possible to create objects of different types from the same table.
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
def instantiate(record)
object =
object =
if subclass_name = record[inheritance_column]
# No type given.
if subclass_name.empty?
allocate
else
require_association_class(subclass_name)
begin
compute_type(subclass_name).allocate
rescue NameError
raise SubclassNotFound,
"The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " +
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
"or overwrite #{self.to_s}.inheritance_column to use another column for that information."
# Ignore type if no column is present since it was probably
# pulled in from a sloppy join.
unless columns_hash.include?(inheritance_column)
allocate
else
begin
compute_type(subclass_name).allocate
rescue NameError
raise SubclassNotFound,
"The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " +
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
"or overwrite #{self.to_s}.inheritance_column to use another column for that information."
end
end
end
else
@ -1012,19 +1097,20 @@ module ActiveRecord #:nodoc:
add_conditions!(sql, options[:conditions], scope)
sql << " GROUP BY #{options[:group]} " if options[:group]
sql << " ORDER BY #{options[:order]} " if options[:order]
add_order!(sql, options[:order], scope)
add_limit!(sql, options, scope)
add_lock!(sql, options, scope)
sql
end
# Merges includes so that the result is a valid +include+
def merge_includes(first, second)
safe_to_array(first) + safe_to_array(second)
(safe_to_array(first) + safe_to_array(second)).uniq
end
# Object#to_a is deprecated, though it does have the desired behaviour
# Object#to_a is deprecated, though it does have the desired behavior
def safe_to_array(o)
case o
when NilClass
@ -1036,16 +1122,32 @@ module ActiveRecord #:nodoc:
end
end
def add_order!(sql, order, scope = :auto)
scope = scope(:find) if :auto == scope
scoped_order = scope[:order] if scope
if order
sql << " ORDER BY #{order}"
sql << ", #{scoped_order}" if scoped_order
else
sql << " ORDER BY #{scoped_order}" if scoped_order
end
end
# The optional scope argument is for the current :find scope.
def add_limit!(sql, options, scope = :auto)
scope = scope(:find) if :auto == scope
if scope
options[:limit] ||= scope[:limit]
options[:offset] ||= scope[:offset]
end
options = options.reverse_merge(:limit => scope[:limit], :offset => scope[:offset]) if scope
connection.add_limit_offset!(sql, options)
end
# The optional scope argument is for the current :find scope.
# The :lock option has precedence over a scoped :lock.
def add_lock!(sql, options, scope = :auto)
scope = scope(:find) if :auto == scope
options = options.reverse_merge(:lock => scope[:lock]) if scope
connection.add_lock!(sql, options)
end
# The optional scope argument is for the current :find scope.
def add_joins!(sql, options, scope = :auto)
scope = scope(:find) if :auto == scope
@ -1060,7 +1162,7 @@ module ActiveRecord #:nodoc:
segments = []
segments << sanitize_sql(scope[:conditions]) if scope && scope[:conditions]
segments << sanitize_sql(conditions) unless conditions.nil?
segments << type_condition unless descends_from_active_record?
segments << type_condition unless descends_from_active_record?
segments.compact!
sql << "WHERE (#{segments.join(") AND (")}) " unless segments.empty?
end
@ -1088,43 +1190,48 @@ module ActiveRecord #:nodoc:
# It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount
# is actually find_all_by_amount(amount, options).
def method_missing(method_id, *arguments)
if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(method_id.to_s)
if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
finder, deprecated_finder = determine_finder(match), determine_deprecated_finder(match)
attribute_names = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(attribute_names)
conditions = construct_conditions_from_arguments(attribute_names, arguments)
attributes = construct_attributes_from_arguments(attribute_names, arguments)
case extra_options = arguments[attribute_names.size]
when nil
options = { :conditions => conditions }
options = { :conditions => attributes }
set_readonly_option!(options)
send(finder, options)
ActiveSupport::Deprecation.silence { send(finder, options) }
when Hash
finder_options = extra_options.merge(:conditions => conditions)
finder_options = extra_options.merge(:conditions => attributes)
validate_find_options(finder_options)
set_readonly_option!(finder_options)
if extra_options[:conditions]
with_scope(:find => { :conditions => extra_options[:conditions] }) do
send(finder, finder_options)
ActiveSupport::Deprecation.silence { send(finder, finder_options) }
end
else
send(finder, finder_options)
ActiveSupport::Deprecation.silence { send(finder, finder_options) }
end
else
send(deprecated_finder, conditions, *arguments[attribute_names.length..-1]) # deprecated API
ActiveSupport::Deprecation.silence do
send(deprecated_finder, sanitize_sql(attributes), *arguments[attribute_names.length..-1])
end
end
elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(method_id.to_s)
elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
instantiator = determine_instantiator(match)
attribute_names = extract_attribute_names_from_match(match)
super unless all_attributes_exists?(attribute_names)
options = { :conditions => construct_conditions_from_arguments(attribute_names, arguments) }
attributes = construct_attributes_from_arguments(attribute_names, arguments)
options = { :conditions => attributes }
set_readonly_option!(options)
find_initial(options) || create(construct_attributes_from_arguments(attribute_names, arguments))
find_initial(options) || send(instantiator, attributes)
else
super
end
@ -1138,16 +1245,14 @@ module ActiveRecord #:nodoc:
match.captures.first == 'all_by' ? :find_all : :find_first
end
def determine_instantiator(match)
match.captures.first == 'initialize' ? :new : :create
end
def extract_attribute_names_from_match(match)
match.captures.last.split('_and_')
end
def construct_conditions_from_arguments(attribute_names, arguments)
conditions = []
attribute_names.each_with_index { |name, idx| conditions << "#{table_name}.#{connection.quote_column_name(name)} #{attribute_condition(arguments[idx])} " }
[ conditions.join(" AND "), *arguments[0...attribute_names.length] ]
end
def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
@ -1162,10 +1267,20 @@ module ActiveRecord #:nodoc:
case argument
when nil then "IS ?"
when Array then "IN (?)"
when Range then "BETWEEN ? AND ?"
else "= ?"
end
end
# Interpret Array and Hash as conditions and anything else as an id.
def expand_id_conditions(id_or_conditions)
case id_or_conditions
when Array, Hash then id_or_conditions
else sanitize_sql(primary_key => id_or_conditions)
end
end
# Defines an "attribute" method (like #inheritance_column or
# #table_name). A new (class) method will be created with the
# given name. If a value is specified, the new method will
@ -1241,9 +1356,9 @@ module ActiveRecord #:nodoc:
def compute_type(type_name)
modularized_name = type_name_with_module(type_name)
begin
instance_eval(modularized_name)
rescue NameError => e
instance_eval(type_name)
class_eval(modularized_name, __FILE__, __LINE__)
rescue NameError
class_eval(type_name, __FILE__, __LINE__)
end
end
@ -1263,12 +1378,38 @@ module ActiveRecord #:nodoc:
klass.base_class.name
end
# Accepts an array or string. The string is returned untouched, but the array has each value
# Accepts an array, hash, or string of sql conditions and sanitizes
# them into a valid SQL fragment.
# ["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(condition)
case condition
when Array; sanitize_sql_array(condition)
when Hash; sanitize_sql_hash(condition)
else condition
end
end
# Sanitizes a hash of attribute/value pairs into SQL conditions.
# { :name => "foo'bar", :group_id => 4 }
# # => "name='foo''bar' and group_id= 4"
# { :status => nil, :group_id => [1,2,3] }
# # => "status IS NULL and group_id IN (1,2,3)"
# { :age => 13..18 }
# # => "age BETWEEN 13 AND 18"
def sanitize_sql_hash(attrs)
conditions = attrs.map do |attr, value|
"#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
end.join(' AND ')
replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
end
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the sql statement.
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
def sanitize_sql(ary)
return ary unless ary.is_a?(Array)
def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) and statement =~ /:\w+/
replace_named_bind_variables(statement, values.first)
@ -1298,9 +1439,20 @@ module ActiveRecord #:nodoc:
end
end
def expand_range_bind_variables(bind_vars) #:nodoc:
bind_vars.each_with_index do |var, index|
bind_vars[index, 1] = [var.first, var.last] if var.is_a?(Range)
end
bind_vars
end
def quote_bound_value(value) #:nodoc:
if (value.respond_to?(:map) && !value.is_a?(String))
value.map { |v| connection.quote(v) }.join(',')
if value.respond_to?(:map) && !value.is_a?(String)
if value.respond_to?(:empty?) && value.empty?
connection.quote(nil)
else
value.map { |v| connection.quote(v) }.join(',')
end
else
connection.quote(value)
end
@ -1317,12 +1469,12 @@ module ActiveRecord #:nodoc:
end
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
:order, :select, :readonly, :group, :from ]
:order, :select, :readonly, :group, :from, :lock ]
def validate_find_options(options) #:nodoc:
options.assert_valid_keys(VALID_FIND_OPTIONS)
end
def set_readonly_option!(options) #:nodoc:
# Inherit :readonly from finder scope if set. Otherwise,
# if :joins is not blank then :readonly defaults to true.
@ -1365,14 +1517,17 @@ module ActiveRecord #:nodoc:
end
# Enables Active Record objects to be used as URL parameters in Action Pack automatically.
alias_method :to_param, :id
def to_param
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
(id = self.id) ? id.to_s : nil # Be sure to stringify the id for routes
end
def id_before_type_cast #:nodoc:
read_attribute_before_type_cast(self.class.primary_key)
end
def quoted_id #:nodoc:
quote(id, column_for_attribute(self.class.primary_key))
quote_value(id, column_for_attribute(self.class.primary_key))
end
# Sets the primary ID.
@ -1388,14 +1543,13 @@ module ActiveRecord #:nodoc:
# * No record exists: Creates a new record with values matching those of the object attributes.
# * A record does exist: Updates the record with values matching those of the object attributes.
def save
raise ReadOnlyRecord if readonly?
create_or_update
end
# Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a
# RecordNotSaved exception
def save!
save || raise(RecordNotSaved)
create_or_update || raise(RecordNotSaved)
end
# Deletes the record in the database and freezes this instance to reflect that no changes should
@ -1438,6 +1592,12 @@ module ActiveRecord #:nodoc:
self.attributes = attributes
save
end
# Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
def update_attributes!(attributes)
self.attributes = attributes
save!
end
# Initializes the +attribute+ to zero if nil and adds one. Only makes sense for number-based attributes. Returns self.
def increment(attribute)
@ -1475,10 +1635,13 @@ module ActiveRecord #:nodoc:
end
# Reloads the attributes of this object from the database.
def reload
# The optional options argument is passed to find when reloading so you
# may do e.g. record.reload(:lock => true) to reload the same record with
# an exclusive row lock.
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
@attributes.update(self.class.find(self.id).instance_variable_get('@attributes'))
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
self
end
@ -1588,13 +1751,13 @@ module ActiveRecord #:nodoc:
# person.respond_to?("name?") which will all return true.
def respond_to?(method, include_priv = false)
if @attributes.nil?
return super
return super
elsif attr_name = self.class.column_methods_hash[method.to_sym]
return true if @attributes.include?(attr_name) || attr_name == self.class.primary_key
return false if self.class.read_methods.include?(attr_name)
elsif @attributes.include?(method_name = method.to_s)
return true
elsif md = /(=|\?|_before_type_cast)$/.match(method_name)
elsif md = self.class.match_attribute_method?(method.to_s)
return true if @attributes.include?(md.pre_match)
end
# super must be called at the end of the method, because the inherited respond_to?
@ -1620,117 +1783,27 @@ module ActiveRecord #:nodoc:
@readonly = true
end
# Builds an XML document to represent the model. Some configuration is
# availble through +options+, however more complicated cases should use
# Builder.
#
# By default the generated XML document will include the processing
# instruction and all object's attributes. For example:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <topic>
# <title>The First Topic</title>
# <author-name>David</author-name>
# <id type="integer">1</id>
# <approved type="boolean">false</approved>
# <replies-count type="integer">0</replies-count>
# <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
# <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
# This behaviour can be controlled with :only, :except, and :skip_instruct
# for instance:
#
# topic.to_xml(:skip_instruct => true, :except => [ :id, bonus_time, :written_on, replies_count ])
#
# <topic>
# <title>The First Topic</title>
# <author-name>David</author-name>
# <approved type="boolean">false</approved>
# <content>Have a nice day</content>
# <author-email-address>david@loudthinking.com</author-email-address>
# <parent-id></parent-id>
# <last-read type="date">2004-04-15</last-read>
# </topic>
#
# To include first level associations use :include
#
# firm.to_xml :include => [ :account, :clients ]
#
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
# <id type="integer">1</id>
# <rating type="integer">1</rating>
# <name>37signals</name>
# <clients>
# <client>
# <rating type="integer">1</rating>
# <name>Summit</name>
# </client>
# <client>
# <rating type="integer">1</rating>
# <name>Microsoft</name>
# </client>
# </clients>
# <account>
# <id type="integer">1</id>
# <credit-limit type="integer">50</credit-limit>
# </account>
# </firm>
def to_xml(options = {})
options[:root] ||= self.class.to_s.underscore
options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only] # skip type column
root_only_or_except = { :only => options[:only], :except => options[:except] }
attributes_for_xml = attributes(root_only_or_except)
if include_associations = options.delete(:include)
include_has_options = include_associations.is_a?(Hash)
for association in include_has_options ? include_associations.keys : Array(include_associations)
association_options = include_has_options ? include_associations[association] : root_only_or_except
case self.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
records = send(association).to_a
unless records.empty?
attributes_for_xml[association] = records.collect do |record|
record.attributes(association_options)
end
end
when :has_one, :belongs_to
if record = send(association)
attributes_for_xml[association] = record.attributes(association_options)
end
end
end
end
attributes_for_xml.to_xml(options)
end
private
def create_or_update
if new_record? then create else update end
raise ReadOnlyRecord if readonly?
result = new_record? ? create : update
result != false
end
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def update
connection.update(
"UPDATE #{self.class.table_name} " +
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
"WHERE #{self.class.primary_key} = #{quote(id)}",
"WHERE #{self.class.primary_key} = #{quote_value(id)}",
"#{self.class.name} Update"
)
return true
end
# Creates a new record with values matching those of the instance attributes.
# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
@ -1745,8 +1818,7 @@ module ActiveRecord #:nodoc:
)
@new_record = false
return true
id
end
# Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord descendent.
@ -1759,6 +1831,7 @@ module ActiveRecord #:nodoc:
end
end
# Allows access to the object attributes, which are held in the @attributes hash, as were
# they first-class methods. So a Person class with a name attribute can use Person#name and
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
@ -1771,20 +1844,16 @@ module ActiveRecord #:nodoc:
method_name = method_id.to_s
if @attributes.include?(method_name) or
(md = /\?$/.match(method_name) and
@attributes.include?(method_name = md.pre_match))
@attributes.include?(query_method_name = md.pre_match) and
method_name = query_method_name)
define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
md ? query_attribute(method_name) : read_attribute(method_name)
elsif self.class.primary_key.to_s == method_name
id
elsif md = /(=|_before_type_cast)$/.match(method_name)
elsif md = self.class.match_attribute_method?(method_name)
attribute_name, method_type = md.pre_match, md.to_s
if @attributes.include?(attribute_name)
case method_type
when '='
write_attribute(attribute_name, args.first)
when '_before_type_cast'
read_attribute_before_type_cast(attribute_name)
end
__send__("attribute#{method_type}", attribute_name, *args, &block)
else
super
end
@ -1821,9 +1890,16 @@ module ActiveRecord #:nodoc:
# ActiveRecord::Base.generate_read_methods is set to true.
def define_read_methods
self.class.columns_hash.each do |name, column|
unless self.class.serialized_attributes[name]
define_read_method(name.to_sym, name, column) unless respond_to_without_attributes?(name)
define_question_method(name) unless respond_to_without_attributes?("#{name}?")
unless respond_to_without_attributes?(name)
if self.class.serialized_attributes[name]
define_read_method_for_serialized_attribute(name)
else
define_read_method(name.to_sym, name, column)
end
end
unless respond_to_without_attributes?("#{name}?")
define_question_method(name)
end
end
end
@ -1841,6 +1917,15 @@ module ActiveRecord #:nodoc:
evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"
end
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
unless attr_name.to_s == self.class.primary_key.to_s
self.class.read_methods << attr_name
end
evaluate_read_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
end
# Define an attribute ? method.
def define_question_method(attr_name)
unless attr_name.to_s == self.class.primary_key.to_s
@ -1857,7 +1942,7 @@ module ActiveRecord #:nodoc:
rescue SyntaxError => err
self.class.read_methods.delete(attr_name)
if logger
logger.warn "Exception occured during reader method compilation."
logger.warn "Exception occurred during reader method compilation."
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
logger.warn "#{err.message}"
end
@ -1944,17 +2029,24 @@ module ActiveRecord #:nodoc:
def attributes_with_quotes(include_primary_key = true)
attributes.inject({}) do |quoted, (name, value)|
if column = column_for_attribute(name)
quoted[name] = quote(value, column) unless !include_primary_key && column.primary
quoted[name] = quote_value(value, column) unless !include_primary_key && column.primary
end
quoted
end
end
# Quote strings appropriately for SQL statements.
def quote(value, column = nil)
def quote_value(value, column = nil)
self.class.connection.quote(value, column)
end
# Deprecated, use quote_value
def quote(value, column = nil)
self.class.connection.quote(value, column)
end
deprecate :quote => :quote_value
# Interpolate custom sql string in instance context.
# Optional record argument is meant for custom insert_sql.
def interpolate_sql(sql, record = nil)
@ -1993,7 +2085,7 @@ module ActiveRecord #:nodoc:
send(name + "=", nil)
else
begin
send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values))
send(name + "=", Time == klass ? (@@default_timezone == :utc ? klass.utc(*values) : klass.local(*values)) : klass.new(*values))
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
end

View file

@ -1,6 +1,6 @@
module ActiveRecord
module Calculations #:nodoc:
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset]
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include]
def self.included(base)
base.extend(ClassMethods)
end
@ -9,7 +9,7 @@ module ActiveRecord
# Count operates using three different approaches.
#
# * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
# * Count by conditions or joins: For backwards compatibility, you can pass in +conditions+ and +joins+ as individual parameters.
# * Count by conditions or joins: This API has been deprecated and will be removed in Rails 2.0
# * Count using options will find the row count matched by the options used.
#
# The last approach, count using options, accepts an option hash as the only parameter. The options are:
@ -29,7 +29,7 @@ module ActiveRecord
# Examples for counting all:
# Person.count # returns the total count of all people
#
# Examples for count by +conditions+ and +joins+ (for backwards compatibility):
# Examples for count by +conditions+ and +joins+ (this has been deprecated):
# Person.count("age > 26") # returns the number of people older than 26
# Person.find("age > 26 AND job.salary > 60000", "LEFT JOIN jobs on jobs.person_id = person.id") # returns the total number of rows matching the conditions and joins fetched by SELECT COUNT(*).
#
@ -42,29 +42,7 @@ module ActiveRecord
#
# Note: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead.
def count(*args)
options = {}
column_name = :all
# For backwards compatibility, we need to handle both count(conditions=nil, joins=nil) or count(options={}) or count(column_name=:all, options={}).
if args.size >= 0 && args.size <= 2
if args.first.is_a?(Hash)
options = args.first
elsif args[1].is_a?(Hash)
options = args[1]
column_name = args.first
else
# Handle legacy paramter options: def count(conditions=nil, joins=nil)
options.merge!(:conditions => args[0]) if args.length > 0
options.merge!(:joins => args[1]) if args.length > 1
end
else
raise(ArgumentError, "Unexpected parameters passed to count(*args): expected either count(conditions=nil, joins=nil) or count(options={})")
end
if options[:include] || scope(:find, :include)
count_with_associations(options)
else
calculate(:count, column_name, options)
end
calculate(:count, *construct_count_options_from_legacy_args(*args))
end
# Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options.
@ -136,44 +114,115 @@ module ActiveRecord
column_name = options[:select] if options[:select]
column_name = '*' if column_name == :all
column = column_for column_name
aggregate = select_aggregate(operation, column_name, options)
aggregate_alias = column_alias_for(operation, column_name)
if options[:group]
execute_grouped_calculation(operation, column_name, column, aggregate, aggregate_alias, options)
else
execute_simple_calculation(operation, column_name, column, aggregate, aggregate_alias, options)
catch :invalid_query do
if options[:group]
return execute_grouped_calculation(operation, column_name, column, options)
else
return execute_simple_calculation(operation, column_name, column, options)
end
end
0
end
protected
def construct_calculation_sql(aggregate, aggregate_alias, options) #:nodoc:
scope = scope(:find)
sql = "SELECT #{aggregate} AS #{aggregate_alias}"
def construct_count_options_from_legacy_args(*args)
options = {}
column_name = :all
# We need to handle
# count()
# count(options={})
# count(column_name=:all, options={})
# count(conditions=nil, joins=nil) # deprecated
if args.size > 2
raise ArgumentError, "Unexpected parameters passed to count(options={}): #{args.inspect}"
elsif args.size > 0
if args[0].is_a?(Hash)
options = args[0]
elsif args[1].is_a?(Hash)
column_name, options = args
else
# Deprecated count(conditions, joins=nil)
ActiveSupport::Deprecation.warn(
"You called count(#{args[0].inspect}, #{args[1].inspect}), which is a deprecated API call. " +
"Instead you should use count(column_name, options). Passing the conditions and joins as " +
"string parameters will be removed in Rails 2.0.", caller(2)
)
options.merge!(:conditions => args[0])
options.merge!(:joins => args[1]) if args[1]
end
end
[column_name, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
operation = operation.to_s.downcase
options = options.symbolize_keys
scope = scope(:find)
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
aggregate_alias = column_alias_for(operation, column_name)
use_workaround = !Base.connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count'
join_dependency = nil
if merged_includes.any? && operation.to_s.downcase == 'count'
options[:distinct] = true
column_name = options[:select] || [table_name, primary_key] * '.'
end
sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
sql << " FROM #{table_name} "
if merged_includes.any?
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
sql << " GROUP BY #{options[:group_field]}" if options[:group]
sql << " HAVING #{options[:having]}" if options[:group] && options[:having]
sql << " ORDER BY #{options[:order]}" if options[:order]
add_limit!(sql, options)
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
if options[:group]
group_key = Base.connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
sql << " GROUP BY #{options[group_key]} "
end
if options[:group] && options[:having]
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if Base.connection.adapter_name == 'FrontBase'
options[:having].downcase!
options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
sql << " HAVING #{options[:having]} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
sql << ')' if use_workaround
sql
end
def execute_simple_calculation(operation, column_name, column, aggregate, aggregate_alias, options) #:nodoc:
value = connection.select_value(construct_calculation_sql(aggregate, aggregate_alias, options))
def execute_simple_calculation(operation, column_name, column, options) #:nodoc:
value = connection.select_value(construct_calculation_sql(operation, column_name, options))
type_cast_calculated_value(value, column, operation)
end
def execute_grouped_calculation(operation, column_name, column, aggregate, aggregate_alias, options) #:nodoc:
def execute_grouped_calculation(operation, column_name, column, options) #:nodoc:
group_attr = options[:group].to_s
association = reflect_on_association(group_attr.to_sym)
associated = association && association.macro == :belongs_to # only count belongs_to associations
group_field = (associated ? "#{options[:group]}_id" : options[:group]).to_s
group_alias = column_alias_for(group_field)
group_column = column_for group_field
sql = construct_calculation_sql(aggregate, aggregate_alias, options.merge(:group_field => group_field, :group_alias => group_alias))
sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias))
calculated_data = connection.select_all(sql)
aggregate_alias = column_alias_for(operation, column_name)
if association
key_ids = calculated_data.collect { |row| row[group_alias] }
@ -181,7 +230,7 @@ module ActiveRecord
key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
end
calculated_data.inject(OrderedHash.new) do |all, row|
calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
key = associated ? key_records[row[group_alias].to_i] : type_cast_calculated_value(row[group_alias], group_column)
value = row[aggregate_alias]
all << [key, type_cast_calculated_value(value, column, operation)]
@ -190,15 +239,7 @@ module ActiveRecord
private
def validate_calculation_options(operation, options = {})
if operation.to_s == 'count'
options.assert_valid_keys(CALCULATIONS_OPTIONS + [:include])
else
options.assert_valid_keys(CALCULATIONS_OPTIONS)
end
end
def select_aggregate(operation, column_name, options)
"#{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name})"
options.assert_valid_keys(CALCULATIONS_OPTIONS)
end
# converts a given key to the value that the database adapter returns as
@ -208,7 +249,7 @@ module ActiveRecord
# count(distinct users.id) #=> count_distinct_users_id
# count(*) #=> count_all
def column_alias_for(*keys)
keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_')
connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_'))
end
def column_for(field)

View file

@ -158,6 +158,12 @@ module ActiveRecord
# after_initialize will only be run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the
# callback types will be called.
#
# == before_validation* returning statements
#
# If the returning value of a before_validation callback can be evaluated to false, the process will be aborted and Base#save will return false.
# If Base#save! is called it will raise a RecordNotSave error.
# Nothing will be appended to the errors object.
#
# == Cancelling callbacks
#
# If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns
@ -170,34 +176,17 @@ module ActiveRecord
after_validation_on_update before_destroy after_destroy
)
def self.append_features(base) #:nodoc:
super
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
class << self
include Observable
alias_method :instantiate_without_callbacks, :instantiate
alias_method :instantiate, :instantiate_with_callbacks
alias_method_chain :instantiate, :callbacks
end
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
[:initialize, :create_or_update, :valid?, :create, :update, :destroy].each do |method|
alias_method_chain method, :callbacks
end
end
CALLBACKS.each do |method|
@ -302,12 +291,12 @@ module ActiveRecord
# existing objects that have a record.
def after_validation_on_update() end
def valid_with_callbacks #:nodoc:
def valid_with_callbacks? #:nodoc:
return false if callback(:before_validation) == false
if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
return false if result == false
result = valid_without_callbacks
result = valid_without_callbacks?
callback(:after_validation)
if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end

View file

@ -11,7 +11,7 @@ module ActiveRecord
# Check for activity after at least +verification_timeout+ seconds.
# Defaults to 0 (always check.)
cattr_accessor :verification_timeout
cattr_accessor :verification_timeout, :instance_writer => false
@@verification_timeout = 0
# The class -> [adapter_method, config] map
@ -86,6 +86,16 @@ module ActiveRecord
conn.disconnect!
end
end
# Clears the cache which maps classes
def clear_reloadable_connections!
@@active_connections.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections.delete(name)
end
end
end
# Verify active connections.
def verify_active_connections! #:nodoc:
@ -248,7 +258,8 @@ module ActiveRecord
if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
active_connections[name] = spec
elsif spec.kind_of?(ConnectionSpecification)
self.connection = self.send(spec.adapter_method, spec.config)
config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency)
self.connection = self.send(spec.adapter_method, config)
elsif spec.nil?
raise ConnectionNotEstablished
else

View file

@ -4,11 +4,14 @@ module ActiveRecord
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select_all(sql, name = nil)
select(sql, name)
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(sql, name = nil)
result = select(sql, name)
result.first if result
end
# Returns a single value from a record
@ -25,19 +28,24 @@ module ActiveRecord
end
# Executes the SQL statement in the context of this connection.
# This abstract method raises a NotImplementedError.
def execute(sql, name = nil)
raise NotImplementedError, "execute is an abstract method"
end
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
raise NotImplementedError, "insert is an abstract method"
end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil) end
def update(sql, name = nil)
execute(sql, name)
end
# Executes the delete statement and returns the number of rows affected.
def delete(sql, name = nil) end
def delete(sql, name = nil)
update(sql, name)
end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
@ -91,6 +99,17 @@ module ActiveRecord
end
end
# Appends a locking clause to a SQL statement. *Modifies the +sql+ parameter*.
# # SELECT * FROM suppliers FOR UPDATE
# add_lock! 'SELECT * FROM suppliers', :lock => true
# add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
def add_lock!(sql, options)
case lock = options[:lock]
when true: sql << ' FOR UPDATE'
when String: sql << " #{lock}"
end
end
def default_sequence_name(table, column)
nil
end
@ -99,6 +118,13 @@ module ActiveRecord
def reset_sequence!(table, column, sequence = nil)
# Do nothing by default. Implement for PostgreSQL, Oracle, ...
end
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil)
raise NotImplementedError, "select is an abstract method"
end
end
end
end

View file

@ -4,22 +4,29 @@ module ActiveRecord
# Quotes the column value to help prevent
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
def quote(value, column = nil)
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
case value
when String
when String, ActiveSupport::Multibyte::Chars
value = value.to_s
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
"'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
elsif column && [:integer, :float].include?(column.type)
elsif column && [:integer, :float].include?(column.type)
value = column.type == :integer ? value.to_i : value.to_f
value.to_s
else
"'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass then "NULL"
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
when Float, Fixnum, Bignum then value.to_s
when Date then "'#{value.to_s}'"
when Time, DateTime then "'#{quoted_date(value)}'"
else "'#{quote_string(value.to_yaml)}'"
when NilClass then "NULL"
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
when Float, Fixnum, Bignum then value.to_s
# BigDecimals need to be output in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
when Date then "'#{value.to_s}'"
when Time, DateTime then "'#{quoted_date(value)}'"
else "'#{quote_string(value.to_yaml)}'"
end
end

View file

@ -1,10 +1,12 @@
require 'parsedate'
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
# An abstract definition of a column in a table.
class Column
attr_reader :name, :default, :type, :limit, :null, :sql_type
attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
attr_accessor :primary
# Instantiates a new column in the table.
@ -14,22 +16,20 @@ module ActiveRecord
# +sql_type+ is only used to extract the column's length, if necessary. For example, <tt>company_name varchar(<b>60</b>)</tt>.
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type = nil, null = true)
@name, @type, @null = name, simplified_type(sql_type), null
@sql_type = sql_type
# have to do this one separately because type_cast depends on #type
@name, @sql_type, @null = name, sql_type, null
@limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
@type = simplified_type(sql_type)
@default = type_cast(default)
@limit = extract_limit(sql_type) unless sql_type.nil?
@primary = nil
@text = [:string, :text].include? @type
@number = [:float, :integer].include? @type
end
def text?
@text
[:string, :text].include? type
end
def number?
@number
[:float, :integer, :decimal].include? type
end
# Returns the Ruby class that corresponds to the abstract data type.
@ -37,6 +37,7 @@ module ActiveRecord
case type
when :integer then Fixnum
when :float then Float
when :decimal then BigDecimal
when :datetime then Time
when :date then Date
when :timestamp then Time
@ -55,6 +56,7 @@ module ActiveRecord
when :text then value
when :integer then value.to_i rescue value ? 1 : 0
when :float then value.to_f
when :decimal then self.class.value_to_decimal(value)
when :datetime then self.class.string_to_time(value)
when :timestamp then self.class.string_to_time(value)
when :time then self.class.string_to_dummy_time(value)
@ -71,6 +73,7 @@ module ActiveRecord
when :text then nil
when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
when :float then "#{var_name}.to_f"
when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
when :datetime then "#{self.class.name}.string_to_time(#{var_name})"
when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})"
@ -108,39 +111,74 @@ module ActiveRecord
def self.string_to_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string)[0..5]
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
time_array = time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)
# treat 0000-00-00 00:00:00 as nil
Time.send(Base.default_timezone, *time_array) rescue nil
Time.send(Base.default_timezone, *time_array) rescue DateTime.new(*time_array[0..5]) rescue nil
end
def self.string_to_dummy_time(string)
return string unless string.is_a?(String)
time_array = ParseDate.parsedate(string)
return nil if string.empty?
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
# pad the resulting array with dummy date information
time_array[0] = 2000; time_array[1] = 1; time_array[2] = 1;
time_array = [2000, 1, 1]
time_array += time_hash.values_at(:hour, :min, :sec, :sec_fraction)
Time.send(Base.default_timezone, *time_array) rescue nil
end
# convert something to a boolean
def self.value_to_boolean(value)
return value if value==true || value==false
case value.to_s.downcase
when "true", "t", "1" then true
else false
if value == true || value == false
value
else
%w(true t 1).include?(value.to_s.downcase)
end
end
private
# convert something to a BigDecimal
def self.value_to_decimal(value)
if value.is_a?(BigDecimal)
value
elsif value.respond_to?(:to_d)
value.to_d
else
value.to_s.to_d
end
end
private
# '0.123456' -> 123456
# '1.123456' -> 123456
def self.microseconds(time)
((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
end
def extract_limit(sql_type)
$1.to_i if sql_type =~ /\((.*)\)/
end
def extract_precision(sql_type)
$2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
end
def extract_scale(sql_type)
case sql_type
when /^(numeric|decimal|number)\((\d+)\)/i then 0
when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
end
end
def simplified_type(field_type)
case field_type
when /int/i
:integer
when /float|double|decimal|numeric/i
when /float|double/i
:float
when /decimal|numeric|number/i
extract_scale(field_type) == 0 ? :integer : :decimal
when /datetime/i
:datetime
when /timestamp/i
@ -164,18 +202,20 @@ module ActiveRecord
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
end
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :default, :null) #:nodoc:
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
def sql_type
base.type_to_sql(type.to_sym, limit, precision, scale) rescue type
end
def to_sql
column_sql = "#{base.quote_column_name(name)} #{type_to_sql(type.to_sym, limit)}"
add_column_options!(column_sql, :null => null, :default => default)
column_sql = "#{base.quote_column_name(name)} #{sql_type}"
add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key
column_sql
end
alias to_s :to_sql
private
def type_to_sql(name, limit)
base.type_to_sql(name, limit) rescue name
end
def add_column_options!(sql, options)
base.add_column_options!(sql, options.merge(:column => self))
@ -195,7 +235,7 @@ module ActiveRecord
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
def primary_key(name)
column(name, native[:primary_key])
column(name, :primary_key)
end
# Returns a ColumnDefinition for the column with name +name+.
@ -206,37 +246,81 @@ module ActiveRecord
# Instantiates a new column for the table.
# The +type+ parameter must be one of the following values:
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
# <tt>:integer</tt>, <tt>:float</tt>, <tt>:datetime</tt>,
# <tt>:timestamp</tt>, <tt>:time</tt>, <tt>:date</tt>,
# <tt>:binary</tt>, <tt>:boolean</tt>.
# <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
# <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
# <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt>:
# Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
# <tt>:binary</tt> or <tt>:integer</tt> columns only)
# * <tt>:default</tt>:
# The column's default value. You cannot explicitely set the default
# value to +NULL+. Simply leave off this option if you want a +NULL+
# default value.
# The column's default value. Use nil for NULL.
# * <tt>:null</tt>:
# Allows or disallows +NULL+ values in the column. This option could
# have been named <tt>:null_allowed</tt>.
# * <tt>:precision</tt>:
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt>:
# Specifies the scale for a <tt>:decimal</tt> column.
#
# Please be aware of different RDBMS implementations behavior with
# <tt>:decimal</tt> columns:
# * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
# <tt>:precision</tt>, and makes no comments about the requirements of
# <tt>:precision</tt>.
# * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
# Default is (10,0).
# * PostgreSQL: <tt>:precision</tt> [1..infinity],
# <tt>:scale</tt> [0..infinity]. No default.
# * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
# Internal storage as strings. No default.
# * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
# but the maximum supported <tt>:precision</tt> is 16. No default.
# * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
# Default is (38,0).
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
# Default unknown.
# * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
# Default (9,0). Internal types NUMERIC and DECIMAL have different
# storage rules, decimal being better.
# * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
# NUMERIC is 19, and DECIMAL is 38.
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
# * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
# * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
#
# This method returns <tt>self</tt>.
#
# ===== Examples
# # Assuming def is an instance of TableDefinition
# def.column(:granted, :boolean)
# # Assuming td is an instance of TableDefinition
# td.column(:granted, :boolean)
# #=> granted BOOLEAN
#
# def.column(:picture, :binary, :limit => 2.megabytes)
# td.column(:picture, :binary, :limit => 2.megabytes)
# #=> picture BLOB(2097152)
#
# def.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
# td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
# #=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
#
# def.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
# #=> bill_gates_money DECIMAL(15,2)
#
# def.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
# #=> sensor_reading DECIMAL(30,20)
#
# # While <tt>:scale</tt> defaults to zero on most databases, it
# # probably wouldn't hurt to include it.
# def.column(:huge_integer, :decimal, :precision => 30)
# #=> huge_integer DECIMAL(30)
def column(name, type, options = {})
column = self[name] || ColumnDefinition.new(@base, name, type)
column.limit = options[:limit] || native[type.to_sym][:limit] if options[:limit] or native[type.to_sym]
column.precision = options[:precision]
column.scale = options[:scale]
column.default = options[:default]
column.null = options[:null]
@columns << column unless @columns.include? column

View file

@ -44,8 +44,8 @@ module ActiveRecord
#
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
# Set to true or false to add/not add a primary key column
# automatically. Defaults to true.
# Whether to automatically add a primary key column. Defaults to true.
# Join tables for has_and_belongs_to_many should set :id => false.
# [<tt>:primary_key</tt>]
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+.
@ -94,7 +94,7 @@ module ActiveRecord
yield table_definition
if options[:force]
drop_table(name) rescue nil
drop_table(name, options) rescue nil
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
@ -112,14 +112,14 @@ module ActiveRecord
end
# Drops a table from the database.
def drop_table(name)
def drop_table(name, options = {})
execute "DROP TABLE #{name}"
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit])}"
add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
execute(add_column_sql)
end
@ -178,14 +178,14 @@ module ActiveRecord
# ====== Creating a unique index
# add_index(:accounts, [:branch_id, :party_id], :unique => true)
# generates
# CREATE UNIQUE INDEX accounts_branch_id_index ON accounts(branch_id, party_id)
# CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
# ====== Creating a named index
# add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
# generates
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
def add_index(table_name, column_name, options = {})
column_names = Array(column_name)
index_name = index_name(table_name, :column => column_names.first)
index_name = index_name(table_name, :column => column_names)
if Hash === options # legacy support, since this param was a string
index_type = options[:unique] ? "UNIQUE" : ""
@ -199,16 +199,14 @@ module ActiveRecord
# Remove the given index from the table.
#
# Remove the suppliers_name_index in the suppliers table (legacy support, use the second or third forms).
# Remove the suppliers_name_index in the suppliers table.
# remove_index :suppliers, :name
# Remove the index named accounts_branch_id in the accounts table.
# Remove the index named accounts_branch_id_index in the accounts table.
# remove_index :accounts, :column => :branch_id
# Remove the index named accounts_branch_id_party_id_index in the accounts table.
# remove_index :accounts, :column => [:branch_id, :party_id]
# Remove the index named by_branch_party in the accounts table.
# remove_index :accounts, :name => :by_branch_party
#
# You can remove an index on multiple columns by specifying the first column.
# add_index :accounts, [:username, :password]
# remove_index :accounts, :username
def remove_index(table_name, options = {})
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))} ON #{table_name}"
end
@ -216,14 +214,14 @@ module ActiveRecord
def index_name(table_name, options) #:nodoc:
if Hash === options # legacy support
if options[:column]
"#{table_name}_#{options[:column]}_index"
"index_#{table_name}_on_#{Array(options[:column]) * '_and_'}"
elsif options[:name]
options[:name]
else
raise ArgumentError, "You must specify the index name"
end
else
"#{table_name}_#{options}_index"
index_name(table_name, :column => options)
end
end
@ -254,18 +252,52 @@ module ActiveRecord
end
def type_to_sql(type, limit = nil) #:nodoc:
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
native = native_database_types[type]
limit ||= native[:limit]
column_type_sql = native[:name]
column_type_sql << "(#{limit})" if limit
column_type_sql
end
column_type_sql = native.is_a?(Hash) ? native[:name] : native
if type == :decimal # ignore limit, use precison and scale
precision ||= native[:precision]
scale ||= native[:scale]
if precision
if scale
column_type_sql << "(#{precision},#{scale})"
else
column_type_sql << "(#{precision})"
end
else
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified" if scale
end
column_type_sql
else
limit ||= native[:limit]
column_type_sql << "(#{limit})" if limit
column_type_sql
end
end
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil?
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
sql << " NOT NULL" if options[:null] == false
end
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
# Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
#
# distinct("posts.id", "posts.created_at desc")
def distinct(columns, order_by)
"DISTINCT #{columns}"
end
# ORDER BY clause for the passed order option.
# PostgreSQL overrides this due to its stricter standards compliance.
def add_order_by_for_association_limiting!(sql, options)
sql << "ORDER BY #{options[:order]}"
end
protected
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
end
end
end

View file

@ -1,5 +1,7 @@
require 'benchmark'
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
@ -77,6 +79,12 @@ module ActiveRecord
@active = false
end
# Returns true if its safe to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
# Lazily verify this connection, calling +active?+ only if it hasn't
# been called for +timeout+ seconds.
def verify!(timeout)

View file

@ -47,14 +47,6 @@ begin
end
end
end
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
select(sql, name).first
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
execute(sql, name = nil)
@ -72,9 +64,6 @@ begin
rows_affected
end
alias_method :update, :execute
alias_method :delete, :execute
def begin_db_transaction
@connection.set_auto_commit_off
end
@ -162,6 +151,7 @@ begin
:text => { :name => 'clob', :limit => 32768 },
:integer => { :name => 'int' },
:float => { :name => 'float' },
:decimal => { :name => 'decimal' },
:datetime => { :name => 'timestamp' },
:timestamp => { :name => 'timestamp' },
:time => { :name => 'time' },

View file

@ -3,16 +3,20 @@
require 'active_record/connection_adapters/abstract_adapter'
module FireRuby # :nodoc: all
NON_EXISTENT_DOMAIN_ERROR = "335544569"
class Database
def self.new_from_params(database, host, port, service)
db_string = ""
if host
db_string << host
db_string << "/#{service || port}" if service || port
db_string << ":"
def self.db_string_for(config)
unless config.has_key?(:database)
raise ArgumentError, "No database specified. Missing argument: database."
end
db_string << database
new(db_string)
host_string = config.values_at(:host, :service, :port).compact.first(2).join("/") if config[:host]
[host_string, config[:database]].join(":")
end
def self.new_from_config(config)
db = new db_string_for(config)
db.character_set = config[:charset]
return db
end
end
end
@ -26,13 +30,9 @@ module ActiveRecord
'The Firebird adapter requires FireRuby version 0.4.0 or greater; you appear ' <<
'to be running an older version -- please update FireRuby (gem install fireruby).'
end
config = config.symbolize_keys
unless config.has_key?(:database)
raise ArgumentError, "No database specified. Missing argument: database."
end
options = config[:charset] ? { CHARACTER_SET => config[:charset] } : {}
connection_params = [config[:username], config[:password], options]
db = FireRuby::Database.new_from_params(*config.values_at(:database, :host, :port, :service))
config.symbolize_keys!
db = FireRuby::Database.new_from_config(config)
connection_params = config.values_at(:username, :password)
connection = db.connect(*connection_params)
ConnectionAdapters::FirebirdAdapter.new(connection, logger, connection_params)
end
@ -45,10 +45,12 @@ module ActiveRecord
def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
@firebird_type = FireRuby::SQLType.to_base_type(type, sub_type).to_s
super(name.downcase, nil, @firebird_type, !null_flag)
@default = parse_default(default_source) if default_source
@limit = type == 'BLOB' ? BLOB_MAX_LENGTH : length
@domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale
@limit = decide_limit(length)
@domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale.abs
end
def type
@ -61,27 +63,12 @@ module ActiveRecord
end
end
# Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
# This enables Firebird to provide an actual value when context variables are used as column
# defaults (such as CURRENT_TIMESTAMP).
def default
if @default
sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
connection = ActiveRecord::Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Firebird' }
if connection
type_cast connection.execute(sql).to_a.first['CAST']
else
raise ConnectionNotEstablished, "No Firebird connections established."
end
end
type_cast(decide_default) if @default
end
def type_cast(value)
if type == :boolean
value == true or value == ActiveRecord::ConnectionAdapters::FirebirdAdapter.boolean_domain[:true]
else
super
end
def self.value_to_boolean(value)
%W(#{FirebirdAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
end
private
@ -90,6 +77,35 @@ module ActiveRecord
return $1 unless $1.upcase == "NULL"
end
def decide_default
if @default =~ /^'?(\d*\.?\d+)'?$/ or
@default =~ /^'(.*)'$/ && [:text, :string, :binary, :boolean].include?(type)
$1
else
firebird_cast_default
end
end
# Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
# This enables Firebird to provide an actual value when context variables are used as column
# defaults (such as CURRENT_TIMESTAMP).
def firebird_cast_default
sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
if connection = Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Firebird' }
connection.execute(sql).to_a.first['CAST']
else
raise ConnectionNotEstablished, "No Firebird connections established."
end
end
def decide_limit(length)
if text? or number?
length
elsif @firebird_type == 'BLOB'
BLOB_MAX_LENGTH
end
end
def column_def
case @firebird_type
when 'BLOB' then "VARCHAR(#{VARCHAR_MAX_LENGTH})"
@ -133,7 +149,7 @@ module ActiveRecord
# Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily
# define a +BOOLEAN+ _domain_ for this purpose, e.g.:
#
# CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1));
# CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1) OR VALUE IS NULL);
#
# When the Firebird adapter encounters a column that is based on a domain
# that includes "BOOLEAN" in the domain name, it will attempt to treat
@ -200,8 +216,23 @@ module ActiveRecord
# as column names as well.
#
# === Migrations
# The Firebird adapter does not currently support Migrations. I hope to
# add this feature in the near future.
# The Firebird Adapter now supports Migrations.
#
# ==== Create/Drop Table and Sequence Generators
# Creating or dropping a table will automatically create/drop a
# correpsonding sequence generator, using the default naming convension.
# You can specify a different name using the <tt>:sequence</tt> option; no
# generator is created if <tt>:sequence</tt> is set to +false+.
#
# ==== Rename Table
# The Firebird #rename_table Migration should be used with caution.
# Firebird 1.5 lacks built-in support for this feature, so it is
# implemented by making a copy of the original table (including column
# definitions, indexes and data records), and then dropping the original
# table. Constraints and Triggers are _not_ properly copied, so avoid
# this method if your original table includes constraints (other than
# the primary key) or triggers. (Consider manually copying your table
# or using a view instead.)
#
# == Connection Options
# The following options are supported by the Firebird adapter. None of the
@ -238,10 +269,12 @@ module ActiveRecord
# Specifies the character set to be used by the connection. Refer to
# Firebird documentation for valid options.
class FirebirdAdapter < AbstractAdapter
@@boolean_domain = { :true => 1, :false => 0 }
TEMP_COLUMN_NAME = 'AR$TEMP_COLUMN'
@@boolean_domain = { :name => "d_boolean", :type => "smallint", :true => 1, :false => 0 }
cattr_accessor :boolean_domain
def initialize(connection, logger, connection_params=nil)
def initialize(connection, logger, connection_params = nil)
super(connection, logger)
@connection_params = connection_params
end
@ -250,13 +283,35 @@ module ActiveRecord
'Firebird'
end
def supports_migrations? # :nodoc:
true
end
def native_database_types # :nodoc:
{
:primary_key => "BIGINT NOT NULL PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "blob sub_type text" },
:integer => { :name => "bigint" },
:decimal => { :name => "decimal" },
:numeric => { :name => "numeric" },
:float => { :name => "float" },
:datetime => { :name => "timestamp" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob sub_type 0" },
:boolean => boolean_domain
}
end
# Returns true for Firebird adapter (since Firebird requires primary key
# values to be pre-fetched before insert). See also #next_sequence_value.
def prefetch_primary_key?(table_name = nil)
true
end
def default_sequence_name(table_name, primary_key) # :nodoc:
def default_sequence_name(table_name, primary_key = nil) # :nodoc:
"#{table_name}_seq"
end
@ -276,7 +331,7 @@ module ActiveRecord
end
def quote_column_name(column_name) # :nodoc:
%Q("#{ar_to_fb_case(column_name)}")
%Q("#{ar_to_fb_case(column_name.to_s)}")
end
def quoted_true # :nodoc:
@ -290,12 +345,16 @@ module ActiveRecord
# CONNECTION MANAGEMENT ====================================
def active?
def active? # :nodoc:
not @connection.closed?
end
def reconnect!
@connection.close
def disconnect! # :nodoc:
@connection.close rescue nil
end
def reconnect! # :nodoc:
disconnect!
@connection = @connection.database.connect(*@connection_params)
end
@ -307,8 +366,7 @@ module ActiveRecord
end
def select_one(sql, name = nil) # :nodoc:
result = select(sql, name)
result.nil? ? nil : result.first
select(sql, name).first
end
def execute(sql, name = nil, &block) # :nodoc:
@ -363,8 +421,37 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
def current_database # :nodoc:
file = @connection.database.file.split(':').last
File.basename(file, '.*')
end
def recreate_database! # :nodoc:
sql = "SELECT rdb$character_set_name FROM rdb$database"
charset = execute(sql).to_a.first[0].rstrip
disconnect!
@connection.database.drop(*@connection_params)
FireRuby::Database.create(@connection.database.file,
@connection_params[0], @connection_params[1], 4096, charset)
end
def tables(name = nil) # :nodoc:
sql = "SELECT rdb$relation_name FROM rdb$relations WHERE rdb$system_flag = 0"
execute(sql, name).collect { |row| row[0].rstrip.downcase }
end
def indexes(table_name, name = nil) # :nodoc:
index_metadata(table_name, false, name).inject([]) do |indexes, row|
if indexes.empty? or indexes.last.name != row[0]
indexes << IndexDefinition.new(table_name, row[0].rstrip.downcase, row[1] == 1, [])
end
indexes.last.columns << row[2].rstrip.downcase
indexes
end
end
def columns(table_name, name = nil) # :nodoc:
sql = <<-END_SQL
sql = <<-end_sql
SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
@ -373,7 +460,7 @@ module ActiveRecord
JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}'
ORDER BY r.rdb$field_position
END_SQL
end_sql
execute(sql, name).collect do |field|
field_values = field.values.collect do |value|
case value
@ -386,7 +473,120 @@ module ActiveRecord
end
end
def create_table(name, options = {}) # :nodoc:
begin
super
rescue StatementInvalid
raise unless non_existent_domain_error?
create_boolean_domain
super
end
unless options[:id] == false or options[:sequence] == false
sequence_name = options[:sequence] || default_sequence_name(name)
create_sequence(sequence_name)
end
end
def drop_table(name, options = {}) # :nodoc:
super(name)
unless options[:sequence] == false
sequence_name = options[:sequence] || default_sequence_name(name)
drop_sequence(sequence_name) if sequence_exists?(sequence_name)
end
end
def add_column(table_name, column_name, type, options = {}) # :nodoc:
super
rescue StatementInvalid
raise unless non_existent_domain_error?
create_boolean_domain
super
end
def change_column(table_name, column_name, type, options = {}) # :nodoc:
change_column_type(table_name, column_name, type, options)
change_column_position(table_name, column_name, options[:position]) if options.include?(:position)
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
end
def change_column_default(table_name, column_name, default) # :nodoc:
table_name = table_name.to_s.upcase
sql = <<-end_sql
UPDATE rdb$relation_fields f1
SET f1.rdb$default_source =
(SELECT f2.rdb$default_source FROM rdb$relation_fields f2
WHERE f2.rdb$relation_name = '#{table_name}'
AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}'),
f1.rdb$default_value =
(SELECT f2.rdb$default_value FROM rdb$relation_fields f2
WHERE f2.rdb$relation_name = '#{table_name}'
AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}')
WHERE f1.rdb$relation_name = '#{table_name}'
AND f1.rdb$field_name = '#{ar_to_fb_case(column_name.to_s)}'
end_sql
transaction do
add_column(table_name, TEMP_COLUMN_NAME, :string, :default => default)
execute sql
remove_column(table_name, TEMP_COLUMN_NAME)
end
end
def rename_column(table_name, column_name, new_column_name) # :nodoc:
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TO #{new_column_name}"
end
def remove_index(table_name, options) #:nodoc:
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
end
def rename_table(name, new_name) # :nodoc:
if table_has_constraints_or_dependencies?(name)
raise ActiveRecordError,
"Table #{name} includes constraints or dependencies that are not supported by " <<
"the Firebird rename_table migration. Try explicitly removing the constraints/" <<
"dependencies first, or manually renaming the table."
end
transaction do
copy_table(name, new_name)
copy_table_indexes(name, new_name)
end
begin
copy_table_data(name, new_name)
copy_sequence_value(name, new_name)
rescue
drop_table(new_name)
raise
end
drop_table(name)
end
def dump_schema_information # :nodoc:
super << ";\n"
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) # :nodoc:
case type
when :integer then integer_sql_type(limit)
when :float then float_sql_type(limit)
when :string then super(type, limit, precision, scale)
else super(type, limit, precision, scale)
end
end
private
def integer_sql_type(limit)
case limit
when (1..2) then 'smallint'
when (3..4) then 'integer'
else 'bigint'
end
end
def float_sql_type(limit)
limit.to_i <= 4 ? 'float' : 'double precision'
end
def select(sql, name = nil)
execute(sql, name).collect do |row|
hashed_row = {}
@ -398,6 +598,120 @@ module ActiveRecord
end
end
def primary_key(table_name)
if pk_row = index_metadata(table_name, true).to_a.first
pk_row[2].rstrip.downcase
end
end
def index_metadata(table_name, pk, name = nil)
sql = <<-end_sql
SELECT i.rdb$index_name, i.rdb$unique_flag, s.rdb$field_name
FROM rdb$indices i
JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
WHERE i.rdb$relation_name = '#{table_name.to_s.upcase}'
end_sql
if pk
sql << "AND c.rdb$constraint_type = 'PRIMARY KEY'\n"
else
sql << "AND (c.rdb$constraint_type IS NULL OR c.rdb$constraint_type != 'PRIMARY KEY')\n"
end
sql << "ORDER BY i.rdb$index_name, s.rdb$field_position\n"
execute sql, name
end
def change_column_type(table_name, column_name, type, options = {})
sql = "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type_to_sql(type, options[:limit])}"
execute sql
rescue StatementInvalid
raise unless non_existent_domain_error?
create_boolean_domain
execute sql
end
def change_column_position(table_name, column_name, position)
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} POSITION #{position}"
end
def copy_table(from, to)
table_opts = {}
if pk = primary_key(from)
table_opts[:primary_key] = pk
else
table_opts[:id] = false
end
create_table(to, table_opts) do |table|
from_columns = columns(from).reject { |col| col.name == table_opts[:primary_key] }
from_columns.each do |column|
col_opts = [:limit, :default, :null].inject({}) { |opts, opt| opts.merge(opt => column.send(opt)) }
table.column column.name, column.type, col_opts
end
end
end
def copy_table_indexes(from, to)
indexes(from).each do |index|
unless index.name[from.to_s]
raise ActiveRecordError,
"Cannot rename index #{index.name}, because the index name does not include " <<
"the original table name (#{from}). Try explicitly removing the index on the " <<
"original table and re-adding it on the new (renamed) table."
end
options = {}
options[:name] = index.name.gsub(from.to_s, to.to_s)
options[:unique] = index.unique
add_index(to, index.columns, options)
end
end
def copy_table_data(from, to)
execute "INSERT INTO #{to} SELECT * FROM #{from}", "Copy #{from} data to #{to}"
end
def copy_sequence_value(from, to)
sequence_value = FireRuby::Generator.new(default_sequence_name(from), @connection).last
execute "SET GENERATOR #{default_sequence_name(to)} TO #{sequence_value}"
end
def sequence_exists?(sequence_name)
FireRuby::Generator.exists?(sequence_name, @connection)
end
def create_sequence(sequence_name)
FireRuby::Generator.create(sequence_name.to_s, @connection)
end
def drop_sequence(sequence_name)
FireRuby::Generator.new(sequence_name.to_s, @connection).drop
end
def create_boolean_domain
sql = <<-end_sql
CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
end_sql
execute sql rescue nil
end
def table_has_constraints_or_dependencies?(table_name)
table_name = table_name.to_s.upcase
sql = <<-end_sql
SELECT 1 FROM rdb$relation_constraints
WHERE rdb$relation_name = '#{table_name}'
AND rdb$constraint_type IN ('UNIQUE', 'FOREIGN KEY', 'CHECK')
UNION
SELECT 1 FROM rdb$dependencies
WHERE rdb$depended_on_name = '#{table_name}'
AND rdb$depended_on_type = 0
end_sql
!select(sql).empty?
end
def non_existent_domain_error?
$!.message.include? FireRuby::NON_EXISTENT_DOMAIN_ERROR
end
# Maps uppercase Firebird column names to lowercase for ActiveRecord;
# mixed-case columns retain their original case.
def fb_to_ar_case(column_name)

View file

@ -1,15 +1,53 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'set'
module MysqlCompat #:nodoc:
# add all_hashes method to standard mysql-c bindings or pure ruby version
def self.define_all_hashes_method!
raise 'Mysql not loaded' unless defined?(::Mysql)
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
return if target.instance_methods.include?('all_hashes')
# Ruby driver has a version string and returns null values in each_hash
# C driver >= 2.7 returns null values in each_hash
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
target.class_eval <<-'end_eval'
def all_hashes
rows = []
each_hash { |row| rows << row }
rows
end
end_eval
# adapters before 2.7 don't have a version constant
# and don't return null values in each_hash
else
target.class_eval <<-'end_eval'
def all_hashes
rows = []
all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
each_hash { |row| rows << all_fields.dup.update(row) }
rows
end
end_eval
end
unless target.instance_methods.include?('all_hashes')
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
end
end
end
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects.
def self.mysql_connection(config) # :nodoc:
# Only include the MySQL driver if one hasn't already been loaded
def self.require_mysql
# Include the MySQL driver if one hasn't already been loaded
unless defined? Mysql
begin
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
# Use the bundled Ruby/MySQL driver if no driver is already in place
begin
require 'active_record/vendor/mysql'
rescue LoadError
@ -18,6 +56,12 @@ module ActiveRecord
end
end
# Define Mysql::Result.all_hashes
MysqlCompat.define_all_hashes_method!
end
# Establishes a connection to the database that's used by all Active Record objects.
def self.mysql_connection(config) # :nodoc:
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@ -31,20 +75,41 @@ module ActiveRecord
raise ArgumentError, "No database specified. Missing argument: database."
end
require_mysql
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
end
end
module ConnectionAdapters
class MysqlColumn < Column #:nodoc:
TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])
def initialize(name, default, sql_type = nil, null = true)
@original_default = default
super
@default = nil if missing_default_forged_as_empty_string?
end
private
def simplified_type(field_type)
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
return :string if field_type =~ /enum/i
super
end
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string, text, binary) but we can for others (integer,
# datetime, boolean, and the rest).
#
# Test whether the column has default '', is not null, and is not
# a type allowing default ''.
def missing_default_forged_as_empty_string?
!null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
end
end
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
@ -83,7 +148,7 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@null_values_in_each_hash = Mysql.const_defined?(:VERSION)
connect
end
@ -95,13 +160,14 @@ module ActiveRecord
true
end
def native_database_types #:nodoc
def native_database_types #:nodoc:
{
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 11 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
@ -118,6 +184,8 @@ module ActiveRecord
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
"'#{value.to_s("F")}'"
else
super
end
@ -171,16 +239,7 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
def select_all(sql, name = nil) #:nodoc:
select(sql, name)
end
def select_one(sql, name = nil) #:nodoc:
result = select(sql, name)
result.nil? ? nil : result.first
end
def execute(sql, name = nil, retries = 2) #:nodoc:
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
@ -200,9 +259,6 @@ module ActiveRecord
@connection.affected_rows
end
alias_method :delete, :update #:nodoc:
def begin_db_transaction #:nodoc:
execute "BEGIN"
rescue Exception
@ -222,7 +278,7 @@ module ActiveRecord
end
def add_limit_offset!(sql, options) #:nodoc
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
unless offset = options[:offset]
sql << " LIMIT #{limit}"
@ -304,13 +360,15 @@ module ActiveRecord
def change_column_default(table_name, column_name, default) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
change_column(table_name, column_name, current_type, { :default => default })
execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
options[:default] ||= select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
unless options_include_default?(options)
options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
end
change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
execute(change_column_sql)
end
@ -327,28 +385,27 @@ module ActiveRecord
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
end
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
@connection.real_connect(*@connection_options)
execute("SET NAMES '#{encoding}'") if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
execute("SET SQL_AUTO_IS_NULL=0")
end
def select(sql, name = nil)
@connection.query_with_result = true
result = execute(sql, name)
rows = []
if @null_values_in_each_hash
result.each_hash { |row| rows << row }
else
all_fields = result.fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
result.each_hash { |row| rows << all_fields.dup.update(row) }
end
rows = result.all_hashes
result.free
rows
end
def supports_views?
version[0] >= 5
end
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end

View file

@ -32,7 +32,7 @@ module ActiveRecord
private
def simplified_type(field_type)
return :integer if field_type.downcase =~ /long/
return :float if field_type.downcase == "money"
return :decimal if field_type.downcase == "money"
return :binary if field_type.downcase == "object"
super
end
@ -55,7 +55,7 @@ module ActiveRecord
#
# Caveat: Operations involving LIMIT and OFFSET do not yet work!
#
# Maintainer: derrickspell@cdmplus.com
# Maintainer: derrick.spell@gmail.com
class OpenBaseAdapter < AbstractAdapter
def adapter_name
'OpenBase'
@ -68,6 +68,7 @@ module ActiveRecord
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
@ -121,7 +122,7 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
def add_limit_offset!(sql, options) #:nodoc
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
unless offset = options[:offset]
sql << " RETURN RESULTS #{limit}"

View file

@ -41,28 +41,17 @@ begin
self.oracle_connection(config)
end
# Enable the id column to be bound into the sql later, by the adapter's insert method.
# This is preferable to inserting the hard-coded value here, because the insert method
# needs to know the id value explicitly.
alias :attributes_with_quotes_pre_oracle :attributes_with_quotes
def attributes_with_quotes(include_primary_key = true) #:nodoc:
aq = attributes_with_quotes_pre_oracle(include_primary_key)
if connection.class == ConnectionAdapters::OracleAdapter
aq[self.class.primary_key] = ":id" if include_primary_key && aq[self.class.primary_key].nil?
end
aq
end
# After setting large objects to empty, select the OCI8::LOB
# and write back the data.
after_save :write_lobs
after_save :write_lobs
def write_lobs() #:nodoc:
if connection.is_a?(ConnectionAdapters::OracleAdapter)
self.class.columns.select { |c| c.type == :binary }.each { |c|
self.class.columns.select { |c| c.sql_type =~ /LOB$/i }.each { |c|
value = self[c.name]
value = value.to_yaml if unserializable_attribute?(c.name, c)
next if value.nil? || (value == '')
lob = connection.select_one(
"SELECT #{ c.name} FROM #{ self.class.table_name } WHERE #{ self.class.primary_key} = #{quote(id)}",
"SELECT #{c.name} FROM #{self.class.table_name} WHERE #{self.class.primary_key} = #{quote_value(id)}",
'Writable Large Object')[c.name]
lob.write value
}
@ -75,57 +64,21 @@ begin
module ConnectionAdapters #:nodoc:
class OracleColumn < Column #:nodoc:
attr_reader :sql_type
# overridden to add the concept of scale, required to differentiate
# between integer and float fields
def initialize(name, default, sql_type, limit, scale, null)
@name, @limit, @sql_type, @scale, @null = name, limit, sql_type, scale, null
@type = simplified_type(sql_type)
@default = type_cast(default)
@primary = nil
@text = [:string, :text].include? @type
@number = [:float, :integer].include? @type
end
def type_cast(value)
return nil if value.nil? || value =~ /^\s*null\s*$/i
case type
when :string then value
when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0)
when :float then value.to_f
when :datetime then cast_to_date_or_time(value)
when :time then cast_to_time(value)
else value
end
return guess_date_or_time(value) if type == :datetime && OracleAdapter.emulate_dates
super
end
private
def simplified_type(field_type)
return :boolean if OracleAdapter.emulate_booleans && field_type == 'NUMBER(1)'
case field_type
when /char/i : :string
when /num|float|double|dec|real|int/i : @scale == 0 ? :integer : :float
when /date|time/i : @name =~ /_at$/ ? :time : :datetime
when /clob/i : :text
when /blob/i : :binary
when /date|time/i then :datetime
else super
end
end
def cast_to_date_or_time(value)
return value if value.is_a? Date
return nil if value.blank?
guess_date_or_time (value.is_a? Time) ? value : cast_to_time(value)
end
def cast_to_time(value)
return value if value.is_a? Time
time_array = ParseDate.parsedate value
time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1;
Time.send(Base.default_timezone, *time_array) rescue nil
end
def guess_date_or_time(value)
(value.hour == 0 and value.min == 0 and value.sec == 0) ?
Date.new(value.year, value.month, value.day) : value
@ -167,6 +120,12 @@ begin
# * <tt>:database</tt>
class OracleAdapter < AbstractAdapter
@@emulate_booleans = true
cattr_accessor :emulate_booleans
@@emulate_dates = false
cattr_accessor :emulate_dates
def adapter_name #:nodoc:
'Oracle'
end
@ -174,14 +133,15 @@ begin
def supports_migrations? #:nodoc:
true
end
def native_database_types #:nodoc
def native_database_types #:nodoc:
{
:primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
:string => { :name => "VARCHAR2", :limit => 255 },
:text => { :name => "CLOB" },
:integer => { :name => "NUMBER", :limit => 38 },
:float => { :name => "NUMBER" },
:decimal => { :name => "DECIMAL" },
:datetime => { :name => "DATE" },
:timestamp => { :name => "DATE" },
:time => { :name => "DATE" },
@ -205,26 +165,26 @@ begin
name =~ /[A-Z]/ ? "\"#{name}\"" : name
end
def quote_string(string) #:nodoc:
string.gsub(/'/, "''")
def quote_string(s) #:nodoc:
s.gsub(/'/, "''")
end
def quote(value, column = nil) #:nodoc:
if column && column.type == :binary
%Q{empty_#{ column.sql_type rescue 'blob' }()}
if column && [:text, :binary].include?(column.type)
%Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
else
case value
when String : %Q{'#{quote_string(value)}'}
when NilClass : 'null'
when TrueClass : '1'
when FalseClass : '0'
when Numeric : value.to_s
when Date, Time : %Q{'#{value.strftime("%Y-%m-%d %H:%M:%S")}'}
else %Q{'#{quote_string(value.to_yaml)}'}
end
super
end
end
def quoted_true
"1"
end
def quoted_false
"0"
end
# CONNECTION MANAGEMENT ====================================
#
@ -232,7 +192,7 @@ begin
# Returns true if the connection is active.
def active?
# Pings the connection to check if it's still good. Note that an
# #active? method is also available, but that simply returns the
# #active? method is also available, but that simply returns the
# last known state, which isn't good enough if the connection has
# gone stale since the last use.
@connection.ping
@ -258,34 +218,23 @@ begin
#
# see: abstract/database_statements.rb
def select_all(sql, name = nil) #:nodoc:
select(sql, name)
end
def select_one(sql, name = nil) #:nodoc:
result = select_all(sql, name)
result.size > 0 ? result.first : nil
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.exec sql }
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
if pk.nil? # Who called us? What does the sql look like? No idea!
execute sql, name
elsif id_value # Pre-assigned id
log(sql, name) { @connection.exec sql }
else # Assume the sql contains a bind-variable for the id
id_value = select_one("select #{sequence_name}.nextval id from dual")['id']
log(sql, name) { @connection.exec sql, id_value }
end
id_value
# Returns the next sequence value from a sequence generator. Not generally
# called directly; used by ActiveRecord to get the next primary key value
# when inserting a new database record (see #prefetch_primary_key?).
def next_sequence_value(sequence_name)
id = 0
@connection.exec("select #{sequence_name}.nextval id from dual") { |r| id = r[0].to_i }
id
end
alias :update :execute #:nodoc:
alias :delete :execute #:nodoc:
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name)
id_value
end
def begin_db_transaction #:nodoc:
@connection.autocommit = false
@ -313,6 +262,12 @@ begin
end
end
# Returns true for Oracle adapter (since Oracle requires primary key
# values to be pre-fetched before insert). See also #next_sequence_value.
def prefetch_primary_key?(table_name = nil)
true
end
def default_sequence_name(table, column) #:nodoc:
"#{table}_seq"
end
@ -338,7 +293,7 @@ begin
FROM user_indexes i, user_ind_columns c
WHERE i.table_name = '#{table_name.to_s.upcase}'
AND c.index_name = i.index_name
AND i.index_name NOT IN (SELECT index_name FROM user_constraints WHERE constraint_type = 'P')
AND i.index_name NOT IN (SELECT uc.index_name FROM user_constraints uc WHERE uc.constraint_type = 'P')
ORDER BY i.index_name, c.column_position
SQL
@ -360,47 +315,54 @@ begin
def columns(table_name, name = nil) #:nodoc:
(owner, table_name) = @connection.describe(table_name)
table_cols = %Q{
select column_name, data_type, data_default, nullable,
table_cols = <<-SQL
select column_name as name, data_type as sql_type, data_default, nullable,
decode(data_type, 'NUMBER', data_precision,
'FLOAT', data_precision,
'VARCHAR2', data_length,
null) as length,
null) as limit,
decode(data_type, 'NUMBER', data_scale, null) as scale
from all_tab_columns
where owner = '#{owner}'
and table_name = '#{table_name}'
order by column_id
}
SQL
select_all(table_cols, name).map do |row|
limit, scale = row['limit'], row['scale']
if limit || scale
row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
end
# clean up odd default spacing from Oracle
if row['data_default']
row['data_default'].sub!(/^(.*?)\s*$/, '\1')
row['data_default'].sub!(/^'(.*)'$/, '\1')
row['data_default'] = nil if row['data_default'] =~ /^null$/i
end
OracleColumn.new(
oracle_downcase(row['column_name']),
row['data_default'],
row['data_type'],
(l = row['length']).nil? ? nil : l.to_i,
(s = row['scale']).nil? ? nil : s.to_i,
row['nullable'] == 'Y'
)
OracleColumn.new(oracle_downcase(row['name']),
row['data_default'],
row['sql_type'],
row['nullable'] == 'Y')
end
end
def create_table(name, options = {}) #:nodoc:
super(name, options)
execute "CREATE SEQUENCE #{name}_seq START WITH 10000" unless options[:id] == false
seq_name = options[:sequence_name] || "#{name}_seq"
execute "CREATE SEQUENCE #{seq_name} START WITH 10000" unless options[:id] == false
end
def rename_table(name, new_name) #:nodoc:
execute "RENAME #{name} TO #{new_name}"
execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil
end
end
def drop_table(name) #:nodoc:
def drop_table(name, options = {}) #:nodoc:
super(name)
execute "DROP SEQUENCE #{name}_seq" rescue nil
seq_name = options[:sequence_name] || "#{name}_seq"
execute "DROP SEQUENCE #{seq_name}" rescue nil
end
def remove_index(table_name, options = {}) #:nodoc:
@ -412,7 +374,7 @@ begin
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
change_column_sql = "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"
change_column_sql = "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
execute(change_column_sql)
end
@ -425,26 +387,45 @@ begin
execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
end
# Find a table's primary key and sequence.
# *Note*: Only primary key is implemented - sequence will be nil.
def pk_and_sequence_for(table_name)
(owner, table_name) = @connection.describe(table_name)
pks = select_values(<<-SQL, 'Primary Key')
select cc.column_name
from all_constraints c, all_cons_columns cc
where c.owner = '#{owner}'
and c.table_name = '#{table_name}'
and c.constraint_type = 'P'
and cc.owner = c.owner
and cc.constraint_name = c.constraint_name
SQL
# only support single column keys
pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
end
def structure_dump #:nodoc:
s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq|
structure << "create sequence #{seq.to_a.first.last};\n\n"
end
select_all("select table_name from user_tables").inject(s) do |structure, table|
ddl = "create table #{table.to_a.first.last} (\n "
ddl = "create table #{table.to_a.first.last} (\n "
cols = select_all(%Q{
select column_name, data_type, data_length, data_precision, data_scale, data_default, nullable
from user_tab_columns
where table_name = '#{table.to_a.first.last}'
order by column_id
}).map do |row|
col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
}).map do |row|
col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
col << "(#{row['data_precision'].to_i}"
col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
col << ')'
elsif row['data_type'].include?('CHAR')
col << "(#{row['data_length'].to_i})"
col << "(#{row['data_length'].to_i})"
end
col << " default #{row['data_default']}" if !row['data_default'].nil?
col << ' not null' if row['nullable'] == 'N'
@ -466,6 +447,41 @@ begin
end
end
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
#
# Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
# queries. However, with those columns included in the SELECT DISTINCT list, you
# won't actually get a distinct list of the column you want (presuming the column
# has duplicates with multiple values for the ordered-by columns. So we use the
# FIRST_VALUE function to get a single (first) value for each column, effectively
# making every row the same.
#
# distinct("posts.id", "posts.created_at desc")
def distinct(columns, order_by)
return "DISTINCT #{columns}" if order_by.blank?
# construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
# FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
"FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
end
sql = "DISTINCT #{columns}, "
sql << order_columns * ", "
end
# ORDER BY clause for the passed order option.
#
# Uses column aliases as defined by #distinct.
def add_order_by_for_association_limiting!(sql, options)
return sql if options[:order].blank?
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
order.map! {|s| $1 if s =~ / (.*)/}
order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
sql << "ORDER BY #{order}"
end
private
@ -542,7 +558,7 @@ begin
def describe(name)
@desc ||= @@env.alloc(OCIDescribe)
@desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
@desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK)
@desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) rescue raise %Q{"DESC #{name}" failed; does it exist?}
info = @desc.attrGet(OCI_ATTR_PARAM)
case info.attrGet(OCI_ATTR_PTYPE)
@ -554,6 +570,7 @@ begin
schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
name = info.attrGet(OCI_ATTR_NAME)
describe(schema + '.' + name)
else raise %Q{"DESC #{name}" failed; not a table or view.}
end
end
@ -563,11 +580,14 @@ begin
# The OracleConnectionFactory factors out the code necessary to connect and
# configure an Oracle/OCI connection.
class OracleConnectionFactory #:nodoc:
def new_connection(username, password, database)
def new_connection(username, password, database, async, prefetch_rows, cursor_sharing)
conn = OCI8.new username, password, database
conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil
conn.autocommit = true
conn.non_blocking = true if async
conn.prefetch_rows = prefetch_rows
conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
conn
end
end
@ -575,10 +595,10 @@ begin
# The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
# reset functionality. If a call to #exec fails, and autocommit is turned on
# (ie., we're not in the middle of a longer transaction), it will
# (ie., we're not in the middle of a longer transaction), it will
# automatically reconnect and try again. If autocommit is turned off,
# this would be dangerous (as the earlier part of the implied transaction
# may have failed silently if the connection died) -- so instead the
# may have failed silently if the connection died) -- so instead the
# connection is marked as dead, to be reconnected on it's next use.
class OCI8AutoRecover < DelegateClass(OCI8) #:nodoc:
attr_accessor :active
@ -592,9 +612,12 @@ begin
def initialize(config, factory = OracleConnectionFactory.new)
@active = true
@username, @password, @database = config[:username], config[:password], config[:database]
@username, @password, @database, = config[:username], config[:password], config[:database]
@async = config[:allow_concurrency]
@prefetch_rows = config[:prefetch_rows] || 100
@cursor_sharing = config[:cursor_sharing] || 'similar'
@factory = factory
@connection = @factory.new_connection @username, @password, @database
@connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
super @connection
end
@ -613,7 +636,7 @@ begin
def reset!
logoff rescue nil
begin
@connection = @factory.new_connection @username, @password, @database
@connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
__setobj__ @connection
@active = true
rescue
@ -623,7 +646,7 @@ begin
end
# ORA-00028: your session has been killed
# ORA-01012: not logged on
# ORA-01012: not logged on
# ORA-03113: end-of-file on communication channel
# ORA-03114: not connected to ORACLE
LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ]
@ -631,11 +654,11 @@ begin
# Adds auto-recovery functionality.
#
# See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
def exec(sql, *bindvars)
def exec(sql, *bindvars, &block)
should_retry = self.class.auto_retry? && autocommit?
begin
@connection.exec(sql, *bindvars)
@connection.exec(sql, *bindvars, &block)
rescue OCIException => e
raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
@active = false
@ -652,13 +675,14 @@ rescue LoadError
# OCI8 driver is unavailable.
module ActiveRecord # :nodoc:
class Base
@@oracle_error_message = "Oracle/OCI libraries could not be loaded: #{$!.to_s}"
def self.oracle_connection(config) # :nodoc:
# Set up a reasonable error message
raise LoadError, "Oracle/OCI libraries could not be loaded."
raise LoadError, @@oracle_error_message
end
def self.oci_connection(config) # :nodoc:
# Set up a reasonable error message
raise LoadError, "Oracle/OCI libraries could not be loaded."
raise LoadError, @@oracle_error_message
end
end
end

View file

@ -8,7 +8,7 @@ module ActiveRecord
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432 unless host.nil?
port = config[:port] || 5432
username = config[:username].to_s
password = config[:password].to_s
@ -46,6 +46,7 @@ module ActiveRecord
# * <tt>:schema_search_path</tt> -- An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the :schema_order option.
# * <tt>:encoding</tt> -- An optional client encoding that is using in a SET client_encoding TO <encoding> call on connection.
# * <tt>:min_messages</tt> -- An optional client min messages that is using in a SET client_min_messages TO <min_messages> call on connection.
# * <tt>:allow_concurrency</tt> -- If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
class PostgreSQLAdapter < AbstractAdapter
def adapter_name
'PostgreSQL'
@ -54,6 +55,7 @@ module ActiveRecord
def initialize(connection, logger, config = {})
super(connection, logger)
@config = config
@async = config[:allow_concurrency]
configure_connection
end
@ -67,7 +69,7 @@ module ActiveRecord
end
# postgres-pr raises a NoMethodError when querying if no conn is available
rescue PGError, NoMethodError
false
false
end
# Close then reopen the connection.
@ -78,7 +80,7 @@ module ActiveRecord
configure_connection
end
end
def disconnect!
# Both postgres and postgres-pr respond to :close
@connection.close rescue nil
@ -91,6 +93,7 @@ module ActiveRecord
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "timestamp" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
@ -99,11 +102,11 @@ module ActiveRecord
:boolean => { :name => "boolean" }
}
end
def supports_migrations?
true
end
end
def table_alias_length
63
end
@ -122,18 +125,13 @@ module ActiveRecord
%("#{name}")
end
def quoted_date(value)
value.strftime("%Y-%m-%d %H:%M:%S.#{sprintf("%06d", value.usec)}")
end
# DATABASE STATEMENTS ======================================
def select_all(sql, name = nil) #:nodoc:
select(sql, name)
end
def select_one(sql, name = nil) #:nodoc:
result = select(sql, name)
result.first if result
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name)
table = sql.split(" ", 4)[2]
@ -141,20 +139,29 @@ module ActiveRecord
end
def query(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
log(sql, name) do
if @async
@connection.async_query(sql)
else
@connection.query(sql)
end
end
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.exec(sql) }
log(sql, name) do
if @async
@connection.async_exec(sql)
else
@connection.exec(sql)
end
end
end
def update(sql, name = nil) #:nodoc:
execute(sql, name).cmdtuples
end
alias_method :delete, :update #:nodoc:
def begin_db_transaction #:nodoc:
execute "BEGIN"
end
@ -162,12 +169,11 @@ module ActiveRecord
def commit_db_transaction #:nodoc:
execute "COMMIT"
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
end
# SCHEMA STATEMENTS ========================================
# Return the list of all tables in the schema search path.
@ -214,9 +220,9 @@ module ActiveRecord
end
def columns(table_name, name = nil) #:nodoc:
column_definitions(table_name).collect do |name, type, default, notnull|
Column.new(name, default_value(default), translate_field_type(type),
notnull == "f")
column_definitions(table_name).collect do |name, type, default, notnull, typmod|
# typmod now unused as limit, precision, scale all handled by superclass
Column.new(name, default_value(default), translate_field_type(type), notnull == "f")
end
end
@ -261,7 +267,7 @@ module ActiveRecord
def pk_and_sequence_for(table)
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = execute(<<-end_sql, 'PK and serial sequence')[0]
result = query(<<-end_sql, 'PK and serial sequence')[0]
SELECT attr.attname, name.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@ -284,8 +290,8 @@ module ActiveRecord
# Support the 7.x and 8.0 nextval('foo'::text) as well as
# the 8.1+ nextval('foo'::regclass).
# TODO: assumes sequence is in same schema as table.
result = execute(<<-end_sql, 'PK and custom sequence')[0]
SELECT attr.attname, name.nspname, split_part(def.adsrc, '\\\'', 2)
result = query(<<-end_sql, 'PK and custom sequence')[0]
SELECT attr.attname, name.nspname, split_part(def.adsrc, '''', 2)
FROM pg_class t
JOIN pg_namespace name ON (t.relnamespace = name.oid)
JOIN pg_attribute attr ON (t.oid = attrelid)
@ -296,8 +302,9 @@ module ActiveRecord
AND def.adsrc ~* 'nextval'
end_sql
end
# check for existence of . in sequence name as in public.foo_sequence. if it does not exist, join the current namespace
result.last['.'] ? [result.first, result.last] : [result.first, "#{result[1]}.#{result[2]}"]
# check for existence of . in sequence name as in public.foo_sequence. if it does not exist, return unqualified sequence
# We cannot qualify unqualified sequences, as rails doesn't qualify any table access, using the search path
[result.first, result.last]
rescue
nil
end
@ -305,42 +312,107 @@ module ActiveRecord
def rename_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
def add_column(table_name, column_name, type, options = {})
execute("ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}")
execute("ALTER TABLE #{table_name} ALTER #{column_name} SET NOT NULL") if options[:null] == false
change_column_default(table_name, column_name, options[:default]) unless options[:default].nil?
default = options[:default]
notnull = options[:null] == false
# Add the column.
execute("ALTER TABLE #{table_name} ADD COLUMN #{column_name} #{type_to_sql(type, options[:limit])}")
# Set optional default. If not null, update nulls to the new default.
if options_include_default?(options)
change_column_default(table_name, column_name, default)
if notnull
execute("UPDATE #{table_name} SET #{column_name}=#{quote(default, options[:column])} WHERE #{column_name} IS NULL")
end
end
if notnull
execute("ALTER TABLE #{table_name} ALTER #{column_name} SET NOT NULL")
end
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
begin
execute "ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type_to_sql(type, options[:limit])}"
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
rescue ActiveRecord::StatementInvalid
# This is PG7, so we use a more arcane way of doing it.
begin_db_transaction
add_column(table_name, "#{column_name}_ar_tmp", type, options)
execute "UPDATE #{table_name} SET #{column_name}_ar_tmp = CAST(#{column_name} AS #{type_to_sql(type, options[:limit])})"
execute "UPDATE #{table_name} SET #{column_name}_ar_tmp = CAST(#{column_name} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
remove_column(table_name, column_name)
rename_column(table_name, "#{column_name}_ar_tmp", column_name)
commit_db_transaction
end
change_column_default(table_name, column_name, options[:default]) unless options[:default].nil?
end
if options_include_default?(options)
change_column_default(table_name, column_name, options[:default])
end
end
def change_column_default(table_name, column_name, default) #:nodoc:
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT '#{default}'"
execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}"
execute "ALTER TABLE #{table_name} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
end
def remove_index(table_name, options) #:nodoc:
execute "DROP INDEX #{index_name(table_name, options)}"
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
return super unless type.to_s == 'integer'
if limit.nil? || limit == 4
'integer'
elsif limit < 4
'smallint'
else
'bigint'
end
end
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
#
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
#
# distinct("posts.id", "posts.created_at desc")
def distinct(columns, order_by)
return "DISTINCT #{columns}" if order_by.blank?
# construct a clean list of column names from the ORDER BY clause, removing
# any asc/desc modifiers
order_columns = order_by.split(',').collect { |s| s.split.first }
order_columns.delete_if &:blank?
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
# return a DISTINCT ON() clause that's distinct on the columns we want but includes
# all the required columns for the ORDER BY to work properly
sql = "DISTINCT ON (#{columns}) #{columns}, "
sql << order_columns * ', '
end
# ORDER BY clause for the passed order option.
#
# PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
# by wrapping the sql as a sub-select and ordering in that query.
def add_order_by_for_association_limiting!(sql, options)
return sql if options[:order].blank?
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
order.map! { |s| 'DESC' if s =~ /\bdesc$/i }
order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ')
sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}"
end
private
BYTEA_COLUMN_TYPE_OID = 17
NUMERIC_COLUMN_TYPE_OID = 1700
TIMESTAMPOID = 1114
TIMESTAMPTZOID = 1184
@ -367,12 +439,14 @@ module ActiveRecord
hashed_row = {}
row.each_index do |cel_index|
column = row[cel_index]
case res.type(cel_index)
when BYTEA_COLUMN_TYPE_OID
column = unescape_bytea(column)
when TIMESTAMPTZOID, TIMESTAMPOID
column = cast_to_time(column)
when NUMERIC_COLUMN_TYPE_OID
column = column.to_d if column.respond_to?(:to_d)
end
hashed_row[fields[cel_index]] = column
@ -380,6 +454,7 @@ module ActiveRecord
rows << hashed_row
end
end
res.clear
return rows
end
@ -430,7 +505,7 @@ module ActiveRecord
end
unescape_bytea(s)
end
# Query a table's column names, default values, and types.
#
# The underlying query is roughly:
@ -466,11 +541,13 @@ module ActiveRecord
def translate_field_type(field_type)
# Match the beginning of field_type since it may have a size constraint on the end.
case field_type
# PostgreSQL array data types.
when /\[\]$/i then 'string'
when /^timestamp/i then 'datetime'
when /^real|^money/i then 'float'
when /^interval/i then 'string'
# geometric types (the line type is currently not implemented in postgresql)
when /^(?:point|lseg|box|"?path"?|polygon|circle)/i then 'string'
when /^(?:point|lseg|box|"?path"?|polygon|circle)/i then 'string'
when /^bytea/i then 'binary'
else field_type # Pass through standard types.
end
@ -480,16 +557,16 @@ module ActiveRecord
# Boolean types
return "t" if value =~ /true/i
return "f" if value =~ /false/i
# Char/String/Bytea type values
return $1 if value =~ /^'(.*)'::(bpchar|text|character varying|bytea)$/
# Numeric values
return value if value =~ /^-?[0-9]+(\.[0-9]*)?/
# 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
@ -499,7 +576,7 @@ module ActiveRecord
def cast_to_time(value)
return value unless value.class == DateTime
v = value
time_array = [v.year, v.month, v.day, v.hour, v.min, v.sec]
time_array = [v.year, v.month, v.day, v.hour, v.min, v.sec, v.usec]
Time.send(Base.default_timezone, *time_array) rescue nil
end
end

View file

@ -19,7 +19,10 @@ module ActiveRecord
:results_as_hash => true,
:type_translation => false
)
ConnectionAdapters::SQLiteAdapter.new(db, logger)
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
ConnectionAdapters::SQLite3Adapter.new(db, logger)
end
# Establishes a connection to the database that's used by all Active Record objects
@ -98,6 +101,10 @@ module ActiveRecord
def supports_migrations? #:nodoc:
true
end
def requires_reloading?
true
end
def supports_count_distinct? #:nodoc:
false
@ -110,6 +117,7 @@ module ActiveRecord
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "datetime" },
@ -184,6 +192,12 @@ module ActiveRecord
end
# SELECT ... FOR UPDATE is redundant since the table is locked.
def add_lock!(sql, options) #:nodoc:
sql
end
# SCHEMA STATEMENTS ========================================
def tables(name = nil) #:nodoc:
@ -217,13 +231,13 @@ module ActiveRecord
end
def rename_table(name, new_name)
move_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
super(table_name, column_name, type, options)
# See last paragraph on http://www.sqlite.org/lang_altertable.html
execute "VACUUM"
end
def remove_column(table_name, column_name) #:nodoc:
@ -240,10 +254,11 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
include_default = options_include_default?(options)
definition[column_name].instance_eval do
self.type = type
self.limit = options[:limit] if options[:limit]
self.default = options[:default] if options[:default]
self.limit = options[:limit] if options.include?(:limit)
self.default = options[:default] if include_default
end
end
end
@ -306,8 +321,9 @@ module ActiveRecord
elsif from == "altered_#{to}"
name = name[5..-1]
end
opts = { :name => name }
# index name can't be the same
opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
opts[:unique] = true if index.unique
add_index(to, index.columns, opts)
end
@ -316,9 +332,10 @@ module ActiveRecord
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
from_columns = columns(from).collect {|col| col.name}
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
@connection.execute "SELECT * FROM #{from}" do |row|
sql = "INSERT INTO #{to} VALUES ("
sql = "INSERT INTO #{to} ("+columns*','+") VALUES ("
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
sql << ')'
@connection.execute sql
@ -337,6 +354,14 @@ module ActiveRecord
end
end
class SQLite3Adapter < SQLiteAdapter # :nodoc:
def table_structure(table_name)
returning structure = @connection.table_info(table_name) do
raise ActiveRecord::StatementInvalid if structure.empty?
end
end
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
# SQLite 2 does not support COUNT(DISTINCT) queries:
#
@ -359,6 +384,17 @@ module ActiveRecord
sql
end
end
def rename_table(name, new_name)
move_table(name, new_name)
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:

View file

@ -1,5 +1,8 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'bigdecimal'
require 'bigdecimal/util'
# sqlserver_adapter.rb -- ActiveRecord adapter for Microsoft SQL Server
#
# Author: Joey Gibson <joey@joeygibson.com>
@ -10,12 +13,14 @@ require 'active_record/connection_adapters/abstract_adapter'
#
# Modifications (ODBC): Mark Imbriaco <mark.imbriaco@pobox.com>
# Date: 6/26/2005
#
# Current maintainer: Ryan Tomayko <rtomayko@gmail.com>
#
# Modifications (Migrations): Tom Ward <tom@popdog.net>
# Date: 27/10/2005
#
# Modifications (Numerous fixes as maintainer): Ryan Tomayko <rtomayko@gmail.com>
# Date: Up to July 2006
# Current maintainer: Tom Ward <tom@popdog.net>
module ActiveRecord
class Base
@ -45,58 +50,49 @@ module ActiveRecord
end # class Base
module ConnectionAdapters
class ColumnWithIdentity < Column# :nodoc:
attr_reader :identity, :is_special, :scale
class SQLServerColumn < Column# :nodoc:
attr_reader :identity, :is_special
def initialize(name, default, sql_type = nil, is_identity = false, null = true, scale_value = 0)
def initialize(name, default, sql_type = nil, identity = false, null = true) # TODO: check ok to remove scale_value = 0
super(name, default, sql_type, null)
@identity = is_identity
@is_special = sql_type =~ /text|ntext|image/i ? true : false
@scale = scale_value
@identity = identity
@is_special = sql_type =~ /text|ntext|image/i
# TODO: check ok to remove @scale = scale_value
# SQL Server only supports limits on *char and float types
@limit = nil unless @type == :float or @type == :string
end
def simplified_type(field_type)
case field_type
when /int|bigint|smallint|tinyint/i then :integer
when /float|double|decimal|money|numeric|real|smallmoney/i then @scale == 0 ? :integer : :float
when /datetime|smalldatetime/i then :datetime
when /timestamp/i then :timestamp
when /time/i then :time
when /text|ntext/i then :text
when /binary|image|varbinary/i then :binary
when /char|nchar|nvarchar|string|varchar/i then :string
when /bit/i then :boolean
when /uniqueidentifier/i then :string
when /money/i then :decimal
when /image/i then :binary
when /bit/i then :boolean
when /uniqueidentifier/i then :string
else super
end
end
def type_cast(value)
return nil if value.nil? || value =~ /^\s*null\s*$/i
return nil if value.nil?
case type
when :string then value
when :integer then value == true || value == false ? value == true ? 1 : 0 : value.to_i
when :float then value.to_f
when :datetime then cast_to_datetime(value)
when :timestamp then cast_to_time(value)
when :time then cast_to_time(value)
when :date then cast_to_datetime(value)
when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or value.to_s == '1'
else value
else super
end
end
def cast_to_time(value)
return value if value.is_a?(Time)
time_array = ParseDate.parsedate(value)
time_array[0] ||= 2000
time_array[1] ||= 1
time_array[2] ||= 1
Time.send(Base.default_timezone, *time_array) rescue nil
end
def cast_to_datetime(value)
return value.to_time if value.is_a?(DBI::Timestamp)
if value.is_a?(Time)
if value.year != 0 and value.month != 0 and value.day != 0
return value
@ -104,9 +100,24 @@ module ActiveRecord
return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil
end
end
if value.is_a?(DateTime)
return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
end
return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil
value
end
# TODO: Find less hack way to convert DateTime objects into Times
def self.string_to_time(value)
if value.is_a?(DateTime)
return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec)
else
super
end
end
# These methods will only allow the adapter to insert binary data with a length of 7K or less
# because of a SQL Server statement length policy.
@ -187,6 +198,7 @@ module ActiveRecord
:text => { :name => "text" },
:integer => { :name => "int" },
:float => { :name => "float", :limit => 8 },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "datetime" },
@ -204,11 +216,23 @@ module ActiveRecord
true
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
return super unless type.to_s == 'integer'
if limit.nil? || limit == 4
'integer'
elsif limit < 4
'smallint'
else
'bigint'
end
end
# CONNECTION MANAGEMENT ====================================#
# Returns true if the connection is active.
def active?
@connection.execute("SELECT 1") { }
@connection.execute("SELECT 1").finish
true
rescue DBI::DatabaseError, DBI::InterfaceError
false
@ -229,21 +253,25 @@ module ActiveRecord
@connection.disconnect rescue nil
end
def select_all(sql, name = nil)
select(sql, name)
end
def select_one(sql, name = nil)
add_limit!(sql, :limit => 1)
result = select(sql, name)
result.nil? ? nil : result.first
end
def columns(table_name, name = nil)
return [] if table_name.blank?
table_name = table_name.to_s if table_name.is_a?(Symbol)
table_name = table_name.split('.')[-1] unless table_name.nil?
sql = "SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, DATA_TYPE as ColType, IS_NULLABLE As IsNullable, COL_LENGTH('#{table_name}', COLUMN_NAME) as Length, COLUMNPROPERTY(OBJECT_ID('#{table_name}'), COLUMN_NAME, 'IsIdentity') as IsIdentity, NUMERIC_SCALE as Scale FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '#{table_name}'"
table_name = table_name.gsub(/[\[\]]/, '')
sql = %Q{
SELECT
cols.COLUMN_NAME as ColName,
cols.COLUMN_DEFAULT as DefaultValue,
cols.NUMERIC_SCALE as numeric_scale,
cols.NUMERIC_PRECISION as numeric_precision,
cols.DATA_TYPE as ColType,
cols.IS_NULLABLE As IsNullable,
COL_LENGTH(cols.TABLE_NAME, cols.COLUMN_NAME) as Length,
COLUMNPROPERTY(OBJECT_ID(cols.TABLE_NAME), cols.COLUMN_NAME, 'IsIdentity') as IsIdentity,
cols.NUMERIC_SCALE as Scale
FROM INFORMATION_SCHEMA.COLUMNS cols
WHERE cols.TABLE_NAME = '#{table_name}'
}
# Comment out if you want to have the Columns select statment logged.
# Personally, I think it adds unnecessary bloat to the log.
# If you do comment it out, make sure to un-comment the "result" line that follows
@ -252,63 +280,49 @@ module ActiveRecord
columns = []
result.each do |field|
default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue]
type = "#{field[:ColType]}(#{field[:Length]})"
if field[:ColType] =~ /numeric|decimal/i
type = "#{field[:ColType]}(#{field[:numeric_precision]},#{field[:numeric_scale]})"
else
type = "#{field[:ColType]}(#{field[:Length]})"
end
is_identity = field[:IsIdentity] == 1
is_nullable = field[:IsNullable] == 'YES'
columns << ColumnWithIdentity.new(field[:ColName], default, type, is_identity, is_nullable, field[:Scale])
columns << SQLServerColumn.new(field[:ColName], default, type, is_identity, is_nullable)
end
columns
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = 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
raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON"
end
end
end
log(sql, name) do
@connection.execute(sql)
id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"]
end
ensure
if ii_enabled
begin
execute enable_identity_insert(table_name, false)
rescue Exception => e
raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF"
end
end
end
end
def execute(sql, name = nil)
if sql =~ /^\s*INSERT/i
insert(sql, name)
elsif sql =~ /^\s*UPDATE|^\s*DELETE/i
log(sql, name) do
@connection.execute(sql)
retVal = select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"]
end
else
log(sql, name) { @connection.execute(sql) }
end
execute(sql, name)
id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"]
end
def update(sql, name = nil)
execute(sql, name)
execute(sql, name) do |handle|
handle.rows
end || select_one("SELECT @@ROWCOUNT AS AffectedRows")["AffectedRows"]
end
alias_method :delete, :update
def execute(sql, name = nil)
if sql =~ /^\s*INSERT/i && (table_name = query_requires_identity_insert?(sql))
log(sql, name) do
with_identity_insert_enabled(table_name) do
@connection.execute(sql) do |handle|
yield(handle) if block_given?
end
end
end
else
log(sql, name) do
@connection.execute(sql) do |handle|
yield(handle) if block_given?
end
end
end
end
def begin_db_transaction
@connection["AutoCommit"] = false
rescue Exception => e
@ -328,20 +342,14 @@ module ActiveRecord
end
def quote(value, column = nil)
return value.quoted_id if value.respond_to?(:quoted_id)
case value
when String
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
"'#{quote_string(column.class.string_to_binary(value))}'"
else
"'#{quote_string(value)}'"
end
when NilClass then "NULL"
when TrueClass then '1'
when FalseClass then '0'
when Float, Fixnum, Bignum then value.to_s
when Date then "'#{value.to_s}'"
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
else "'#{quote_string(value.to_yaml)}'"
when Time, DateTime then "'#{value.strftime("%Y%m%d %H:%M:%S")}'"
when Date then "'#{value.strftime("%Y%m%d")}'"
else super
end
end
@ -349,25 +357,17 @@ module ActiveRecord
string.gsub(/\'/, "''")
end
def quoted_true
"1"
end
def quoted_false
"0"
end
def quote_column_name(name)
"[#{name}]"
end
def add_limit_offset!(sql, options)
if options[:limit] and options[:offset]
total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT\b/i, "SELECT TOP 1000000000")}) tally")[0][:TotalRows].to_i
total_rows = @connection.select_all("SELECT count(*) as TotalRows from (#{sql.gsub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally")[0][:TotalRows].to_i
if (options[:limit] + options[:offset]) >= total_rows
options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
end
sql.sub!(/^\s*SELECT/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT TOP #{options[:limit] + options[:offset]} ")
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]} ")
sql << ") AS tmp1"
if options[:order]
options[:order] = options[:order].split(',').map do |field|
@ -378,7 +378,9 @@ module ActiveRecord
tc << '\\]'
end
if sql =~ /#{tc} AS (t\d_r\d\d?)/
parts[0] = $1
parts[0] = $1
elsif parts[0] =~ /\w+\.(\w+)/
parts[0] = $1
end
parts.join(' ')
end.join(', ')
@ -387,7 +389,7 @@ module ActiveRecord
sql << " ) AS tmp2"
end
elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
sql.sub!(/^\s*SELECT([\s]*distinct)?/i) do
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do
"SELECT#{$1} TOP #{options[:limit]}"
end unless options[:limit].nil?
end
@ -411,42 +413,55 @@ module ActiveRecord
end
def tables(name = nil)
execute("SELECT table_name from information_schema.tables WHERE table_type = 'BASE TABLE'", name).inject([]) do |tables, field|
table_name = field[0]
tables << table_name unless table_name == 'dtproperties'
tables
execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'", name) do |sth|
sth.inject([]) do |tables, field|
table_name = field[0]
tables << table_name unless table_name == 'dtproperties'
tables
end
end
end
def indexes(table_name, name = nil)
indexes = []
execute("EXEC sp_helpindex #{table_name}", name).each do |index|
unique = index[1] =~ /unique/
primary = index[1] =~ /primary key/
if !primary
indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", "))
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = false
indexes = []
execute("EXEC sp_helpindex '#{table_name}'", name) do |sth|
sth.each do |index|
unique = index[1] =~ /unique/
primary = index[1] =~ /primary key/
if !primary
indexes << IndexDefinition.new(table_name, index[0], unique, index[2].split(", "))
end
end
end
indexes
ensure
ActiveRecord::Base.connection.instance_variable_get("@connection")["AutoCommit"] = true
end
def rename_table(name, new_name)
execute "EXEC sp_rename '#{name}', '#{new_name}'"
end
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
# TODO: Add support to mimic date columns, using constraints to mark them as such in the database
# add_column_sql << " CONSTRAINT ck__#{table_name}__#{column_name}__date_only CHECK ( CONVERT(CHAR(12), #{quote_column_name(column_name)}, 14)='00:00:00:000' )" if type == :date
execute(add_column_sql)
end
def rename_column(table, column, new_column_name)
execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'"
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}"]
if options[:default]
sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"]
if options_include_default?(options)
remove_default_constraint(table_name, column_name)
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}"
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{column_name}"
end
sql_commands.each {|c|
execute(c)
@ -454,51 +469,66 @@ module ActiveRecord
end
def remove_column(table_name, column_name)
remove_check_constraints(table_name, column_name)
remove_default_constraint(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
execute "ALTER TABLE [#{table_name}] DROP COLUMN [#{column_name}]"
end
def remove_default_constraint(table_name, column_name)
defaults = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id"
defaults.each {|constraint|
constraints = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id"
constraints.each do |constraint|
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}"
}
end
end
def remove_check_constraints(table_name, column_name)
# TODO remove all constraints in single method
constraints = select "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{table_name}' and COLUMN_NAME = '#{column_name}'"
constraints.each do |constraint|
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["CONSTRAINT_NAME"]}"
end
end
def remove_index(table_name, options = {})
execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}"
execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
end
def type_to_sql(type, limit = nil) #:nodoc:
native = native_database_types[type]
# if there's no :limit in the default type definition, assume that type doesn't support limits
limit = limit || native[:limit]
column_type_sql = native[:name]
column_type_sql << "(#{limit})" if limit
column_type_sql
end
private
private
def select(sql, name = nil)
rows = []
repair_special_columns(sql)
log(sql, name) do
@connection.select_all(sql) do |row|
record = {}
row.column_names.each do |col|
record[col] = row[col]
record[col] = record[col].to_time if record[col].is_a? DBI::Timestamp
result = []
execute(sql) do |handle|
handle.each do |row|
row_hash = {}
row.each_with_index do |value, i|
if value.is_a? DBI::Timestamp
value = DateTime.new(value.year, value.month, value.day, value.hour, value.minute, value.sec)
end
row_hash[handle.column_names[i]] = value
end
rows << record
result << row_hash
end
end
rows
result
end
def enable_identity_insert(table_name, enable = true)
if has_identity_column(table_name)
"SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
end
# Turns IDENTITY_INSERT ON for table during execution of the block
# N.B. This sets the state of IDENTITY_INSERT to OFF after the
# block has been executed without regard to its previous state
def with_identity_insert_enabled(table_name, &block)
set_identity_insert(table_name, true)
yield
ensure
set_identity_insert(table_name, false)
end
def set_identity_insert(table_name, enable = true)
execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
rescue Exception => e
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
end
def get_table_name(sql)
@ -511,11 +541,7 @@ module ActiveRecord
end
end
def has_identity_column(table_name)
!get_identity_column(table_name).nil?
end
def get_identity_column(table_name)
def identity_column(table_name)
@table_columns = {} unless @table_columns
@table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil
@table_columns[table_name].each do |col|
@ -525,8 +551,10 @@ module ActiveRecord
return nil
end
def query_contains_identity_column(sql, col)
sql =~ /\[#{col}\]/
def query_requires_identity_insert?(sql)
table_name = get_table_name(sql)
id_column = identity_column(table_name)
sql =~ /\[#{id_column}\]/ ? table_name : nil
end
def change_order_direction(order)

View file

@ -1,11 +1,20 @@
# sybase_adaptor.rb
# Author: John Sheets <dev@metacasa.net>
# Date: 01 Mar 2006
#
# Based on code from Will Sobel (http://dev.rubyonrails.org/ticket/2030)
#
# Author: John R. Sheets
#
# 01 Mar 2006: Initial version. Based on code from Will Sobel
# (http://dev.rubyonrails.org/ticket/2030)
#
# 17 Mar 2006: Added support for migrations; fixed issues with :boolean columns.
#
#
# 13 Apr 2006: Improved column type support to properly handle dates and user-defined
# types; fixed quoting of integer columns.
#
# 05 Jan 2007: Updated for Rails 1.2 release:
# restricted Fixtures#insert_fixtures monkeypatch to Sybase adapter;
# removed SQL type precision from TEXT type to fix broken
# ActiveRecordStore (jburks, #6878); refactored select() to use execute();
# fixed leaked exception for no-op change_column(); removed verbose SQL dump
# from columns(); added missing scale parameter in normalize_type().
require 'active_record/connection_adapters/abstract_adapter'
@ -35,7 +44,7 @@ module ActiveRecord
ConnectionAdapters::SybaseAdapter.new(
SybSQL.new({'S' => host, 'U' => username, 'P' => password},
ConnectionAdapters::SybaseAdapterContext), database, logger)
ConnectionAdapters::SybaseAdapterContext), database, config, logger)
end
end # class Base
@ -48,7 +57,7 @@ module ActiveRecord
#
# * <tt>:host</tt> -- The name of the database server. No default, must be provided.
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
# * <tt>:username</tt> -- Defaults to sa.
# * <tt>:username</tt> -- Defaults to "sa".
# * <tt>:password</tt> -- Defaults to empty string.
#
# Usage Notes:
@ -75,7 +84,7 @@ module ActiveRecord
# 2> go
class SybaseAdapter < AbstractAdapter # :nodoc:
class ColumnWithIdentity < Column
attr_reader :identity, :primary
attr_reader :identity
def initialize(name, default, sql_type = nil, nullable = nil, identity = nil, primary = nil)
super(name, default, sql_type, nullable)
@ -84,14 +93,15 @@ module ActiveRecord
def simplified_type(field_type)
case field_type
when /int|bigint|smallint|tinyint/i then :integer
when /float|double|decimal|money|numeric|real|smallmoney/i then :float
when /text|ntext/i then :text
when /binary|image|varbinary/i then :binary
when /char|nchar|nvarchar|string|varchar/i then :string
when /bit/i then :boolean
when /datetime|smalldatetime/i then :datetime
else super
when /int|bigint|smallint|tinyint/i then :integer
when /float|double|real/i then :float
when /decimal|money|numeric|smallmoney/i then :decimal
when /text|ntext/i then :text
when /binary|image|varbinary/i then :binary
when /char|nchar|nvarchar|string|varchar/i then :string
when /bit/i then :boolean
when /datetime|smalldatetime/i then :datetime
else super
end
end
@ -106,10 +116,12 @@ module ActiveRecord
end # class ColumnWithIdentity
# Sybase adapter
def initialize(connection, database, logger = nil)
def initialize(connection, database, config = {}, logger = nil)
super(connection, logger)
context = connection.context
context.init(logger)
@config = config
@numconvert = config.has_key?(:numconvert) ? config[:numconvert] : true
@limit = @offset = 0
unless connection.sql_norow("USE #{database}")
raise "Cannot USE #{database}"
@ -123,6 +135,7 @@ module ActiveRecord
:text => { :name => "text" },
:integer => { :name => "int" },
:float => { :name => "float", :limit => 8 },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
@ -132,6 +145,15 @@ module ActiveRecord
}
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
return super unless type.to_s == 'integer'
if !limit.nil? && limit < 4
'smallint'
else
'integer'
end
end
def adapter_name
'Sybase'
end
@ -154,29 +176,6 @@ module ActiveRecord
30
end
# Check for a limit statement and parse out the limit and
# offset if specified. Remove the limit from the sql statement
# and call select.
def select_all(sql, name = nil)
select(sql, name)
end
# Remove limit clause from statement. This will almost always
# contain LIMIT 1 from the caller. set the rowcount to 1 before
# calling select.
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, column|
name, default, type, nullable, identity, primary = column
columns << ColumnWithIdentity.new(name, default, type, nullable, identity, primary)
columns
end
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
begin
table_name = get_table_name(sql)
@ -186,7 +185,7 @@ module ActiveRecord
if col != nil
if query_contains_identity_column(sql, col)
begin
execute enable_identity_insert(table_name, true)
enable_identity_insert(table_name, true)
ii_enabled = true
rescue Exception => e
raise ActiveRecordError, "IDENTITY_INSERT could not be turned ON"
@ -202,7 +201,7 @@ module ActiveRecord
ensure
if ii_enabled
begin
execute enable_identity_insert(table_name, false)
enable_identity_insert(table_name, false)
rescue Exception => e
raise ActiveRecordError, "IDENTITY_INSERT could not be turned OFF"
end
@ -211,45 +210,62 @@ module ActiveRecord
end
def execute(sql, name = nil)
log(sql, name) do
@connection.context.reset
@connection.set_rowcount(@limit || 0)
@limit = @offset = nil
@connection.sql_norow(sql)
if @connection.cmd_fail? or @connection.context.failed?
raise "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}"
end
end
# Return rows affected
raw_execute(sql, name)
@connection.results[0].row_count
end
alias_method :update, :execute
alias_method :delete, :execute
def begin_db_transaction() raw_execute "BEGIN TRAN" end
def commit_db_transaction() raw_execute "COMMIT TRAN" end
def rollback_db_transaction() raw_execute "ROLLBACK TRAN" end
def begin_db_transaction() execute "BEGIN TRAN" end
def commit_db_transaction() execute "COMMIT TRAN" end
def rollback_db_transaction() execute "ROLLBACK TRAN" end
def current_database
select_one("select DB_NAME() as name")["name"]
end
def tables(name = nil)
tables = []
select("select name from sysobjects where type='U'", name).each do |row|
tables << row['name']
end
tables
select("select name from sysobjects where type='U'", name).map { |row| row['name'] }
end
def indexes(table_name, name = nil)
indexes = []
select("exec sp_helpindex #{table_name}", name).each do |index|
select("exec sp_helpindex #{table_name}", name).map do |index|
unique = index["index_description"] =~ /unique/
primary = index["index_description"] =~ /^clustered/
if !primary
cols = index["index_keys"].split(", ").each { |col| col.strip! }
indexes << IndexDefinition.new(table_name, index["index_name"], unique, cols)
IndexDefinition.new(table_name, index["index_name"], unique, cols)
end
end.compact
end
def columns(table_name, name = nil)
sql = <<SQLTEXT
SELECT col.name AS name, type.name AS type, col.prec, col.scale,
col.length, col.status, obj.sysstat2, def.text
FROM sysobjects obj, syscolumns col, systypes type, syscomments def
WHERE obj.id = col.id AND col.usertype = type.usertype AND col.cdefault *= def.id
AND obj.type = 'U' AND obj.name = '#{table_name}' ORDER BY col.colid
SQLTEXT
@logger.debug "Get Column Info for table '#{table_name}'" if @logger
@connection.set_rowcount(0)
@connection.sql(sql)
raise "SQL Command for table_structure for #{table_name} failed\nMessage: #{@connection.context.message}" if @connection.context.failed?
return nil if @connection.cmd_fail?
@connection.top_row_result.rows.map do |row|
name, type, prec, scale, length, status, sysstat2, default = row
name.sub!(/_$/o, '')
type = normalize_type(type, prec, scale, length)
default_value = nil
if default =~ /DEFAULT\s+(.+)/o
default_value = $1.strip
default_value = default_value[1...-1] if default_value =~ /^['"]/o
end
nullable = (status & 8) == 8
identity = status >= 128
primary = (sysstat2 & 8) == 8
ColumnWithIdentity.new(name, default_value, type, nullable, identity, primary)
end
indexes
end
def quoted_true
@ -261,11 +277,13 @@ module ActiveRecord
end
def quote(value, column = nil)
return value.quoted_id if value.respond_to?(:quoted_id)
case value
when String
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
"#{quote_string(column.class.string_to_binary(value))}"
elsif value =~ /^[+-]?[0-9]+$/o
elsif @numconvert && force_numeric?(column) && value =~ /^[+-]?[0-9]+$/o
value
else
"'#{quote_string(value)}'"
@ -273,39 +291,16 @@ module ActiveRecord
when NilClass then (column && column.type == :boolean) ? '0' : "NULL"
when TrueClass then '1'
when FalseClass then '0'
when Float, Fixnum, Bignum then value.to_s
when Date then "'#{value.to_s}'"
when Float, Fixnum, Bignum then force_numeric?(column) ? value.to_s : "'#{value.to_s}'"
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
else "'#{quote_string(value.to_yaml)}'"
else super
end
end
def quote_column(type, value)
case type
when :boolean
case value
when String then value =~ /^[ty]/o ? 1 : 0
when true then 1
when false then 0
else value.to_i
end
when :integer then value.to_i
when :float then value.to_f
when :text, :string, :enum
case value
when String, Symbol, Fixnum, Float, Bignum, TrueClass, FalseClass
"'#{quote_string(value.to_s)}'"
else
"'#{quote_string(value.to_yaml)}'"
end
when :date, :datetime, :time
case value
when Time, DateTime then "'#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
when Date then "'#{value.to_s}'"
else "'#{quote_string(value)}'"
end
else "'#{quote_string(value.to_yaml)}'"
end
# True if column is explicitly declared non-numeric, or
# if column is nil (not specified).
def force_numeric?(column)
(column.nil? || [:integer, :float, :decimal].include?(column.type))
end
def quote_string(s)
@ -313,13 +308,15 @@ module ActiveRecord
end
def quote_column_name(name)
"[#{name}]"
# If column name is close to max length, skip the quotes, since they
# seem to count as part of the length.
((name.to_s.length + 2) <= table_alias_length) ? "[#{name}]" : name.to_s
end
def add_limit_offset!(sql, options) # :nodoc:
@limit = options[:limit]
@offset = options[:offset]
if !normal_select?
if use_temp_table?
# Use temp table to hack offset with Sybase
sql.sub!(/ FROM /i, ' INTO #artemp FROM ')
elsif zero_limit?
@ -335,6 +332,11 @@ module ActiveRecord
end
end
def add_lock!(sql, options) #:nodoc:
@logger.info "Warning: Sybase :lock option '#{options[:lock].inspect}' not supported" if @logger && options.has_key?(:lock)
sql
end
def supports_migrations? #:nodoc:
true
end
@ -348,12 +350,18 @@ module ActiveRecord
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
sql_commands = ["ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"]
if options[:default]
remove_default_constraint(table_name, column_name)
sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{options[:default]} FOR #{column_name}"
begin
execute "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"
rescue StatementInvalid => e
# Swallow exception and reset context if no-op.
raise e unless e.message =~ /no columns to drop, add or modify/
@connection.context.reset
end
if options.has_key?(:default)
remove_default_constraint(table_name, column_name)
execute "ALTER TABLE #{table_name} REPLACE #{column_name} DEFAULT #{quote options[:default]}"
end
sql_commands.each { |c| execute(c) }
end
def remove_column(table_name, column_name)
@ -362,10 +370,10 @@ module ActiveRecord
end
def remove_default_constraint(table_name, column_name)
defaults = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id"
defaults.each {|constraint|
sql = "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id"
select(sql).each do |constraint|
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}"
}
end
end
def remove_index(table_name, options = {})
@ -373,7 +381,7 @@ module ActiveRecord
end
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" unless options[:default].nil?
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
if check_null_for_column?(options[:column], sql)
sql << (options[:null] == false ? " NOT NULL" : " NULL")
@ -381,6 +389,12 @@ module ActiveRecord
sql
end
def enable_identity_insert(table_name, enable = true)
if has_identity_column(table_name)
execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
end
end
private
def check_null_for_column?(col, sql)
# Sybase columns are NOT NULL by default, so explicitly set NULL
@ -424,34 +438,44 @@ module ActiveRecord
end
end
def normal_select?
# If limit is not set at all, we can ignore offset;
# If limit *is* set but offset is zero, use normal select
# with simple SET ROWCOUNT. Thus, only use the temp table
# if limit is set and offset > 0.
has_limit = !@limit.nil?
has_offset = !@offset.nil? && @offset > 0
!has_limit || !has_offset
# If limit is not set at all, we can ignore offset;
# if limit *is* set but offset is zero, use normal select
# with simple SET ROWCOUNT. Thus, only use the temp table
# if limit is set and offset > 0.
def use_temp_table?
!@limit.nil? && !@offset.nil? && @offset > 0
end
def zero_limit?
!@limit.nil? && @limit == 0
end
# Select limit number of rows starting at optional offset.
def select(sql, name = nil)
@connection.context.reset
def raw_execute(sql, name = nil)
log(sql, name) do
if normal_select?
# If limit is not explicitly set, return all results.
@logger.debug "Setting row count to (#{@limit || 'off'})" if @logger
# Run a normal select
@connection.set_rowcount(@limit || 0)
@connection.context.reset
@logger.debug "Setting row count to (#{@limit})" if @logger && @limit
@connection.set_rowcount(@limit || 0)
if sql =~ /^\s*SELECT/i
@connection.sql(sql)
else
@connection.sql_norow(sql)
end
@limit = @offset = nil
if @connection.cmd_fail? or @connection.context.failed?
raise "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}"
end
end
end
# Select limit number of rows starting at optional offset.
def select(sql, name = nil)
if !use_temp_table?
execute(sql, name)
else
log(sql, name) do
# Select into a temp table and prune results
@logger.debug "Selecting #{@limit + (@offset || 0)} or fewer rows into #artemp" if @logger
@connection.context.reset
@connection.set_rowcount(@limit + (@offset || 0))
@connection.sql_norow(sql) # Select into temp table
@logger.debug "Deleting #{@offset || 0} or fewer rows from #artemp" if @logger
@ -462,29 +486,21 @@ module ActiveRecord
end
end
raise StatementInvalid, "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}" if @connection.context.failed? or @connection.cmd_fail?
rows = []
if @connection.context.failed? or @connection.cmd_fail?
raise StatementInvalid, "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}"
else
results = @connection.top_row_result
if results && results.rows.length > 0
fields = fixup_column_names(results.columns)
results.rows.each do |row|
hashed_row = {}
row.zip(fields) { |cell, column| hashed_row[column] = cell }
rows << hashed_row
end
results = @connection.top_row_result
if results && results.rows.length > 0
fields = results.columns.map { |column| column.sub(/_$/, '') }
results.rows.each do |row|
hashed_row = {}
row.zip(fields) { |cell, column| hashed_row[column] = cell }
rows << hashed_row
end
end
@connection.sql_norow("drop table #artemp") if !normal_select?
@connection.sql_norow("drop table #artemp") if use_temp_table?
@limit = @offset = nil
return 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
rows
end
def get_table_name(sql)
@ -502,80 +518,42 @@ module ActiveRecord
end
def get_identity_column(table_name)
@table_columns = {} unless @table_columns
@table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil
@table_columns[table_name].each do |col|
return col.name if col.identity
@id_columns ||= {}
if !@id_columns.has_key?(table_name)
@logger.debug "Looking up identity column for table '#{table_name}'" if @logger
col = columns(table_name).detect { |col| col.identity }
@id_columns[table_name] = col.nil? ? nil : col.name
end
return nil
@id_columns[table_name]
end
def query_contains_identity_column(sql, col)
sql =~ /\[#{col}\]/
end
# Remove trailing _ from names.
def fixup_column_names(columns)
columns.map { |column| column.sub(/_$/, '') }
end
def table_structure(table_name)
sql = <<SQLTEXT
SELECT col.name AS name, type.name AS type, col.prec, col.scale, col.length,
col.status, obj.sysstat2, def.text
FROM sysobjects obj, syscolumns col, systypes type, syscomments def
WHERE obj.id = col.id AND col.usertype = type.usertype AND col.cdefault *= def.id
AND obj.type = 'U' AND obj.name = '#{table_name}' ORDER BY col.colid
SQLTEXT
log(sql, "Get Column Info ") do
@connection.set_rowcount(0)
@connection.sql(sql)
end
if @connection.context.failed?
raise "SQL Command for table_structure for #{table_name} failed\nMessage: #{@connection.context.message}"
elsif !@connection.cmd_fail?
columns = []
results = @connection.top_row_result
results.rows.each do |row|
name, type, prec, scale, length, status, sysstat2, default = row
type = normalize_type(type, prec, scale, length)
default_value = nil
name.sub!(/_$/o, '')
if default =~ /DEFAULT\s+(.+)/o
default_value = $1.strip
default_value = default_value[1...-1] if default_value =~ /^['"]/o
end
nullable = (status & 8) == 8
identity = status >= 128
primary = (sysstat2 & 8) == 8
columns << [name, default_value, type, nullable, identity, primary]
end
columns
else
nil
end
# Resolve all user-defined types (udt) to their fundamental types.
def resolve_type(field_type)
(@udts ||= {})[field_type] ||= select_one("sp_help #{field_type}")["Storage_type"].strip
end
def normalize_type(field_type, prec, scale, length)
if field_type =~ /numeric/i and (scale.nil? or scale == 0)
type = 'int'
has_scale = (!scale.nil? && scale > 0)
type = if field_type =~ /numeric/i and !has_scale
'int'
elsif field_type =~ /money/i
type = 'numeric'
'numeric'
else
type = field_type
resolve_type(field_type.strip)
end
size = ''
if prec
size = "(#{prec})"
elsif length
size = "(#{length})"
end
return type + size
end
def default_value(value)
spec = if prec
has_scale ? "(#{prec},#{scale})" : "(#{prec})"
elsif length && !(type =~ /date|time|text/)
"(#{length})"
else
''
end
"#{type}#{spec}"
end
end # class SybaseAdapter
@ -667,18 +645,18 @@ class Fixtures
alias :original_insert_fixtures :insert_fixtures
def insert_fixtures
values.each do |fixture|
allow_identity_inserts table_name, true
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
allow_identity_inserts table_name, false
if @connection.instance_of?(ActiveRecord::ConnectionAdapters::SybaseAdapter)
values.each do |fixture|
@connection.enable_identity_insert(table_name, true)
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
@connection.enable_identity_insert(table_name, false)
end
else
original_insert_fixtures
end
end
def allow_identity_inserts(table_name, enable)
@connection.execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" rescue nil
end
end
rescue LoadError => cannot_require_sybase
# Couldn't load sybase adapter
end
end

View file

@ -4,65 +4,77 @@ module ActiveRecord
def deprecated_collection_count_method(collection_name)# :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def #{collection_name}_count(force_reload = false)
unless has_attribute?(:#{collection_name}_count)
ActiveSupport::Deprecation.warn :#{collection_name}_count
end
#{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
deprecate :add_#{association_name} => "use #{association_name}.concat instead"
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
deprecate :remove_#{association_name} => "use #{association_name}.delete instead"
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
deprecate :has_#{collection_name}? => "use !#{collection_name}.empty? instead"
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
deprecate :find_in_#{collection_name} => "use #{collection_name}.find instead"
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)
ActiveSupport::Deprecation.silence do
#{collection_name}.find_all(runtime_conditions, orderings, limit, joins)
end
end
deprecate :find_all_in_#{collection_name} => "use #{collection_name}.find(:all, ...) instead"
end_eval
end
def deprecated_collection_create_method(collection_name)# :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def create_in_#{collection_name}(attributes = {})
#{collection_name}.create(attributes)
end
deprecate :create_in_#{collection_name} => "use #{collection_name}.create instead"
end_eval
end
def deprecated_collection_build_method(collection_name)# :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def build_to_#{collection_name}(attributes = {})
#{collection_name}.build(attributes)
end
deprecate :build_to_#{collection_name} => "use #{collection_name}.build instead"
end_eval
end
@ -75,16 +87,18 @@ module ActiveRecord
raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
end
end
deprecate :#{association_name}? => :==
end_eval
end
def deprecated_has_association_method(association_name) # :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def has_#{association_name}?(force_reload = false)
!#{association_name}(force_reload).nil?
end
deprecate :has_#{association_name}? => "use !#{association_name} insead"
end_eval
end
end
end
end
end

View file

@ -10,6 +10,7 @@ module ActiveRecord
def find_on_conditions(ids, conditions) # :nodoc:
find(ids, :conditions => conditions)
end
deprecate :find_on_conditions => "use find(ids, :conditions => conditions)"
# This method is deprecated in favor of find(:first, options).
#
@ -21,6 +22,7 @@ module ActiveRecord
def find_first(conditions = nil, orderings = nil, joins = nil) # :nodoc:
find(:first, :conditions => conditions, :order => orderings, :joins => joins)
end
deprecate :find_first => "use find(:first, ...)"
# This method is deprecated in favor of find(:all, options).
#
@ -36,6 +38,7 @@ module ActiveRecord
limit, offset = limit.is_a?(Array) ? limit : [ limit, nil ]
find(:all, :conditions => conditions, :order => orderings, :joins => joins, :limit => limit, :offset => offset)
end
deprecate :find_all => "use find(:all, ...)"
end
end
end
end

View file

@ -252,7 +252,7 @@ class Fixtures < YAML::Omap
end
all_loaded_fixtures.merge! fixtures_map
connection.transaction do
connection.transaction(Thread.current['open_transactions'] == 0) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
@ -276,6 +276,8 @@ class Fixtures < YAML::Omap
@class_name = class_name ||
(ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
@table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
@table_name = class_name.table_name if class_name.respond_to?(:table_name)
@connection = class_name.connection if class_name.respond_to?(:connection)
read_fixture_files
end
@ -294,21 +296,24 @@ class Fixtures < YAML::Omap
def read_fixture_files
if File.file?(yaml_file_path)
# YAML fixtures
yaml_string = ""
Dir["#{@fixture_path}/**/*.yml"].select {|f| test(?f,f) }.each do |subfixture_path|
yaml_string << IO.read(subfixture_path)
end
yaml_string << IO.read(yaml_file_path)
begin
yaml_string = ""
Dir["#{@fixture_path}/**/*.yml"].select {|f| test(?f,f) }.each do |subfixture_path|
yaml_string << IO.read(subfixture_path)
end
yaml_string << IO.read(yaml_file_path)
if yaml = YAML::load(erb_render(yaml_string))
yaml = yaml.value if yaml.respond_to?(:type_id) and yaml.respond_to?(:value)
yaml.each do |name, data|
self[name] = Fixture.new(data, @class_name)
end
end
yaml = YAML::load(erb_render(yaml_string))
rescue Exception=>boom
raise Fixture::FormatError, "a YAML error occured parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{boom.class}: #{boom}"
raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{boom.class}: #{boom}"
end
if yaml
yaml = yaml.value if yaml.respond_to?(:type_id) and yaml.respond_to?(:value)
yaml.each do |name, data|
unless data
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
end
self[name] = Fixture.new(data, @class_name)
end
end
elsif File.file?(csv_file_path)
# CSV fixtures
@ -368,7 +373,7 @@ class Fixture #:nodoc:
when String
@fixture = read_fixture_file(fixture)
else
raise ArgumentError, "Bad fixture argument #{fixture.inspect}"
raise ArgumentError, "Bad fixture argument #{fixture.inspect} during creation of #{class_name} fixture"
end
@class_name = class_name
@ -392,7 +397,13 @@ class Fixture #:nodoc:
end
def value_list
@fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
klass = @class_name.constantize rescue nil
list = @fixture.inject([]) do |fixtures, (key, value)|
col = klass.columns_hash[key] if klass.kind_of?(ActiveRecord::Base)
fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
end
list * ', '
end
def find
@ -460,7 +471,7 @@ module Test #:nodoc:
file_name = table_name.to_s
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
begin
require file_name
require_dependency file_name
rescue LoadError
# Let's hope the developer has included it himself
end
@ -484,12 +495,13 @@ module Test #:nodoc:
end
def self.uses_transaction(*methods)
@uses_transaction ||= []
@uses_transaction.concat methods.map { |m| m.to_s }
@uses_transaction = [] unless defined?(@uses_transaction)
@uses_transaction.concat methods.map(&:to_s)
end
def self.uses_transaction?(method)
@uses_transaction && @uses_transaction.include?(method.to_s)
@uses_transaction = [] unless defined?(@uses_transaction)
@uses_transaction.include?(method.to_s)
end
def use_transactional_fixtures?
@ -498,6 +510,8 @@ module Test #:nodoc:
end
def setup_with_fixtures
return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@ -512,7 +526,7 @@ module Test #:nodoc:
load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.lock_mutex
ActiveRecord::Base.send :increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
@ -528,10 +542,12 @@ module Test #:nodoc:
alias_method :setup, :setup_with_fixtures
def teardown_with_fixtures
# Rollback changes.
if use_transactional_fixtures?
return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
# Rollback changes if a transaction is active.
if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
ActiveRecord::Base.connection.rollback_db_transaction
ActiveRecord::Base.unlock_mutex
Thread.current['open_transactions'] = 0
end
ActiveRecord::Base.verify_active_connections!
end

View file

@ -64,8 +64,10 @@ module ActiveRecord
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+.
# * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
# named +column_name+ specified to be one of the following types:
# :string, :text, :integer, :float, :datetime, :timestamp, :time, :date, :binary, :boolean. A default value can be specified
# by passing an +options+ hash like { :default => 11 }.
# :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time,
# :date, :binary, :boolean. A default value can be specified by passing an
# +options+ hash like { :default => 11 }. Other options include :limit and :null (e.g. { :limit => 50, :null => false })
# -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
# * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
# parameters as add_column.
@ -156,7 +158,7 @@ module ActiveRecord
# add_column :people, :salary, :integer
# Person.reset_column_information
# Person.find(:all).each do |p|
# p.salary = SalaryCalculator.compute(p)
# p.update_attribute :salary, SalaryCalculator.compute(p)
# end
# end
# end
@ -176,7 +178,7 @@ module ActiveRecord
# ...
# say_with_time "Updating salaries..." do
# Person.find(:all).each do |p|
# p.salary = SalaryCalculator.compute(p)
# p.update_attribute :salary, SalaryCalculator.compute(p)
# end
# end
# ...
@ -381,7 +383,8 @@ module ActiveRecord
end
def reached_target_version?(version)
(up? && version.to_i - 1 == @target_version) || (down? && version.to_i == @target_version)
return false if @target_version == nil
(up? && version.to_i - 1 >= @target_version) || (down? && version.to_i <= @target_version)
end
def irrelevant_migration?(version)

View file

@ -1,10 +1,10 @@
require 'singleton'
require 'set'
module ActiveRecord
module Observing # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
@ -13,18 +13,44 @@ module ActiveRecord
# # Calls PersonObserver.instance
# ActiveRecord::Base.observers = :person_observer
#
# # Calls Cacher.instance and GarbageCollector.instance
# # Calls Cacher.instance and GarbageCollector.instance
# ActiveRecord::Base.observers = :cacher, :garbage_collector
#
# # Same as above, just using explicit class references
# ActiveRecord::Base.observers = Cacher, GarbageCollector
#
# Note: Setting this does not instantiate the observers yet. #instantiate_observers is
# called during startup, and before each development request.
def observers=(*observers)
observers = [ observers ].flatten.each do |observer|
observer.is_a?(Symbol) ?
observer.to_s.camelize.constantize.instance :
@observers = observers.flatten
end
# Gets the current observers.
def observers
@observers ||= []
end
# Instantiate the global ActiveRecord observers
def instantiate_observers
return if @observers.blank?
@observers.each do |observer|
if observer.respond_to?(:to_sym) # Symbol or String
observer.to_s.camelize.constantize.instance
elsif observer.respond_to?(:instance)
observer.instance
else
raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
end
end
end
protected
# Notify observers when the observed class is subclassed.
def inherited(subclass)
super
changed
notify_observers :observed_class_inherited, subclass
end
end
end
@ -85,12 +111,12 @@ module ActiveRecord
# The observer can implement callback methods for each of the methods described in the Callbacks module.
#
# == Storing Observers in Rails
#
#
# If you're using Active Record within Rails, observer classes are usually stored in app/models with the
# naming convention of app/models/audit_observer.rb.
#
# == Configuration
#
#
# In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration setting in your
# <tt>config/environment.rb</tt> file.
#
@ -103,37 +129,50 @@ module ActiveRecord
# Observer subclasses should be reloaded by the dispatcher in Rails
# when Dependencies.mechanism = :load.
include Reloadable::Subclasses
# Attaches the observer to the supplied model classes.
def self.observe(*models)
define_method(:observed_class) { models }
include Reloadable::Deprecated
class << self
# Attaches the observer to the supplied model classes.
def observe(*models)
define_method(:observed_classes) { Set.new(models) }
end
# The class observed by default is inferred from the observer's class name:
# assert_equal [Person], PersonObserver.observed_class
def observed_class
name.scan(/(.*)Observer/)[0][0].constantize
end
end
# Start observing the declared classes and their subclasses.
def initialize
observed_classes = [ observed_class ].flatten
observed_subclasses_class = observed_classes.collect {|c| c.send(:subclasses) }.flatten!
(observed_classes + observed_subclasses_class).each do |klass|
Set.new(observed_classes + observed_subclasses).each { |klass| add_observer! klass }
end
# Send observed_method(object) if the method exists.
def update(observed_method, object) #:nodoc:
send(observed_method, object) if respond_to?(observed_method)
end
# Special method sent by the observed class when it is inherited.
# Passes the new subclass.
def observed_class_inherited(subclass) #:nodoc:
self.class.observe(observed_classes + [subclass])
add_observer!(subclass)
end
protected
def observed_classes
Set.new([self.class.observed_class].flatten)
end
def observed_subclasses
observed_classes.sum(&:subclasses)
end
def add_observer!(klass)
klass.add_observer(self)
klass.send(:define_method, :after_find) unless klass.respond_to?(:after_find)
end
end
def update(callback_method, object) #:nodoc:
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]
klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find)
end
end
end

View file

@ -21,33 +21,46 @@ module ActiveRecord
reflection
end
# Returns a hash containing all AssociationReflection objects for the current class
# Example:
#
# Invoice.reflections
# Account.reflections
#
def reflections
read_inheritable_attribute(:reflections) or write_inheritable_attribute(:reflections, {})
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
end
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
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)
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
end
# Returns an array of AssociationReflection objects for all the aggregations in the class. If you only want to reflect on a
# certain association type, pass in the symbol (:has_many, :has_one, :belongs_to) for that as the first parameter. Example:
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
# certain association type, pass in the symbol (:has_many, :has_one, :belongs_to) for that as the first parameter.
# Example:
#
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
# Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
#
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
@ -147,6 +160,7 @@ module ActiveRecord
# Gets an array of possible :through source reflection names
#
# [singularized, pluralized]
#
def source_reflection_names
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
end
@ -166,7 +180,7 @@ module ActiveRecord
def check_validity!
if options[:through]
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(self)
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
if source_reflection.nil?
@ -174,7 +188,7 @@ module ActiveRecord
end
if source_reflection.options[:polymorphic]
raise HasManyThroughAssociationPolymorphicError.new(class_name, self, source_reflection)
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?

View file

@ -1,3 +1,6 @@
require 'stringio'
require 'bigdecimal'
module ActiveRecord
# This class is used to dump the database schema for some connection to some
# output format (i.e., ActiveRecord::Schema).
@ -82,13 +85,27 @@ HEADER
tbl.print ", :force => true"
tbl.puts " do |t|"
columns.each do |column|
column_specs = columns.map do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
next if column.name == pk
tbl.print " t.column #{column.name.inspect}, #{column.type.inspect}"
tbl.print ", :limit => #{column.limit.inspect}" if column.limit != @types[column.type][:limit]
tbl.print ", :default => #{column.default.inspect}" if !column.default.nil?
tbl.print ", :null => false" if !column.null
spec = {}
spec[:name] = column.name.inspect
spec[:type] = column.type.inspect
spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
spec[:precision] = column.precision.inspect if !column.precision.nil?
spec[:scale] = column.scale.inspect if !column.scale.nil?
spec[:null] = 'false' if !column.null
spec[:default] = default_string(column.default) if !column.default.nil?
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
spec
end.compact
keys = [:name, :type, :limit, :precision, :scale, :default, :null] & column_specs.map{ |spec| spec.keys }.inject([]){ |a,b| a | b }
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
format_string = lengths.map{ |len| "%-#{len}s" }.join("")
column_specs.each do |colspec|
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
tbl.print " t.column "
tbl.print((format_string % values).gsub(/,\s*$/, ''))
tbl.puts
end
@ -108,6 +125,17 @@ HEADER
stream
end
def default_string(value)
case value
when BigDecimal
value.to_s
when Date, DateTime, Time
"'" + value.to_s(:db) + "'"
else
value.inspect
end
end
def indexes(table, stream)
indexes = @connection.indexes(table)
indexes.each do |index|

View file

@ -1,26 +1,35 @@
module ActiveRecord
# Active Records will automatically record creation and/or update timestamps of database objects
# if fields of the names created_at/created_on or updated_at/updated_on are present. This module is
# automatically included, so you don't need to do that manually.
# Active Record automatically timestamps create and update if the table has fields
# created_at/created_on or updated_at/updated_on.
#
# This behavior can be turned off by setting <tt>ActiveRecord::Base.record_timestamps = false</tt>.
# This behavior by default uses local time, but can use UTC by setting <tt>ActiveRecord::Base.default_timezone = :utc</tt>
# Timestamping can be turned off by setting
# <tt>ActiveRecord::Base.record_timestamps = false</tt>
#
# Keep in mind that, via inheritance, you can turn off timestamps on a per
# model basis by setting <tt>record_timestamps</tt> to false in the desired
# models.
#
# class Feed < ActiveRecord::Base
# self.record_timestamps = false
# # ...
# end
#
# Timestamps are in the local timezone by default but can use UTC by setting
# <tt>ActiveRecord::Base.default_timezone = :utc</tt>
module Timestamp
def self.append_features(base) # :nodoc:
def self.included(base) #:nodoc:
super
base.class_eval do
alias_method :create_without_timestamps, :create
alias_method :create, :create_with_timestamps
base.alias_method_chain :create, :timestamps
base.alias_method_chain :update, :timestamps
alias_method :update_without_timestamps, :update
alias_method :update, :update_with_timestamps
end
base.cattr_accessor :record_timestamps, :instance_writer => false
base.record_timestamps = true
end
def create_with_timestamps #:nodoc:
if record_timestamps
t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
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?
@ -32,31 +41,11 @@ module ActiveRecord
def update_with_timestamps #:nodoc:
if record_timestamps
t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
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)
end
update_without_timestamps
end
end
class Base
# Records the creation date and possibly time in created_on (date only) or created_at (date and time) and the update date and possibly
# time in updated_on and updated_at. This only happens if the object responds to either of these messages, which they will do automatically
# if the table has columns of either of these names. This feature is turned on by default.
@@record_timestamps = true
cattr_accessor :record_timestamps
# deprecated: use ActiveRecord::Base.default_timezone instead.
@@timestamps_gmt = false
def self.timestamps_gmt=( gmt ) #:nodoc:
warn "timestamps_gmt= is deprecated. use default_timezone= instead"
self.default_timezone = ( gmt ? :utc : :local )
end
def self.timestamps_gmt #:nodoc:
warn "timestamps_gmt is deprecated. use default_timezone instead"
self.default_timezone == :utc
end
end
end

View file

@ -4,21 +4,16 @@ require 'thread'
module ActiveRecord
module Transactions # :nodoc:
TRANSACTION_MUTEX = Mutex.new
class TransactionError < ActiveRecordError # :nodoc:
end
def self.append_features(base)
super
def self.included(base)
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
[:destroy, :save, :save!].each do |method|
alias_method_chain method, :transactions
end
end
end
@ -60,7 +55,7 @@ module ActiveRecord
# 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
# == Object-level transactions (deprecated)
#
# You can enable object-level transactions for Active Record objects, though. You do this by naming each of the Active Records
# that you want to enable object-level transactions for, like this:
@ -70,8 +65,14 @@ module ActiveRecord
# 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.
# 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.
#
# However, useful state such as validation errors are also rolled back,
# limiting the usefulness of this feature. As such it is deprecated in
# Rails 1.2 and will be removed in the next release. Install the
# object_transactions plugin if you wish to continue using it.
#
# == Exception handling
#
@ -82,11 +83,14 @@ module ActiveRecord
module ClassMethods
def transaction(*objects, &block)
previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" }
lock_mutex
increment_open_transactions
begin
objects.each { |o| o.extend(Transaction::Simple) }
objects.each { |o| o.start_transaction }
unless objects.empty?
ActiveSupport::Deprecation.warn "Object transactions are deprecated and will be removed from Rails 2.0. See http://www.rubyonrails.org/deprecation for details.", caller
objects.each { |o| o.extend(Transaction::Simple) }
objects.each { |o| o.start_transaction }
end
result = connection.transaction(Thread.current['start_db_transaction'], &block)
@ -96,22 +100,21 @@ module ActiveRecord
objects.each { |o| o.abort_transaction }
raise
ensure
unlock_mutex
decrement_open_transactions
trap('TERM', previous_handler)
end
end
def lock_mutex#:nodoc:
Thread.current['open_transactions'] ||= 0
TRANSACTION_MUTEX.lock if Thread.current['open_transactions'] == 0
Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0)
Thread.current['open_transactions'] += 1
end
def unlock_mutex#:nodoc:
Thread.current['open_transactions'] -= 1
TRANSACTION_MUTEX.unlock if Thread.current['open_transactions'] == 0
end
private
def increment_open_transactions #:nodoc:
open = Thread.current['open_transactions'] ||= 0
Thread.current['start_db_transaction'] = open.zero?
Thread.current['open_transactions'] = open + 1
end
def decrement_open_transactions #:nodoc:
Thread.current['open_transactions'] -= 1
end
end
def transaction(*objects, &block)
@ -121,9 +124,13 @@ module ActiveRecord
def destroy_with_transactions #:nodoc:
transaction { destroy_without_transactions }
end
def save_with_transactions(perform_validation = true) #:nodoc:
transaction { save_without_transactions(perform_validation) }
end
def save_with_transactions! #:nodoc:
transaction { save_without_transactions! }
end
end
end

View file

@ -87,6 +87,7 @@ module ActiveRecord
end
alias :add_on_boundry_breaking :add_on_boundary_breaking
deprecate :add_on_boundary_breaking => :validates_length_of, :add_on_boundry_breaking => :validates_length_of
# Returns true if the specified +attribute+ has errors associated with it.
def invalid?(attribute)
@ -97,13 +98,9 @@ module ActiveRecord
# * 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.to_s].nil?
nil
elsif @errors[attribute.to_s].length == 1
@errors[attribute.to_s].first
else
@errors[attribute.to_s]
end
errors = @errors[attribute.to_s]
return nil if errors.nil?
errors.size == 1 ? errors.first : errors
end
alias :[] :on
@ -139,13 +136,12 @@ module ActiveRecord
end
end
end
return full_messages
full_messages
end
# Returns true if no errors have been added.
def empty?
return @errors.empty?
@errors.empty?
end
# Removes all the errors that have been added.
@ -156,13 +152,23 @@ module ActiveRecord
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
# with this as well.
def size
error_count = 0
@errors.each_value { |attribute| error_count += attribute.length }
error_count
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
end
alias_method :count, :size
alias_method :length, :size
# Return an XML representation of this error object.
def to_xml(options={})
options[:root] ||= "errors"
options[:indent] ||= 2
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
options[:builder].instruct! unless options.delete(:skip_instruct)
options[:builder].errors do |e|
full_messages.each { |msg| e.error(msg) }
end
end
end
@ -209,18 +215,12 @@ module ActiveRecord
module Validations
VALIDATIONS = %w( validate validate_on_create validate_on_update )
def self.append_features(base) # :nodoc:
super
def self.included(base) # :nodoc:
base.extend ClassMethods
base.class_eval do
alias_method :save_without_validation, :save
alias_method :save, :save_with_validation
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
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
alias_method_chain :update_attribute, :validation_skipping
end
end
@ -290,7 +290,7 @@ module ActiveRecord
# method, proc or string should return or evaluate to a true or false value.
def validates_each(*attrs)
options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
attrs = attrs.flatten
attrs = attrs.flatten
# Declare the validation.
send(validation_method(options[:on] || :save)) do |record|
@ -375,6 +375,10 @@ module ActiveRecord
#
# The first_name attribute must be in the object and it cannot be blank.
#
# If you want to validate the presence of a boolean field (where the real values are true and false),
# you will want to use validates_inclusion_of :field_name, :in => [true, false]
# This is due to the way Object#blank? handles boolean values. false.blank? # => true
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "can't be blank")
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
@ -515,17 +519,24 @@ module ActiveRecord
# Configuration options:
# * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
# * <tt>scope</tt> - One or more columns by which to limit the scope of the uniquness constraint.
# * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default).
# * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_uniqueness_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
validates_each(attr_names,configuration) do |record, attr_name, value|
condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params = [value]
if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
condition_params = [value]
else
condition_sql = "LOWER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
condition_params = [value.downcase]
end
if scope = configuration[:scope]
Array(scope).map do |scope_item|
scope_value = record.send(scope_item)
@ -543,13 +554,17 @@ module ActiveRecord
end
end
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
# provided.
#
# class Person < ActiveRecord::Base
# validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :on => :create
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
# end
#
# Note: use \A and \Z to match the start and end of the string, ^ and $ match the start/end of a line.
#
# A regular expression must be provided or else an exception will be raised.
#
# Configuration options:
@ -663,7 +678,7 @@ module ActiveRecord
# Validates whether the value of the specified attribute is numeric by trying to convert it to
# a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression
# <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true).
# <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>integer</tt> is set to true).
#
# class Person < ActiveRecord::Base
# validates_numericality_of :value, :on => :create
@ -684,7 +699,7 @@ module ActiveRecord
if configuration[:only_integer]
validates_each(attr_names,configuration) do |record, attr_name,value|
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /^[+-]?\d+$/
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /\A[+-]?\d+\Z/
end
else
validates_each(attr_names,configuration) do |record, attr_name,value|
@ -705,6 +720,7 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr) }
else
attributes ||= {}
attributes.reverse_merge!(scope(:create)) if scoped?(:create)
object = new(attributes)

View file

@ -6,7 +6,7 @@
class Mysql
VERSION = "4.0-ruby-0.2.5"
VERSION = "4.0-ruby-0.2.6-plus-changes"
require "socket"
require "digest/sha1"
@ -18,6 +18,9 @@ class Mysql
MYSQL_PORT = 3306
PROTOCOL_VERSION = 10
SCRAMBLE_LENGTH = 20
SCRAMBLE_LENGTH_323 = 8
# Command
COM_SLEEP = 0
COM_QUIT = 1
@ -147,12 +150,23 @@ class Mysql
@db = db.dup
end
write data
read
pkt = read
handle_auth_fallback(pkt, passwd)
ObjectSpace.define_finalizer(self, Mysql.finalizer(@net))
self
end
alias :connect :real_connect
def handle_auth_fallback(pkt, passwd)
# A packet like this means that we need to send an old-format password
if pkt.size == 1 and pkt[0] == 254 and
@server_capabilities & CLIENT_SECURE_CONNECTION != 0 then
data = scramble(passwd, @scramble_buff, @protocol_version == 9)
write data + "\0"
read
end
end
def escape_string(str)
Mysql::escape_string str
end
@ -208,7 +222,8 @@ class Mysql
else
data = user+"\0"+scramble41(passwd, @scramble_buff)+db
end
command COM_CHANGE_USER, data
pkt = command COM_CHANGE_USER, data
handle_auth_fallback(pkt, passwd)
@user = user
@passwd = passwd
@db = db
@ -534,10 +549,10 @@ class Mysql
return "" if password == nil or password == ""
raise "old version password is not implemented" if old_ver
hash_pass = hash_password password
hash_message = hash_password message
hash_message = hash_password message.slice(0,SCRAMBLE_LENGTH_323)
rnd = Random::new hash_pass[0] ^ hash_message[0], hash_pass[1] ^ hash_message[1]
to = []
1.upto(message.length) do
1.upto(SCRAMBLE_LENGTH_323) do
to << ((rnd.rnd*31)+64).floor
end
extra = (rnd.rnd*31).floor

View file

@ -1,8 +1,8 @@
module ActiveRecord
module VERSION #:nodoc:
MAJOR = 1
MINOR = 14
TINY = 4
MINOR = 15
TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end

View file

@ -9,8 +9,7 @@ module ActiveRecord
end
end
def self.append_features(base)
super
def self.included(base)
base.extend(ClassMethods)
end