TeX and CSS tweaks.
Sync with latest Instiki Trunk (Updates Rails to 1.2.2)
This commit is contained in:
parent
0ac586ee25
commit
c358389f25
443 changed files with 24218 additions and 9823 deletions
16
vendor/rails/activerecord/lib/active_record.rb
vendored
16
vendor/rails/activerecord/lib/active_record.rb
vendored
|
@ -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|
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
520
vendor/rails/activerecord/lib/active_record/base.rb
vendored
520
vendor/rails/activerecord/lib/active_record/base.rb
vendored
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,8 +9,7 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.append_features(base)
|
||||
super
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue