Upgrade to Rails 2.0.2

Upgraded to Rails 2.0.2, except that we maintain

   vendor/rails/actionpack/lib/action_controller/routing.rb

from Rail 1.2.6 (at least for now), so that Routes don't change. We still
get to enjoy Rails's many new features.

Also fixed a bug in Chunk-handling: disable WikiWord processing in tags (for real this time).
This commit is contained in:
Jacques Distler 2007-12-21 01:48:59 -06:00
parent 0f6889e09f
commit 6873fc8026
1083 changed files with 52810 additions and 41058 deletions

View file

@ -1,5 +1,5 @@
#--
# Copyright (c) 2004-2006 David Heinemeier Hansson
# Copyright (c) 2004-2007 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -24,18 +24,21 @@
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined?(ActiveSupport)
begin
$:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
require 'active_support'
rescue LoadError
unless defined? ActiveSupport
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
if File.exist?(active_support_path)
$:.unshift active_support_path
require 'active_support'
else
require 'rubygems'
gem 'activesupport'
require 'active_support'
end
end
require 'active_record/base'
require 'active_record/observer'
require 'active_record/query_cache'
require 'active_record/validations'
require 'active_record/callbacks'
require 'active_record/reflection'
@ -43,18 +46,16 @@ require 'active_record/associations'
require 'active_record/aggregations'
require 'active_record/transactions'
require 'active_record/timestamp'
require 'active_record/acts/list'
require 'active_record/acts/tree'
require 'active_record/acts/nested_set'
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/serialization'
require 'active_record/attribute_methods'
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
include ActiveRecord::Validations
include ActiveRecord::Locking::Optimistic
include ActiveRecord::Locking::Pessimistic
@ -65,21 +66,11 @@ ActiveRecord::Base.class_eval do
include ActiveRecord::Aggregations
include ActiveRecord::Transactions
include ActiveRecord::Reflection
include ActiveRecord::Acts::Tree
include ActiveRecord::Acts::List
include ActiveRecord::Acts::NestedSet
include ActiveRecord::Calculations
include ActiveRecord::XmlSerialization
include ActiveRecord::Serialization
include ActiveRecord::AttributeMethods
end
unless defined?(RAILS_CONNECTION_ADAPTERS)
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase )
end
require 'active_record/connection_adapters/abstract_adapter'
RAILS_CONNECTION_ADAPTERS.each do |adapter|
require "active_record/connection_adapters/" + adapter + "_adapter"
end
require 'active_record/query_cache'
require 'active_record/schema_dumper'

View file

@ -1,256 +0,0 @@
module ActiveRecord
module Acts #:nodoc:
module List #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# This act provides the capabilities for sorting and reordering a number of objects in a list.
# The class that has this specified needs to have a "position" column defined as an integer on
# the mapped database table.
#
# Todo list example:
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, :order => "position"
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list :scope => :todo_list
# end
#
# todo_list.first.move_to_bottom
# todo_list.last.move_higher
module ClassMethods
# Configuration options are:
#
# * +column+ - specifies the column name to use for keeping the position integer (default: position)
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
# (if that hasn't been already) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
def acts_as_list(options = {})
configuration = { :column => "position", :scope => "1 = 1" }
configuration.update(options) if options.is_a?(Hash)
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
if configuration[:scope].is_a?(Symbol)
scope_condition_method = %(
def scope_condition
if #{configuration[:scope].to_s}.nil?
"#{configuration[:scope].to_s} IS NULL"
else
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
end
end
)
else
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
end
class_eval <<-EOV
include ActiveRecord::Acts::List::InstanceMethods
def acts_as_list_class
::#{self.name}
end
def position_column
'#{configuration[:column]}'
end
#{scope_condition_method}
before_destroy :remove_from_list
before_create :add_to_list_bottom
EOV
end
end
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return true if that chapter is
# the first in the list of all chapters.
module InstanceMethods
# Insert the item at the given position (defaults to the top position of 1).
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
acts_as_list_class.transaction do
lower_item.decrement_position
increment_position
end
end
# Swap positions with the next higher item, if one exists.
def move_higher
return unless higher_item
acts_as_list_class.transaction do
higher_item.increment_position
decrement_position
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
decrement_positions_on_lower_items
assume_bottom_position
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
increment_positions_on_higher_items
assume_top_position
end
end
# Removes the item from the list.
def remove_from_list
if in_list?
decrement_positions_on_lower_items
update_attribute position_column, nil
end
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 =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
)
end
# Return the next lower item in the list.
def lower_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
)
end
# Test if this record is in a list
def in_list?
!send(position_column).nil?
end
private
def add_to_list_top
increment_positions_on_all_items
end
def add_to_list_bottom
self[position_column] = bottom_position_in_list.to_i + 1
end
# Overwrite this method to define the scope of the list changes
def scope_condition() "1" end
# Returns the bottom position number in the list.
# bottom_position_in_list # => 2
def bottom_position_in_list(except = nil)
item = bottom_item(except)
item ? item.send(position_column) : 0
end
# Returns the bottom item
def bottom_item(except = nil)
conditions = scope_condition
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
end
# Forces item to assume the bottom position in the list.
def assume_bottom_position
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
end
# Forces item to assume the top position in the list.
def assume_top_position
update_attribute(position_column, 1)
end
# This has the effect of moving all the higher items up one.
def decrement_positions_on_higher_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
)
end
# This has the effect of moving all the lower items up one.
def decrement_positions_on_lower_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
)
end
# This has the effect of moving all the higher items down one.
def increment_positions_on_higher_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
)
end
# This has the effect of moving all the lower items down one.
def increment_positions_on_lower_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
)
end
# Increments position (<tt>position_column</tt>) of all items in the list.
def increment_positions_on_all_items
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
)
end
def insert_at_position(position)
remove_from_list
increment_positions_on_lower_items(position)
self.update_attribute(position_column, position)
end
end
end
end
end

View file

@ -1,211 +0,0 @@
module ActiveRecord
module Acts #:nodoc:
module NestedSet #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# This acts provides Nested Set functionality. Nested Set is similiar to Tree, but with
# the added feature that you can select the children and all of their descendents with
# a single query. A good use case for this is a threaded post system, where you want
# to display every reply to a comment without multiple selects.
#
# A google search for "Nested Set" should point you in the direction to explain the
# database theory. I figured out a bunch of this from
# http://threebit.net/tutorials/nestedset/tutorial1.html
#
# Instead of picturing a leaf node structure with children pointing back to their parent,
# the best way to imagine how this works is to think of the parent entity surrounding all
# of its children, and its parent surrounding it, etc. Assuming that they are lined up
# horizontally, we store the left and right boundries in the database.
#
# Imagine:
# root
# |_ Child 1
# |_ Child 1.1
# |_ Child 1.2
# |_ Child 2
# |_ Child 2.1
# |_ Child 2.2
#
# If my cirlces in circles description didn't make sense, check out this sweet
# ASCII art:
#
# ___________________________________________________________________
# | Root |
# | ____________________________ ____________________________ |
# | | Child 1 | | Child 2 | |
# | | __________ _________ | | __________ _________ | |
# | | | C 1.1 | | C 1.2 | | | | C 2.1 | | C 2.2 | | |
# 1 2 3_________4 5________6 7 8 9_________10 11_______12 13 14
# | |___________________________| |___________________________| |
# |___________________________________________________________________|
#
# The numbers represent the left and right boundries. The table then might
# look like this:
# ID | PARENT | LEFT | RIGHT | DATA
# 1 | 0 | 1 | 14 | root
# 2 | 1 | 2 | 7 | Child 1
# 3 | 2 | 3 | 4 | Child 1.1
# 4 | 2 | 5 | 6 | Child 1.2
# 5 | 1 | 8 | 13 | Child 2
# 6 | 5 | 9 | 10 | Child 2.1
# 7 | 5 | 11 | 12 | Child 2.2
#
# So, to get all children of an entry, you
# SELECT * WHERE CHILD.LEFT IS BETWEEN PARENT.LEFT AND PARENT.RIGHT
#
# To get the count, it's (LEFT - RIGHT + 1)/2, etc.
#
# To get the direct parent, it falls back to using the PARENT_ID field.
#
# There are instance methods for all of these.
#
# The structure is good if you need to group things together; the downside is that
# keeping data integrity is a pain, and both adding and removing an entry
# require a full table write.
#
# This sets up a before_destroy trigger to prune the tree correctly if one of its
# elements gets deleted.
#
module ClassMethods
# Configuration options are:
#
# * +parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
# * +left_column+ - column name for left boundry data, default "lft"
# * +right_column+ - column name for right boundry data, default "rgt"
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
# (if that hasn't been already) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
def acts_as_nested_set(options = {})
configuration = { :parent_column => "parent_id", :left_column => "lft", :right_column => "rgt", :scope => "1 = 1" }
configuration.update(options) if options.is_a?(Hash)
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
if configuration[:scope].is_a?(Symbol)
scope_condition_method = %(
def scope_condition
if #{configuration[:scope].to_s}.nil?
"#{configuration[:scope].to_s} IS NULL"
else
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
end
end
)
else
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
end
class_eval <<-EOV
include ActiveRecord::Acts::NestedSet::InstanceMethods
#{scope_condition_method}
def left_col_name() "#{configuration[:left_column]}" end
def right_col_name() "#{configuration[:right_column]}" end
def parent_column() "#{configuration[:parent_column]}" end
EOV
end
end
module InstanceMethods
# Returns true is this is a root node.
def root?
parent_id = self[parent_column]
(parent_id == 0 || parent_id.nil?) && (self[left_col_name] == 1) && (self[right_col_name] > self[left_col_name])
end
# Returns true is this is a child node
def child?
parent_id = self[parent_column]
!(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name])
end
# Returns true if we have no idea what this is
def unknown?
!root? && !child?
end
# Adds a child to this object in the tree. If this object hasn't been initialized,
# it gets set up as a root node. Otherwise, this method will update all of the
# other elements in the tree and shift them to the right, keeping everything
# balanced.
def add_child( child )
self.reload
child.reload
if child.root?
raise "Adding sub-tree isn\'t currently supported"
else
if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) )
# Looks like we're now the root node! Woo
self[left_col_name] = 1
self[right_col_name] = 4
# What do to do about validation?
return nil unless self.save
child[parent_column] = self.id
child[left_col_name] = 2
child[right_col_name]= 3
return child.save
else
# OK, we need to add and shift everything else to the right
child[parent_column] = self.id
right_bound = self[right_col_name]
child[left_col_name] = right_bound
child[right_col_name] = right_bound + 1
self[right_col_name] += 2
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
}
end
end
end
# Returns the number of nested children of this object.
def children_count
return (self[right_col_name] - self[left_col_name] - 1)/2
end
# Returns a set of itself and all of its nested children
def full_set
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.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.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
# back to the left so the counts still work.
def before_destroy
return if self[right_col_name].nil? || self[left_col_name].nil?
dif = self[right_col_name] - self[left_col_name] + 1
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
end
end
end

View file

@ -1,96 +0,0 @@
module ActiveRecord
module Acts #:nodoc:
module Tree #:nodoc:
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
# 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:
# root
# \_ child1
# \_ subchild1
# \_ subchild2
#
# root = Category.create("name" => "root")
# child1 = root.children.create("name" => "child1")
# subchild1 = child1.children.create("name" => "subchild1")
#
# root.parent # => nil
# child1.parent # => root
# 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
# 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)
# * ancestors : Returns all the ancestors of the current node ([child1, root] when called from subchild2)
# * root : Returns the root of the current node (root when called from subchild2)
module ClassMethods
# Configuration options are:
#
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: parent_id)
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
# * <tt>counter_cache</tt> - keeps a count in a children_count column if set to true (default: false).
def acts_as_tree(options = {})
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
configuration.update(options) if options.is_a?(Hash)
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
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
def self.root
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
end
EOV
end
end
module InstanceMethods
# Returns list of ancestors, starting from parent until root.
#
# subchild1.ancestors # => [child1, root]
def ancestors
node, nodes = self, []
nodes << node = node.parent while node.parent
nodes
end
# Returns the root node of the tree.
def root
node = self
node = node.parent while node.parent
node
end
# Returns all siblings of the current node.
#
# subchild1.siblings # => [subchild2]
def siblings
self_and_siblings - [self]
end
# Returns all siblings and a reference to the current node.
#
# subchild1.self_and_siblings # => [subchild1, subchild2]
def self_and_siblings
parent ? parent.children : self.class.roots
end
end
end
end
end

View file

@ -70,7 +70,7 @@ module ActiveRecord
# end
#
# Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
# composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
# composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
# +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
#
# customer.balance = Money.new(20) # sets the Money value object and the attribute
@ -92,19 +92,19 @@ module ActiveRecord
#
# == Writing value objects
#
# Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
# $5. Two Money objects both representing $5 should be equal (through methods such as == and <=> from Comparable if ranking
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as Customer can
# Value objects are immutable and interchangeable objects that represent a given value, such as a +Money+ object representing
# $5. Two +Money+ objects both representing $5 should be equal (through methods such as == and <=> from +Comparable+ if ranking
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as +Customer+ can
# easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
# relational unique identifiers (such as primary keys). Normal <tt>ActiveRecord::Base</tt> classes are entity objects.
#
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
# creation. Create a new money object with the new value instead. This is exemplified by the Money#exchanged_to method that
# It's also important to treat the value objects as immutable. Don't allow the +Money+ object to have its amount changed after
# creation. Create a new +Money+ object with the new value instead. This is exemplified by the <tt>Money#exchanged_to</tt> method that
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
# changed through other means than the writer method.
# changed through means other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
# change it afterwards will result in a TypeError.
# change it afterwards will result in a <tt>TypeError</tt>.
#
# 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
@ -119,71 +119,60 @@ module ActiveRecord
# * <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.
# attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
# This defaults to +false+.
#
# An optional block can be passed to convert the argument that is passed to the writer method into an instance of
# <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
#
# Option examples:
# composed_of :temperature, :mapping => %w(reading celsius)
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
# composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
# 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 = {})
def composed_of(part_id, options = {}, &block)
options.assert_valid_keys(:class_name, :mapping, :allow_nil)
name = part_id.id2name
class_name = options[:class_name] || name.camelize
mapping = options[:mapping] || [ name, name ]
mapping = [ mapping ] unless mapping.first.is_a?(Array)
allow_nil = options[:allow_nil] || false
reader_method(name, class_name, mapping, allow_nil)
writer_method(name, class_name, mapping, allow_nil)
writer_method(name, class_name, mapping, allow_nil, block)
create_reflection(:composed_of, part_id, options, self)
end
private
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"
module_eval do
define_method(name) do |*args|
force_reload = args.first || false
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
end
return instance_variable_get("@#{name}")
end
end
module_eval <<-end_eval
def #{name}(force_reload = false)
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, allow_nil)
mapping = (Array === mapping.first ? mapping : [ mapping ])
end
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
def writer_method(name, class_name, mapping, allow_nil, conversion)
module_eval do
define_method("#{name}=") do |part|
if part.nil? && allow_nil
mapping.each { |pair| @attributes[pair.first] = nil }
instance_variable_set("@#{name}", nil)
else
part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
mapping.each { |pair| @attributes[pair.first] = part.send(pair.last) }
instance_variable_set("@#{name}", part.freeze)
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

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ module ActiveRecord
load_target
@target.to_ary
end
def reset
reset_target!
@loaded = false
@ -17,7 +17,7 @@ module ActiveRecord
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
def <<(*records)
result = true
load_target
load_target if @owner.new_record?
@owner.transaction do
flatten_deeper(records).each do |record|
@ -34,7 +34,7 @@ module ActiveRecord
alias_method :push, :<<
alias_method :concat, :<<
# Remove all records from this association
def delete_all
load_target
@ -66,9 +66,9 @@ module ActiveRecord
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
return self if length.zero? # forces load_target if hasn't happened already
return self if length.zero? # forces load_target if it hasn't happened already
if @reflection.options[:dependent] && @reflection.options[:dependent] == :delete_all
if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
destroy_all
else
delete_all
@ -84,27 +84,24 @@ module ActiveRecord
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)
attributes.collect { |attr| create(attr) }
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
else
record = build(attributes)
if @owner.new_record?
ActiveSupport::Deprecation.warn("Calling .create on a has_many association without saving its owner will not work in rails 2.0, you probably want .build instead")
else
record.save
end
record
create_record(attrs) { |record| record.save }
end
end
def create!(attrs = {})
create_record(attrs) { |record| record.save! }
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# 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? && !@reflection.options[:uniq]
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
@target.size
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
unsaved_records = Array(@target.detect { |r| r.new_record? })
@ -124,6 +121,14 @@ module ActiveRecord
size.zero?
end
def any?(&block)
if block_given?
method_missing(:any?, &block)
else
!empty?
end
end
def uniq(collection = self)
seen = Set.new
collection.inject([]) do |kept, record|
@ -150,7 +155,21 @@ module ActiveRecord
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
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
end
end
# overloaded in derived Association classes to provide useful scoping depending on association type.
def construct_scope
{}
end
def reset_target!
@target = Array.new
end
@ -167,6 +186,27 @@ module ActiveRecord
end
private
def create_record(attrs, &block)
ensure_owner_is_not_new
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
add_record_to_target_with_callbacks(record, &block)
end
def build_record(attrs, &block)
record = @reflection.klass.new(attrs)
add_record_to_target_with_callbacks(record, &block)
end
def add_record_to_target_with_callbacks(record)
callback(:before_add, record)
yield(record) if block_given?
@target ||= [] unless loaded?
@target << record
callback(:after_add, record)
record
end
def callback(method, record)
callbacks_for(method).each do |callback|
case callback
@ -187,8 +227,14 @@ module ActiveRecord
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
end
end
def ensure_owner_is_not_new
if @owner.new_record?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end
end
end
end
end
end

View file

@ -12,15 +12,15 @@ module ActiveRecord
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
@ -28,55 +28,61 @@ module ActiveRecord
def respond_to?(symbol, include_priv = false)
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
end
# Explicitly proxy === because the instance method removal above
# doesn't catch it.
def ===(other)
load_target
other === @target
end
def aliased_table_name
@reflection.klass.table_name
end
def conditions
@conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
end
alias :sql_conditions :conditions
def reset
@target = nil
@loaded = false
@target = nil
end
def reload
reset
load_target
self unless @target.nil?
end
def loaded?
@loaded
end
def loaded
@loaded = true
end
def target
@target
end
def target=(target)
@target = target
loaded
end
def inspect
reload unless loaded?
@target.inspect
end
protected
def dependent?
@reflection.options[:dependent] || false
@reflection.options[:dependent]
end
def quoted_record_ids(records)
records.map { |record| record.quoted_id }.join(',')
end
@ -93,10 +99,6 @@ module ActiveRecord
@reflection.klass.send(:sanitize_sql, sql)
end
def extract_options_from_args!(args)
@owner.send(:extract_options_from_args!, args)
end
def set_belongs_to_association_for(record)
if @reflection.options[:as]
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
@ -116,10 +118,10 @@ module ActiveRecord
:select => @reflection.options[:select]
)
end
private
def method_missing(method, *args, &block)
if load_target
if load_target
@target.send(method, *args, &block)
end
end
@ -138,14 +140,14 @@ module ActiveRecord
end
# Can be overwritten by associations that might have the foreign key available for an association without
# having the object itself (and still being a new record). Currently, only belongs_to present this scenario.
# having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
def foreign_key_present
false
end
def raise_on_type_mismatch(record)
unless record.is_a?(@reflection.klass)
raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.klass} expected, got #{record.class}"
end
end

View file

@ -5,31 +5,26 @@ module ActiveRecord
super
construct_sql
end
def build(attributes = {})
load_target
record = @reflection.klass.new(attributes)
@target << record
record
build_record(attributes)
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
create_record(attributes) { |record| insert_record(record) }
end
def create!(attributes = {})
create_record(attributes) { |record| insert_record(record, true) }
end
def find_first
load_target.first
end
def find(*args)
options = Base.send(:extract_options_from_args!, args)
options = args.extract_options!
# If using a custom finder_sql, scan the entire collection.
if @reflection.options[:finder_sql]
@ -52,7 +47,7 @@ module ActiveRecord
options[:conditions] = conditions
options[:joins] = @join_sql
options[:readonly] = finding_with_ambigious_select?(options[:select])
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
if options[:order] && @reflection.options[:order]
options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
@ -62,47 +57,26 @@ module ActiveRecord
merge_options_from_reflection!(options)
options[:select] ||= (@reflection.options[:select] || '*')
# Pass through args exactly as we received them.
args << options
@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 }
callback(:before_add, record)
insert_record(record) unless @owner.new_record?
@target << record
callback(:after_add, record)
self
end
deprecate :push_with_attributes => "consider using has_many :through instead"
alias :concat_with_attributes :push_with_attributes
protected
def method_missing(method, *args, &block)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
else
@reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
@reflection.klass.send(method, *args, &block)
end
end
end
def count_records
load_target.size
end
def insert_record(record)
def insert_record(record, force=true)
if record.new_record?
return false unless record.save
if force
record.save!
else
return false unless record.save
end
end
if @reflection.options[:insert_sql]
@ -131,10 +105,10 @@ module ActiveRecord
@owner.connection.execute(sql)
end
return true
end
def delete_records(records)
if sql = @reflection.options[:delete_sql]
records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
@ -144,7 +118,7 @@ module ActiveRecord
@owner.connection.execute(sql)
end
end
def construct_sql
interpolate_sql_options!(@reflection.options, :finder_sql)
@ -158,12 +132,33 @@ module ActiveRecord
@join_sql = "INNER JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
end
# Join tables with additional columns on top of the two foreign keys must be considered ambigious unless a select
# clause has been explicitly defined. Otherwise you can get broken records back, if, say, the join column also has
# and id column, which will then overwrite the id column of the records coming back.
def finding_with_ambigious_select?(select_clause)
def construct_scope
{ :find => { :conditions => @finder_sql,
:joins => @join_sql,
:readonly => false,
:order => @reflection.options[:order],
:limit => @reflection.options[:limit] } }
end
# Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
# clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
# an id column. This will then overwrite the id column of the records coming back.
def finding_with_ambiguous_select?(select_clause)
!select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
end
private
def create_record(attributes)
# Can't use Base.create because the foreign key may be a protected attribute.
ensure_owner_is_not_new
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr) }
else
record = build(attributes)
yield(record)
record
end
end
end
end
end

View file

@ -10,35 +10,10 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr) }
else
record = @reflection.klass.new(attributes)
set_belongs_to_association_for(record)
@target ||= [] unless loaded?
@target << record
record
build_record(attributes) { |record| set_belongs_to_association_for(record) }
end
end
# DEPRECATED.
def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
if @reflection.options[:finder_sql]
@reflection.klass.find_by_sql(@finder_sql)
else
conditions = @finder_sql
conditions += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions
orderings ||= @reflection.options[:order]
@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(*args)
if @reflection.options[:counter_sql]
@ -46,7 +21,7 @@ module ActiveRecord
elsif @reflection.options[:finder_sql]
@reflection.klass.count_by_sql(@finder_sql)
else
column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
options[:conditions] = options[:conditions].nil? ?
@finder_sql :
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
@ -57,12 +32,12 @@ module ActiveRecord
end
def find(*args)
options = Base.send(:extract_options_from_args!, args)
options = args.extract_options!
# If using a custom finder_sql, scan the entire collection.
if @reflection.options[:finder_sql]
expects_array = args.first.kind_of?(Array)
ids = args.flatten.compact.uniq
ids = args.flatten.compact.uniq.map(&:to_i)
if ids.size == 1
id = ids.first
@ -93,26 +68,6 @@ module ActiveRecord
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
}
) do
@reflection.klass.send(method, *args, &block)
end
end
end
def load_target
if !@owner.new_record? || foreign_key_present
begin
@ -164,14 +119,17 @@ module ActiveRecord
end
def delete_records(records)
if @reflection.options[:dependent]
records.each { |r| r.destroy }
else
ids = quoted_record_ids(records)
@reflection.klass.update_all(
"#{@reflection.primary_key_name} = NULL",
"#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
)
case @reflection.options[:dependent]
when :destroy
records.each(&:destroy)
when :delete_all
@reflection.klass.delete(records.map(&:id))
else
ids = quoted_record_ids(records)
@reflection.klass.update_all(
"#{@reflection.primary_key_name} = NULL",
"#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
)
end
end
@ -205,6 +163,12 @@ module ActiveRecord
@counter_sql = @finder_sql
end
end
def construct_scope
create_scoping = {}
set_belongs_to_association_for(create_scoping)
{ :find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit] }, :create => create_scoping }
end
end
end
end

View file

@ -9,7 +9,7 @@ module ActiveRecord
end
def find(*args)
options = Base.send(:extract_options_from_args!, args)
options = args.extract_options!
conditions = "#{@finder_sql}"
if sanitized_conditions = sanitize_sql(options[:conditions])
@ -51,16 +51,14 @@ module ActiveRecord
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
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(associate)) { klass.create! }
@target << associate if loaded?
end
end
@ -69,43 +67,60 @@ module ActiveRecord
[:push, :concat].each { |method| alias_method method, :<< }
# Remove +records+ from this association. Does not destroy +records+.
# Removes +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)
raise ActiveRecord::HasManyThroughCantDissociateNewRecords.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::HasManyThroughCantDissociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
@owner.send(through.name).proxy_target.delete(klass.delete_all(construct_join_attributes(associate)))
@target.delete(associate)
end
end
self
end
def build(attrs = nil)
raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection)
end
alias_method :new, :build
def create!(attrs = nil)
@reflection.klass.transaction do
self << @reflection.klass.with_scope(:create => attrs) { @reflection.klass.create! }
self << (object = @reflection.klass.send(:with_scope, :create => attrs) { @reflection.klass.create! })
object
end
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# 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
return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
return @target.size if loaded?
return count
end
# Calculate sum using SQL, not Enumerable
def sum(*args, &block)
calculate(:sum, *args, &block)
end
def count(*args)
column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
if @reflection.options[:uniq]
# This is needed becase 'SELECT count(DISTINCT *)..' is not valid sql statement.
# This is needed because 'SELECT count(DISTINCT *)..' is not valid sql statement.
column_name = "#{@reflection.klass.table_name}.#{@reflection.klass.primary_key}" if column_name == :all
options.merge!(:distinct => true)
end
@ -117,7 +132,7 @@ module ActiveRecord
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
super
else
@reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) }
@reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
end
end
@ -133,7 +148,8 @@ module ActiveRecord
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include]
)
@reflection.options[:uniq] ? records.to_set.to_a : records
records.uniq! if @reflection.options[:uniq]
records
end
# Construct attributes for associate pointing to owner.
@ -148,11 +164,11 @@ module ActiveRecord
# Construct attributes for :through pointing to owner and associate.
def construct_join_attributes(associate)
returning construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) do |join_attributes|
if @reflection.options[:source_type]
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
end
join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
if @reflection.options[:source_type]
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
end
join_attributes
end
# Associate attributes pointing to owner, quoted.
@ -220,7 +236,9 @@ module ActiveRecord
:find => { :from => construct_from,
:conditions => construct_conditions,
:joins => construct_joins,
:select => construct_select } }
:select => construct_select,
:order => @reflection.options[:order],
:limit => @reflection.options[:limit] } }
end
def construct_sql
@ -247,11 +265,20 @@ module ActiveRecord
@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]),
(interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.source_reflection.options[:conditions])) if @reflection.source_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?)
].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && !@reflection.source_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
end
alias_method :sql_conditions, :conditions
def has_cached_counter?
@owner.attribute_present?(cached_counter_attribute_name)
end
def cached_counter_attribute_name
"#{@reflection.name}_count"
end
end
end
end

View file

@ -6,23 +6,16 @@ module ActiveRecord
construct_sql
end
def create(attributes = {}, replace_existing = true)
record = build(attributes, replace_existing)
record.save
record
def create(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.create(attrs) }
end
def build(attributes = {}, replace_existing = true)
record = @reflection.klass.new(attributes)
def create!(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.create!(attrs) }
end
if replace_existing
replace(record, true)
else
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
end
record
def build(attrs = {}, replace_existing = true)
new_record(replace_existing) { |klass| klass.new(attrs) }
end
def replace(obj, dont_save = false)
@ -75,6 +68,29 @@ module ActiveRecord
end
@finder_sql << " AND (#{conditions})" if conditions
end
def construct_scope
create_scoping = {}
set_belongs_to_association_for(create_scoping)
{ :create => create_scoping }
end
def new_record(replace_existing)
# Make sure we load the target first, if we plan on replacing the existing
# instance. Otherwise, if the target has not previously been loaded
# elsewhere, the instance we create will get orphaned.
load_target if replace_existing
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { yield @reflection.klass }
if replace_existing
replace(record, true)
else
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
end
record
end
end
end
end

View file

@ -1,10 +1,13 @@
module ActiveRecord
module AttributeMethods #:nodoc:
DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
def self.included(base)
base.extend ClassMethods
base.attribute_method_suffix *DEFAULT_SUFFIXES
base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
end
# Declare and check for suffixed attribute methods.
@ -43,6 +46,68 @@ module ActiveRecord
@@attribute_method_regexp.match(method_name)
end
# Contains the names of the generated attribute methods.
def generated_methods #:nodoc:
@generated_methods ||= Set.new
end
def generated_methods?
!generated_methods.empty?
end
# generates all the attribute related methods for columns in the database
# accessors, mutators and query methods
def define_attribute_methods
return if generated_methods?
columns_hash.each do |name, column|
unless instance_method_already_implemented?(name)
if self.serialized_attributes[name]
define_read_method_for_serialized_attribute(name)
else
define_read_method(name.to_sym, name, column)
end
end
unless instance_method_already_implemented?("#{name}=")
define_write_method(name.to_sym)
end
unless instance_method_already_implemented?("#{name}?")
define_question_method(name)
end
end
end
# Check to see if the method is defined in the model or any of its subclasses that also derive from ActiveRecord.
# Raise DangerousAttributeError if the method is defined by ActiveRecord though.
def instance_method_already_implemented?(method_name)
return true if method_name =~ /^id(=$|\?$|$)/
@_defined_class_methods ||= Set.new(ancestors.first(ancestors.index(ActiveRecord::Base)).collect! { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.flatten)
@@_defined_activerecord_methods ||= Set.new(ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false))
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
@_defined_class_methods.include?(method_name)
end
alias :define_read_methods :define_attribute_methods
# +cache_attributes+ allows you to declare which converted attribute values should
# be cached. Usually caching only pays off for attributes with expensive conversion
# methods, like date columns (e.g. created_at, updated_at).
def cache_attributes(*attribute_names)
attribute_names.each {|attr| cached_attributes << attr.to_s}
end
# returns the attributes where
def cached_attributes
@cached_attributes ||=
columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map(&:name).to_set
end
def cache_attribute?(attr_name)
cached_attributes.include?(attr_name)
end
private
# Suffixes a, ?, c become regexp /(a|\?|c)$/
def rebuild_attribute_method_regexp
@ -54,9 +119,197 @@ module ActiveRecord
def attribute_method_suffixes
@@attribute_method_suffixes ||= []
end
# Define an attribute reader method. Cope with nil column.
def define_read_method(symbol, attr_name, column)
cast_code = column.type_cast_code('v') if column
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
unless attr_name.to_s == self.primary_key.to_s
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
end
if cache_attribute?(attr_name)
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
end
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
end
# Define read method for serialized attribute.
def define_read_method_for_serialized_attribute(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
end
# Define an attribute ? method.
def define_question_method(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
end
def define_write_method(attr_name)
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
end
# Evaluate the definition for an attribute related method
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
unless method_name.to_s == primary_key.to_s
generated_methods << method_name
end
begin
class_eval(method_definition, __FILE__, __LINE__)
rescue SyntaxError => err
generated_methods.delete(attr_name)
if logger
logger.warn "Exception occurred during reader method compilation."
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
logger.warn "#{err.message}"
end
end
end
end # ClassMethods
# Allows access to the object attributes, which are held in the @attributes hash, as though they
# were first-class methods. So a Person class with a name attribute can use Person#name and
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
# the completed attribute is not nil or 0.
#
# It's also possible to instantiate related objects, so a Client class belonging to the clients
# table with a master_id foreign key can instantiate master through Client#master.
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
if !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)
return self.send(method_id, *args, &block)
end
end
if self.class.primary_key.to_s == method_name
id
elsif md = self.class.match_attribute_method?(method_name)
attribute_name, method_type = md.pre_match, md.to_s
if @attributes.include?(attribute_name)
__send__("attribute#{method_type}", attribute_name, *args, &block)
else
super
end
elsif @attributes.include?(method_name)
read_attribute(method_name)
else
super
end
end
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
attr_name = attr_name.to_s
if !(value = @attributes[attr_name]).nil?
if column = column_for_attribute(attr_name)
if unserializable_attribute?(attr_name, column)
unserialize_attribute(attr_name)
else
column.type_cast(value)
end
else
value
end
else
nil
end
end
def read_attribute_before_type_cast(attr_name)
@attributes[attr_name]
end
# Returns true if the attribute is of a text column and marked for serialization.
def unserializable_attribute?(attr_name, column)
column.text? && self.class.serialized_attributes[attr_name]
end
# Returns the unserialized object of the attribute.
def unserialize_attribute(attr_name)
unserialized_object = object_from_yaml(@attributes[attr_name])
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
else
raise SerializationTypeMismatch,
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
end
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
# columns are turned into nil.
def write_attribute(attr_name, value)
attr_name = attr_name.to_s
@attributes_cache.delete(attr_name)
if (column = column_for_attribute(attr_name)) && column.number?
@attributes[attr_name] = convert_number_column_value(value)
else
@attributes[attr_name] = value
end
end
def query_attribute(attr_name)
unless value = read_attribute(attr_name)
false
else
column = self.class.columns_hash[attr_name]
if column.nil?
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
!value.blank?
end
elsif column.number?
!value.zero?
else
!value.blank?
end
end
end
# A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
# person.respond_to?("name?") which will all return true.
alias :respond_to_without_attributes? :respond_to?
def respond_to?(method, include_priv = false)
method_name = method.to_s
if super
return true
elsif !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)
return true
end
end
if @attributes.nil?
return super
elsif @attributes.include?(method_name)
return true
elsif md = self.class.match_attribute_method?(method_name)
return true if @attributes.include?(md.pre_match)
end
super
end
private
def missing_attribute(attr_name, stack)
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
end
# Handle *? for method_missing.
def attribute?(attribute_name)
query_attribute(attribute_name)

File diff suppressed because it is too large Load diff

View file

@ -6,79 +6,80 @@ module ActiveRecord
end
module ClassMethods
# Count operates using three different approaches.
# 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: This API has been deprecated and will be removed in Rails 2.0
# * Count using column : By passing a column name to count, it will return a count of all the rows for the model with supplied column present
# * 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:
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
#
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
# * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
# or named associations in the same form used for the :include option, which will perform an INNER JOIN on the associated table(s).
# If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
# Pass :readonly => false to override.
# * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
# to already defined associations. When using named associations count returns the number DISTINCT items for the model you're counting.
# to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting.
# See eager loading under Associations.
# * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * <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
# * <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>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
#
# Examples for counting all:
# Person.count # returns the total count of all people
#
# 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(*).
# Examples for counting by column:
# Person.count(:age) # returns the total count of all people whose age is present in database
#
# Examples for count with options:
# Person.count(:conditions => "age > 26")
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
# Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
# Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
#
# Note: Person.count(:all) will not work because it will use :all as the condition. Use Person.count instead.
def count(*args)
calculate(:count, *construct_count_options_from_legacy_args(*args))
calculate(:count, *construct_count_options_from_args(*args))
end
# Calculates average value on a given column. The value is returned as a float. See #calculate for examples with options.
# Calculates the average value on a given column. The value is returned as a float. See #calculate for examples with options.
#
# Person.average('age')
def average(column_name, options = {})
calculate(:avg, column_name, options)
end
# Calculates the minimum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options.
# Calculates the minimum value on a given column. The value is returned with the same data type of the column. See #calculate for examples with options.
#
# Person.minimum('age')
def minimum(column_name, options = {})
calculate(:min, column_name, options)
end
# Calculates the maximum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options.
# Calculates the maximum value on a given column. The value is returned with the same data type of the column. See #calculate for examples with options.
#
# Person.maximum('age')
def maximum(column_name, options = {})
calculate(:max, column_name, options)
end
# Calculates the sum value on a given column. The value is returned with the same data type of the column.. See #calculate for examples with options.
# Calculates the sum of values on a given column. The value is returned with the same data type of the column. See #calculate for examples with options.
#
# Person.sum('age')
def sum(column_name, options = {})
calculate(:sum, column_name, options)
end
# This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
# Options such as :conditions, :order, :group, :having, and :joins can be passed to customize the query.
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
# Options such as :conditions, :order, :group, :having, and :joins can be passed to customize the query.
#
# There are two basic forms of output:
# * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else.
# * Grouped values: This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name
# * Grouped values: This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name
# of a belongs_to association.
#
# values = Person.maximum(:age, :group => 'last_name')
@ -95,14 +96,15 @@ module ActiveRecord
# end
#
# Options:
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <tt>:joins</tt>: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
# * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
# * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * <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
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * <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>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
# * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
#
# Examples:
# Person.calculate(:count, :all) # The same as Person.count
@ -125,60 +127,54 @@ module ActiveRecord
end
protected
def construct_count_options_from_legacy_args(*args)
def construct_count_options_from_args(*args)
options = {}
column_name = :all
# We need to handle
# count()
# count(:column_name=:all)
# 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
case args.size
when 1
args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
when 2
column_name, options = args
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
end if args.size > 0
[column_name, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
operation = operation.to_s.downcase
options = options.symbolize_keys
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] * '.'
if operation == 'count'
if merged_includes.any?
options[:distinct] = true
column_name = options[:select] || [connection.quote_table_name(table_name), primary_key] * '.'
end
if options[:distinct]
use_workaround = !connection.supports_count_distinct?
end
end
sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
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} "
sql << " FROM #{connection.quote_table_name(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
@ -188,17 +184,17 @@ module ActiveRecord
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
group_key = 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'
if connection.adapter_name == 'FrontBase'
options[:having].downcase!
options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
sql << " HAVING #{options[:having]} "
end
@ -231,7 +227,8 @@ module ActiveRecord
end
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)
key = type_cast_calculated_value(row[group_alias], group_column)
key = key_records[key] if associated
value = row[aggregate_alias]
all << [key, type_cast_calculated_value(value, column, operation)]
end
@ -243,7 +240,7 @@ module ActiveRecord
end
# Converts a given key to the value that the database adapter returns as
# as a usable column name.
# a usable column name.
# users.id #=> users_id
# sum(id) #=> sum_id
# count(distinct users.id) #=> count_distinct_users_id
@ -261,7 +258,7 @@ module ActiveRecord
operation = operation.to_s.downcase
case operation
when 'count' then value.to_i
when 'avg' then value.to_f
when 'avg' then value && value.to_f
else column ? column.type_cast(value) : value
end
end

View file

@ -1,25 +1,25 @@
require 'observer'
module ActiveRecord
# Callbacks are hooks into the lifecycle of an Active Record object that allows you to trigger logic
# Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
# dependent objects are deleted when destroy is called (by overwriting before_destroy) or to massage attributes
# before they're validated (by overwriting before_validation). As an example of the callbacks initiated, consider
# the Base#save call:
# dependent objects are deleted when destroy is called (by overwriting +before_destroy+) or to massage attributes
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
# the <tt>Base#save</tt> call:
#
# * (-) save
# * (-) valid?
# * (1) before_validation
# * (2) before_validation_on_create
# * (-) validate
# * (-) validate_on_create
# * (3) after_validation
# * (4) after_validation_on_create
# * (5) before_save
# * (6) before_create
# * (-) create
# * (7) after_create
# * (8) after_save
# * (-) <tt>save</tt>
# * (-) <tt>valid</tt>
# * (1) <tt>before_validation</tt>
# * (2) <tt>before_validation_on_create</tt>
# * (-) <tt>validate</tt>
# * (-) <tt>validate_on_create</tt>
# * (3) <tt>after_validation</tt>
# * (4) <tt>after_validation_on_create</tt>
# * (5) <tt>before_save</tt>
# * (6) <tt>before_create</tt>
# * (-) <tt>create</tt>
# * (7) <tt>after_create</tt>
# * (8) <tt>after_save</tt>
#
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
# Active Record lifecycle.
@ -62,8 +62,8 @@ module ActiveRecord
# before_destroy :destroy_readers
# end
#
# Now, when Topic#destroy is run only +destroy_author+ is called. When Reply#destroy is run both +destroy_author+ and
# +destroy_readers+ is called. Contrast this to the situation where we've implemented the save behavior through overwriteable
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is run, both +destroy_author+ and
# +destroy_readers+ are called. Contrast this to the situation where we've implemented the save behavior through overwriteable
# methods:
#
# class Topic < ActiveRecord::Base
@ -74,9 +74,9 @@ module ActiveRecord
# def before_destroy() destroy_readers end
# end
#
# In that case, Reply#destroy would only run +destroy_readers+ and _not_ +destroy_author+. So use the callback macros when
# you want to ensure that a certain callback is called for the entire hierarchy and the regular overwriteable methods when you
# want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. So, use the callback macros when
# you want to ensure that a certain callback is called for the entire hierarchy, and use the regular overwriteable methods
# when you want to leave it up to each descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the
# associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't
@ -143,7 +143,7 @@ module ActiveRecord
# before_destroy 'self.class.delete_all "parent_id = #{id}"'
# end
#
# Notice that single plings (') are used so the #{id} part isn't evaluated until the callback is triggered. Also note that these
# Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback is triggered. Also note that these
# inline callbacks can be stacked just like the regular ones:
#
# class Topic < ActiveRecord::Base
@ -151,23 +151,23 @@ module ActiveRecord
# 'puts "Evaluated after parents are destroyed"'
# end
#
# == The after_find and after_initialize exceptions
# == The +after_find+ and +after_initialize+ exceptions
#
# Because after_find and after_initialize are called for each object found and instantiated by a finder, such as Base.find(:all), we've had
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
# after_initialize will only be run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the
# Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, such as <tt>Base.find(:all)</tt>, we've had
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, +after_find+ and
# +after_initialize+ 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
# == <tt>before_validation*</tt> 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.
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Base#save</tt> will return +false+.
# If <tt>Base#save!</tt> is called it will raise a +RecordNotSaved+ exception.
# Nothing will be appended to the errors object.
#
# == Cancelling callbacks
# == Canceling callbacks
#
# If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns
# false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.
module Callbacks
CALLBACKS = %w(
@ -177,16 +177,10 @@ module ActiveRecord
)
def self.included(base) #:nodoc:
base.extend(ClassMethods)
base.class_eval do
class << self
include Observable
alias_method_chain :instantiate, :callbacks
end
base.extend Observable
[:initialize, :create_or_update, :valid?, :create, :update, :destroy].each do |method|
alias_method_chain method, :callbacks
end
[:create_or_update, :valid?, :create, :update, :destroy].each do |method|
base.send :alias_method_chain, method, :callbacks
end
CALLBACKS.each do |method|
@ -199,39 +193,16 @@ module ActiveRecord
end
end
module ClassMethods #:nodoc:
def instantiate_with_callbacks(record)
object = instantiate_without_callbacks(record)
if object.respond_to_without_attributes?(:after_find)
object.send(:callback, :after_find)
end
if object.respond_to_without_attributes?(:after_initialize)
object.send(:callback, :after_initialize)
end
object
end
end
# Is called when the object was instantiated by one of the finders, like Base.find.
# Is called when the object was instantiated by one of the finders, like <tt>Base.find</tt>.
#def after_find() end
# Is called after the object has been instantiated by a call to Base.new.
# Is called after the object has been instantiated by a call to <tt>Base.new</tt>.
#def after_initialize() end
def initialize_with_callbacks(attributes = nil) #:nodoc:
initialize_without_callbacks(attributes)
result = yield self if block_given?
callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
result
end
# Is called _before_ Base.save (regardless of whether it's a create or update save).
# Is called _before_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
def before_save() end
# Is called _after_ Base.save (regardless of whether it's a create or update save).
# Is called _after_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
#
# class Contact < ActiveRecord::Base
# after_save { logger.info( 'New contact saved!' ) }
@ -243,11 +214,12 @@ module ActiveRecord
callback(:after_save)
result
end
private :create_or_update_with_callbacks
# Is called _before_ Base.save on new objects that haven't been saved yet (no record exists).
# Is called _before_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
def before_create() end
# Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
# Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
def after_create() end
def create_with_callbacks #:nodoc:
return false if callback(:before_create) == false
@ -255,11 +227,12 @@ module ActiveRecord
callback(:after_create)
result
end
private :create_with_callbacks
# Is called _before_ Base.save on existing objects that have a record.
# Is called _before_ <tt>Base.save</tt> on existing objects that have a record.
def before_update() end
# Is called _after_ Base.save on existing objects that have a record.
# Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
def after_update() end
def update_with_callbacks #:nodoc:
@ -268,26 +241,27 @@ module ActiveRecord
callback(:after_update)
result
end
private :update_with_callbacks
# Is called _before_ Validations.validate (which is part of the Base.save call).
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
def before_validation() end
# Is called _after_ Validations.validate (which is part of the Base.save call).
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
def after_validation() end
# Is called _before_ Validations.validate (which is part of the Base.save call) on new objects
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
# that haven't been saved yet (no record exists).
def before_validation_on_create() end
# Is called _after_ Validations.validate (which is part of the Base.save call) on new objects
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
# that haven't been saved yet (no record exists).
def after_validation_on_create() end
# Is called _before_ Validations.validate (which is part of the Base.save call) on
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
# existing objects that have a record.
def before_validation_on_update() end
# Is called _after_ Validations.validate (which is part of the Base.save call) on
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
# existing objects that have a record.
def after_validation_on_update() end
@ -304,13 +278,13 @@ module ActiveRecord
return result
end
# Is called _before_ Base.destroy.
# Is called _before_ <tt>Base.destroy</tt>.
#
# Note: If you need to _destroy_ or _nullify_ associated records first,
# use the _:dependent_ option on your associations.
# use the <tt>:dependent</tt> option on your associations.
def before_destroy() end
# Is called _after_ Base.destroy (and all the attributes have been frozen).
# Is called _after_ <tt>Base.destroy</tt> (and all the attributes have been frozen).
#
# class Contact < ActiveRecord::Base
# after_destroy { |record| logger.info( "Contact #{record.id} was destroyed." ) }

View file

@ -89,10 +89,23 @@ module ActiveRecord
# 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)
if @@allow_concurrency
# With concurrent connections @@active_connections is
# a hash keyed by thread id.
@@active_connections.each do |thread_id, conns|
conns.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections[thread_id].delete(name)
end
end
end
else
@@active_connections.each do |name, conn|
if conn.requires_reloading?
conn.disconnect!
@@active_connections.delete(name)
end
end
end
end
@ -206,15 +219,31 @@ module ActiveRecord
else
spec = spec.symbolize_keys
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
begin
require 'rubygems'
gem "activerecord-#{spec[:adapter]}-adapter"
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError
begin
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError
raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})"
end
end
adapter_method = "#{spec[:adapter]}_connection"
unless respond_to?(adapter_method) then raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" end
if !respond_to?(adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
end
remove_connection
establish_connection(ConnectionSpecification.new(spec, adapter_method))
end
end
# Locate the connection of the nearest super class. This can be an
# active or defined connections: if it is the latter, it will be
# active or defined connection: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
# for (not necessarily the current class).
def self.retrieve_connection #:nodoc:
@ -235,15 +264,15 @@ module ActiveRecord
conn or raise ConnectionNotEstablished
end
# Returns true if a connection that's accessible to this class have already been opened.
# Returns true if a connection that's accessible to this class has already been opened.
def self.connected?
active_connections[active_connection_name] ? true : false
end
# Remove the connection for this class. This will close the active
# connection and the defined connection (if they exist). The result
# can be used as argument for establish_connection, for easy
# re-establishing of the connection.
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def self.remove_connection(klass=self)
spec = @@defined_connections[klass.name]
konn = active_connections[klass.name]

View file

@ -10,21 +10,28 @@ module ActiveRecord
# 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 = select_all(sql, name)
result.first if result
end
# Returns a single value from a record
def select_value(sql, name = nil)
result = select_one(sql, name)
result.nil? ? nil : result.values.first
if result = select_one(sql, name)
result.values.first
end
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(sql, name = nil)
result = select_all(sql, name)
result.map{ |v| v.values.first }
result = select_rows(sql, name)
result.map { |v| v[0] }
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by #columns.
def select_rows(sql, name = nil)
raise NotImplementedError, "select_rows is an abstract method"
end
# Executes the SQL statement in the context of this connection.
@ -34,17 +41,17 @@ module ActiveRecord
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
raise NotImplementedError, "insert is an abstract method"
insert_sql(sql, name, pk, id_value, sequence_name)
end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil)
execute(sql, name)
update_sql(sql, name)
end
# Executes the delete statement and returns the number of rows affected.
def delete(sql, name = nil)
update(sql, name)
delete_sql(sql, name)
end
# Wrap a block in a transaction. Returns result of block.
@ -53,7 +60,7 @@ module ActiveRecord
begin
if block_given?
if start_db_transaction
begin_db_transaction
begin_db_transaction
transaction_open = true
end
yield
@ -63,10 +70,17 @@ module ActiveRecord
transaction_open = false
rollback_db_transaction
end
raise
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
commit_db_transaction if transaction_open
if transaction_open
begin
commit_db_transaction
rescue Exception => database_transaction_rollback
rollback_db_transaction
raise
end
end
end
# Begins the transaction (and turns off auto-committing).
@ -84,7 +98,7 @@ module ActiveRecord
add_limit_offset!(sql, options) if options
end
# Appends +LIMIT+ and +OFFSET+ options to a SQL statement.
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement.
# This method *modifies* the +sql+ parameter.
# ===== Examples
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
@ -99,14 +113,15 @@ module ActiveRecord
end
end
# Appends a locking clause to a SQL statement. *Modifies the +sql+ parameter*.
# Appends a locking clause to an SQL statement.
# This method *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}"
when true; sql << ' FOR UPDATE'
when String; sql << " #{lock}"
end
end
@ -119,12 +134,38 @@ module ActiveRecord
# Do nothing by default. Implement for PostgreSQL, Oracle, ...
end
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
end
def empty_insert_statement(table_name)
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
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
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
execute(sql, name)
id_value
end
# Executes the update statement and returns the number of rows affected.
def update_sql(sql, name = nil)
execute(sql, name)
end
# Executes the delete statement and returns the number of rows affected.
def delete_sql(sql, name = nil)
update_sql(sql, name)
end
end
end
end

View file

@ -0,0 +1,87 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module QueryCache
class << self
def included(base)
base.class_eval do
attr_accessor :query_cache_enabled
alias_method_chain :columns, :query_cache
alias_method_chain :select_all, :query_cache
end
dirties_query_cache base, :insert, :update, :delete
end
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__
def #{method_name}_with_query_dirty(*args)
clear_query_cache if @query_cache_enabled
#{method_name}_without_query_dirty(*args)
end
alias_method_chain :#{method_name}, :query_dirty
end_code
end
end
end
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
@query_cache ||= {}
yield
ensure
clear_query_cache
@query_cache_enabled = old
end
# Disable the query cache within the block.
def uncached
old, @query_cache_enabled = @query_cache_enabled, false
yield
ensure
@query_cache_enabled = old
end
def clear_query_cache
@query_cache.clear if @query_cache
end
def select_all_with_query_cache(*args)
if @query_cache_enabled
cache_sql(args.first) { select_all_without_query_cache(*args) }
else
select_all_without_query_cache(*args)
end
end
def columns_with_query_cache(*args)
if @query_cache_enabled
@query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
else
columns_without_query_cache(*args)
end
end
private
def cache_sql(sql)
result =
if @query_cache.has_key?(sql)
log_info(sql, "CACHE", 0.0)
@query_cache[sql]
else
@query_cache[sql] = yield
end
if Array === result
result.collect { |row| row.dup }
else
result.duplicable? ? result.dup : result
end
rescue TypeError
result
end
end
end
end

View file

@ -11,12 +11,12 @@ module ActiveRecord
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)
"#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
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)
"#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass then "NULL"
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
@ -24,9 +24,12 @@ module ActiveRecord
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(:db)}'"
when Time, DateTime then "'#{quoted_date(value)}'"
else "'#{quote_string(value.to_yaml)}'"
else
if value.acts_like?(:date) || value.acts_like?(:time)
"'#{quoted_date(value)}'"
else
"#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
end
end
end
@ -36,22 +39,30 @@ module ActiveRecord
s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
end
# Returns a quoted form of the column name. This is highly adapter
# specific.
def quote_column_name(name)
name
# Quotes the column name. Defaults to no quoting.
def quote_column_name(column_name)
column_name
end
# Quotes the table name. Defaults to column name quoting.
def quote_table_name(table_name)
quote_column_name(table_name)
end
def quoted_true
"'t'"
end
def quoted_false
"'f'"
end
def quoted_date(value)
value.strftime("%Y-%m-%d %H:%M:%S")
value.to_s(:db)
end
def quoted_string_prefix
''
end
end
end

View file

@ -6,6 +6,11 @@ module ActiveRecord
module ConnectionAdapters #:nodoc:
# An abstract definition of a column in a table.
class Column
module Format
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
attr_accessor :primary
@ -19,7 +24,7 @@ module ActiveRecord
@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)
@default = extract_default(default)
@primary = nil
end
@ -92,70 +97,113 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
# Used to convert from Strings to BLOBs
def self.string_to_binary(value)
value
def extract_default(default)
type_cast(default)
end
# Used to convert from BLOBs to Strings
def self.binary_to_string(value)
value
end
def self.string_to_date(string)
return string unless string.is_a?(String)
date_array = ParseDate.parsedate(string)
# treat 0000-00-00 as nil
Date.new(date_array[0], date_array[1], date_array[2]) rescue nil
end
def self.string_to_time(string)
return string unless string.is_a?(String)
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 DateTime.new(*time_array[0..5]) rescue nil
end
def self.string_to_dummy_time(string)
return string unless string.is_a?(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 = [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)
if value == true || value == false
class << self
# Used to convert from Strings to BLOBs
def string_to_binary(value)
value
else
%w(true t 1).include?(value.to_s.downcase)
end
end
# convert something to a BigDecimal
def self.value_to_decimal(value)
if value.is_a?(BigDecimal)
# Used to convert from BLOBs to Strings
def binary_to_string(value)
value
elsif value.respond_to?(:to_d)
value.to_d
else
value.to_s.to_d
end
def string_to_date(string)
return string unless string.is_a?(String)
return nil if string.empty?
fast_string_to_date(string) || fallback_string_to_date(string)
end
def string_to_time(string)
return string unless string.is_a?(String)
return nil if string.empty?
fast_string_to_time(string) || fallback_string_to_time(string)
end
def string_to_dummy_time(string)
return string unless string.is_a?(String)
return nil if string.empty?
string_to_time "2000-01-01 #{string}"
end
# convert something to a boolean
def value_to_boolean(value)
if value == true || value == false
value
else
%w(true t 1).include?(value.to_s.downcase)
end
end
# convert something to a BigDecimal
def 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
protected
# '0.123456' -> 123456
# '1.123456' -> 123456
def microseconds(time)
((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
end
def new_date(year, mon, mday)
if year && year != 0
Date.new(year, mon, mday) rescue nil
end
end
def new_time(year, mon, mday, hour, min, sec, microsec)
# Treat 0000-00-00 00:00:00 as nil.
return nil if year.nil? || year == 0
Time.send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec)
# Over/underflow to DateTime
rescue ArgumentError, TypeError
zone_offset = Base.default_timezone == :local ? DateTime.local_offset : 0
DateTime.civil(year, mon, mday, hour, min, sec, zone_offset) rescue nil
end
def fast_string_to_date(string)
if string =~ Format::ISO_DATE
new_date $1.to_i, $2.to_i, $3.to_i
end
end
# Doesn't handle time zones.
def fast_string_to_time(string)
if string =~ Format::ISO_DATETIME
microsec = ($7.to_f * 1_000_000).to_i
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
end
end
def fallback_string_to_date(string)
new_date *ParseDate.parsedate(string)[0..2]
end
def fallback_string_to_time(string)
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
new_time *time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)
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
@ -223,7 +271,7 @@ module ActiveRecord
end
# Represents a SQL table in an abstract way.
# Columns are stored as ColumnDefinition in the #columns attribute.
# Columns are stored as a ColumnDefinition in the #columns attribute.
class TableDefinition
attr_accessor :columns
@ -244,24 +292,29 @@ module ActiveRecord
end
# Instantiates a new column for the table.
# The +type+ parameter must be one of the following values:
# The +type+ parameter is normally one of the migrations native types,
# which is one of the following:
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</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>.
#
# You may use a type not in this list as long as it is supported by your
# database (for example, "polygon" in MySQL), but this will not be database
# agnostic and should usually be avoided.
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt>:
# * <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>:
# * <tt>:default</tt> -
# The column's default value. Use nil for NULL.
# * <tt>:null</tt>:
# * <tt>:null</tt> -
# Allows or disallows +NULL+ values in the column. This option could
# have been named <tt>:null_allowed</tt>.
# * <tt>:precision</tt>:
# * <tt>:precision</tt> -
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt>:
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
#
# Please be aware of different RDBMS implementations behavior with
@ -295,7 +348,7 @@ module ActiveRecord
#
# This method returns <tt>self</tt>.
#
# ===== Examples
# == Examples
# # Assuming td is an instance of TableDefinition
# td.column(:granted, :boolean)
# #=> granted BOOLEAN
@ -316,6 +369,52 @@ module ActiveRecord
# # probably wouldn't hurt to include it.
# def.column(:huge_integer, :decimal, :precision => 30)
# #=> huge_integer DECIMAL(30)
#
# == Short-hand examples
#
# Instead of calling column directly, you can also work with the short-hand definitions for the default types.
# They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
# in a single statement.
#
# What can be written like this with the regular calls to column:
#
# create_table "products", :force => true do |t|
# t.column "shop_id", :integer
# t.column "creator_id", :integer
# t.column "name", :string, :default => "Untitled"
# t.column "value", :string, :default => "Untitled"
# t.column "created_at", :datetime
# t.column "updated_at", :datetime
# end
#
# Can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
# t.string :name, :value, :default => "Untitled"
# t.timestamps
# end
#
# There's a short-hand method for each of the type values declared at the top. And then there's
# TableDefinition#timestamps that'll add created_at and updated_at as datetimes.
#
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
# column if the :polymorphic option is supplied. If :polymorphic is a hash of options, these will be
# used when creating the _type column. So what can be written like this:
#
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
# t.string :tagger_type
# t.string :taggable_type, :default => 'Photo'
# end
#
# Can also be written as follows using references:
#
# create_table :taggings do |t|
# t.references :tag
# t.references :tagger, :polymorphic => true
# t.references :taggable, :polymorphic => { :default => 'Photo' }
# end
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]
@ -327,8 +426,38 @@ module ActiveRecord
self
end
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV
def #{column_type}(*args)
options = args.extract_options!
column_names = args
column_names.each { |name| column(name, '#{column_type}', options) }
end
EOV
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
def timestamps
column(:created_at, :datetime)
column(:updated_at, :datetime)
end
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
args.each do |col|
column("#{col}_id", :integer, options)
unless polymorphic.nil?
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : {})
end
end
end
alias :belongs_to :references
# Returns a String whose contents are the column definitions
# concatenated together. This string can then be pre and appended to
# concatenated together. This string can then be prepended and appended to
# to generate the final SQL to create the table.
def to_sql
@columns * ', '

View file

@ -54,7 +54,7 @@ module ActiveRecord
# [<tt>:temporary</tt>]
# Make a temporary table.
# [<tt>:force</tt>]
# Set to true or false to drop the table before creating it.
# Set to true to drop the table before creating it.
# Defaults to false.
#
# ===== Examples
@ -81,45 +81,45 @@ module ActiveRecord
# t.column :supplier_id, :integer
# end
# generates:
# CREATE TABLE categories_suppliers_join (
# CREATE TABLE categories_suppliers (
# category_id int,
# supplier_id int
# )
#
# See also TableDefinition#column for details on how to create columns.
def create_table(name, options = {})
def create_table(table_name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
if options[:force]
drop_table(name, options) rescue nil
drop_table(table_name, options) rescue nil
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << "#{quote_table_name(table_name)} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
execute create_sql
end
# Renames a table.
# ===== Example
# rename_table('octopuses', 'octopi')
def rename_table(name, new_name)
def rename_table(table_name, new_name)
raise NotImplementedError, "rename_table is not implemented"
end
# Drops a table from the database.
def drop_table(name, options = {})
execute "DROP TABLE #{name}"
def drop_table(table_name, options = {})
execute "DROP TABLE #{quote_table_name(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], options[:precision], options[:scale])}"
add_column_sql = "ALTER TABLE #{quote_table_name(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
@ -128,7 +128,7 @@ module ActiveRecord
# ===== Examples
# remove_column(:suppliers, :qualification)
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{quote_column_name(column_name)}"
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
end
# Changes the column's definition according to the new options.
@ -142,7 +142,7 @@ module ActiveRecord
# Sets a new default value for a column. If you want to set the default
# value to +NULL+, you are out of luck. You need to
# DatabaseStatements#execute the apppropriate SQL statement yourself.
# DatabaseStatements#execute the appropriate SQL statement yourself.
# ===== Examples
# change_column_default(:suppliers, :qualification, 'new')
# change_column_default(:accounts, :authorized, 1)
@ -160,13 +160,13 @@ module ActiveRecord
# Adds a new index to the table. +column_name+ can be a single Symbol, or
# an Array of Symbols.
#
# The index will be named after the table and the first column names,
# The index will be named after the table and the first column name,
# unless you pass +:name+ as an option.
#
# When creating an index on multiple columns, the first column is used as a name
# for the index. For example, when you specify an index on two columns
# [+:first+, +:last+], the DBMS creates an index for both columns as well as an
# index for the first colum +:first+. Using just the first name for this index
# index for the first column +:first+. Using just the first name for this index
# makes sense, because you will never have to create a singular index with this
# name.
#
@ -194,7 +194,7 @@ module ActiveRecord
index_type = options
end
quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{table_name} (#{quoted_column_names})"
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
end
# Remove the given index from the table.
@ -234,17 +234,17 @@ module ActiveRecord
# The migrations module handles this automatically.
def initialize_schema_information
begin
execute "CREATE TABLE #{ActiveRecord::Migrator.schema_info_table_name} (version #{type_to_sql(:integer)})"
execute "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} (version) VALUES(0)"
execute "CREATE TABLE #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version #{type_to_sql(:integer)})"
execute "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES(0)"
rescue ActiveRecord::StatementInvalid
# Schema has been intialized
# Schema has been initialized
end
end
def dump_schema_information #:nodoc:
begin
if (current_schema = ActiveRecord::Migrator.current_version) > 0
return "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} (version) VALUES (#{current_schema})"
return "INSERT INTO #{quote_table_name(ActiveRecord::Migrator.schema_info_table_name)} (version) VALUES (#{current_schema})"
end
rescue ActiveRecord::StatementInvalid
# No Schema Info
@ -253,25 +253,28 @@ module ActiveRecord
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
native = native_database_types[type]
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})"
if native = native_database_types[type]
column_type_sql = native.is_a?(Hash) ? native[:name] : native
if type == :decimal # ignore limit, use precision and scale
precision ||= native[:precision]
scale ||= native[:scale]
if precision
if scale
column_type_sql << "(#{precision},#{scale})"
else
column_type_sql << "(#{precision})"
end
else
column_type_sql << "(#{precision})"
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified" if scale
end
column_type_sql
else
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified" if scale
limit ||= native[:limit]
column_type_sql << "(#{limit})" if limit
column_type_sql
end
column_type_sql
else
limit ||= native[:limit]
column_type_sql << "(#{limit})" if limit
column_type_sql
column_type_sql = type
end
end
@ -291,7 +294,7 @@ module ActiveRecord
# 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]}"
sql << " ORDER BY #{options[:order]}"
end
protected

View file

@ -8,6 +8,7 @@ require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
require 'active_record/connection_adapters/abstract/quoting'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@ -22,8 +23,9 @@ module ActiveRecord
# SchemaStatements#remove_column are very useful.
class AbstractAdapter
include Quoting, DatabaseStatements, SchemaStatements
include QueryCache
@@row_even = true
def initialize(connection, logger = nil) #:nodoc:
@connection, @logger = connection, logger
@runtime = 0
@ -41,7 +43,7 @@ module ActiveRecord
def supports_migrations?
false
end
# Does this adapter support using DISTINCT within COUNT? This is +true+
# for all adapters except sqlite.
def supports_count_distinct?
@ -61,6 +63,19 @@ module ActiveRecord
rt
end
# QUOTING ==================================================
# Override to return the quoted table name if the database needs it
def quote_table_name(name)
name
end
# REFERENTIAL INTEGRITY ====================================
# Override to turn off referential integrity while executing +&block+
def disable_referential_integrity(&block)
yield
end
# CONNECTION MANAGEMENT ====================================
@ -86,7 +101,7 @@ module ActiveRecord
end
# Lazily verify this connection, calling +active?+ only if it hasn't
# been called for +timeout+ seconds.
# been called for +timeout+ seconds.
def verify!(timeout)
now = Time.now.to_i
if (now - @last_verification) > timeout
@ -94,7 +109,7 @@ module ActiveRecord
@last_verification = now
end
end
# Provides access to the underlying database connection. Useful for
# when you need to call a proprietary method such as postgresql's lo_*
# methods
@ -102,10 +117,17 @@ module ActiveRecord
@connection
end
def log_info(sql, name, runtime)
if @logger && @logger.debug?
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})"
@logger.debug format_log_entry(name, sql.squeeze(' '))
end
end
protected
def log(sql, name)
if block_given?
if @logger and @logger.level <= Logger::INFO
if @logger and @logger.debug?
result = nil
seconds = Benchmark.realtime { result = yield }
@runtime += seconds
@ -120,7 +142,7 @@ module ActiveRecord
end
rescue Exception => e
# Log message and raise exception.
# Set last_verfication to 0, so that connection gets verified
# Set last_verification to 0, so that connection gets verified
# upon reentering the request loop
@last_verification = 0
message = "#{e.class.name}: #{e.message}: #{sql}"
@ -128,17 +150,6 @@ module ActiveRecord
raise ActiveRecord::StatementInvalid, message
end
def log_info(sql, name, runtime)
return unless @logger
@logger.debug(
format_log_entry(
"#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})",
sql.gsub(/ +/, " ")
)
)
end
def format_log_entry(message, dump = nil)
if ActiveRecord::Base.colorize_logging
if @@row_even

View file

@ -1,228 +0,0 @@
# Author/Maintainer: Maik Schmidt <contact@maik-schmidt.de>
require 'active_record/connection_adapters/abstract_adapter'
begin
require 'db2/db2cli' unless self.class.const_defined?(:DB2CLI)
require 'active_record/vendor/db2'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by
# all Active Record objects
def self.db2_connection(config) # :nodoc:
config = config.symbolize_keys
usr = config[:username]
pwd = config[:password]
schema = config[:schema]
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, 'No database specified. Missing argument: database.'
end
connection = DB2::Connection.new(DB2::Environment.new)
connection.connect(database, usr, pwd)
ConnectionAdapters::DB2Adapter.new(connection, logger, :schema => schema)
end
end
module ConnectionAdapters
# The DB2 adapter works with the C-based CLI driver (http://rubyforge.org/projects/ruby-dbi/)
#
# Options:
#
# * <tt>:username</tt> -- Defaults to nothing
# * <tt>:password</tt> -- Defaults to nothing
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
# * <tt>:schema</tt> -- Database schema to be set initially.
class DB2Adapter < AbstractAdapter
def initialize(connection, logger, connection_options)
super(connection, logger)
@connection_options = connection_options
if schema = @connection_options[:schema]
with_statement do |stmt|
stmt.exec_direct("SET SCHEMA=#{schema}")
end
end
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
execute(sql, name = nil)
id_value || last_insert_id
end
def execute(sql, name = nil)
rows_affected = 0
with_statement do |stmt|
log(sql, name) do
stmt.exec_direct(sql)
rows_affected = stmt.row_count
end
end
rows_affected
end
def begin_db_transaction
@connection.set_auto_commit_off
end
def commit_db_transaction
@connection.commit
@connection.set_auto_commit_on
end
def rollback_db_transaction
@connection.rollback
@connection.set_auto_commit_on
end
def quote_column_name(column_name)
column_name
end
def adapter_name()
'DB2'
end
def quote_string(string)
string.gsub(/'/, "''") # ' (for ruby-mode)
end
def add_limit_offset!(sql, options)
if limit = options[:limit]
offset = options[:offset] || 0
# The following trick was added by andrea+rails@webcom.it.
sql.gsub!(/SELECT/i, 'SELECT B.* FROM (SELECT A.*, row_number() over () AS internal$rownum FROM (SELECT')
sql << ") A ) B WHERE B.internal$rownum > #{offset} AND B.internal$rownum <= #{limit + offset}"
end
end
def tables(name = nil)
result = []
schema = @connection_options[:schema] || '%'
with_statement do |stmt|
stmt.tables(schema).each { |t| result << t[2].downcase }
end
result
end
def indexes(table_name, name = nil)
tmp = {}
schema = @connection_options[:schema] || ''
with_statement do |stmt|
stmt.indexes(table_name, schema).each do |t|
next unless t[5]
next if t[4] == 'SYSIBM' # Skip system indexes.
idx_name = t[5].downcase
col_name = t[8].downcase
if tmp.has_key?(idx_name)
tmp[idx_name].columns << col_name
else
is_unique = t[3] == 0
tmp[idx_name] = IndexDefinition.new(table_name, idx_name, is_unique, [col_name])
end
end
end
tmp.values
end
def columns(table_name, name = nil)
result = []
schema = @connection_options[:schema] || '%'
with_statement do |stmt|
stmt.columns(table_name, schema).each do |c|
c_name = c[3].downcase
c_default = c[12] == 'NULL' ? nil : c[12]
c_default.gsub!(/^'(.*)'$/, '\1') if !c_default.nil?
c_type = c[5].downcase
c_type += "(#{c[6]})" if !c[6].nil? && c[6] != ''
result << Column.new(c_name, c_default, c_type, c[17] == 'YES')
end
end
result
end
def native_database_types
{
:primary_key => 'int generated by default as identity (start with 42) primary key',
:string => { :name => 'varchar', :limit => 255 },
:text => { :name => 'clob', :limit => 32768 },
:integer => { :name => 'int' },
:float => { :name => 'float' },
:decimal => { :name => 'decimal' },
:datetime => { :name => 'timestamp' },
:timestamp => { :name => 'timestamp' },
:time => { :name => 'time' },
:date => { :name => 'date' },
:binary => { :name => 'blob', :limit => 32768 },
:boolean => { :name => 'decimal', :limit => 1 }
}
end
def quoted_true
'1'
end
def quoted_false
'0'
end
def active?
@connection.select_one 'select 1 from ibm.sysdummy1'
true
rescue Exception
false
end
def reconnect!
end
def table_alias_length
128
end
private
def with_statement
stmt = DB2::Statement.new(@connection)
yield stmt
stmt.free
end
def last_insert_id
row = select_one(<<-GETID.strip)
with temp(id) as (values (identity_val_local())) select * from temp
GETID
row['id'].to_i
end
def select(sql, name = nil)
rows = []
with_statement do |stmt|
log(sql, name) do
stmt.exec_direct("#{sql.gsub(/=\s*null/i, 'IS NULL')} with ur")
end
while row = stmt.fetch_as_hash
row.delete('internal$rownum')
rows << row
end
end
rows
end
end
end
end
rescue LoadError
# DB2 driver is unavailable.
module ActiveRecord # :nodoc:
class Base
def self.db2_connection(config) # :nodoc:
# Set up a reasonable error message
raise LoadError, "DB2 Libraries could not be loaded."
end
end
end
end

View file

@ -1,728 +0,0 @@
# Author: Ken Kunz <kennethkunz@gmail.com>
require 'active_record/connection_adapters/abstract_adapter'
module FireRuby # :nodoc: all
NON_EXISTENT_DOMAIN_ERROR = "335544569"
class Database
def self.db_string_for(config)
unless config.has_key?(:database)
raise ArgumentError, "No database specified. Missing argument: database."
end
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
module ActiveRecord
class << Base
def firebird_connection(config) # :nodoc:
require_library_or_gem 'fireruby'
unless defined? FireRuby::SQLType
raise AdapterNotFound,
'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.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
end
module ConnectionAdapters
class FirebirdColumn < Column # :nodoc:
VARCHAR_MAX_LENGTH = 32_765
BLOB_MAX_LENGTH = 32_767
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 = decide_limit(length)
@domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale.abs
end
def type
if @domain =~ /BOOLEAN/
:boolean
elsif @type == :binary and @sub_type == 1
:text
else
@type
end
end
def default
type_cast(decide_default) if @default
end
def self.value_to_boolean(value)
%W(#{FirebirdAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
end
private
def parse_default(default_source)
default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i
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})"
when 'CHAR', 'VARCHAR' then "#{@firebird_type}(#{@limit})"
when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})"
when 'DOUBLE' then "DOUBLE PRECISION"
else @firebird_type
end
end
def simplified_type(field_type)
if field_type == 'TIMESTAMP'
:datetime
else
super
end
end
end
# The Firebird adapter relies on the FireRuby[http://rubyforge.org/projects/fireruby/]
# extension, version 0.4.0 or later (available as a gem or from
# RubyForge[http://rubyforge.org/projects/fireruby/]). FireRuby works with
# Firebird 1.5.x on Linux, OS X and Win32 platforms.
#
# == Usage Notes
#
# === Sequence (Generator) Names
# The Firebird adapter supports the same approach adopted for the Oracle
# adapter. See ActiveRecord::Base#set_sequence_name for more details.
#
# Note that in general there is no need to create a <tt>BEFORE INSERT</tt>
# trigger corresponding to a Firebird sequence generator when using
# ActiveRecord. In other words, you don't have to try to make Firebird
# simulate an <tt>AUTO_INCREMENT</tt> or +IDENTITY+ column. When saving a
# new record, ActiveRecord pre-fetches the next sequence value for the table
# and explicitly includes it in the +INSERT+ statement. (Pre-fetching the
# next primary key value is the only reliable method for the Firebird
# adapter to report back the +id+ after a successful insert.)
#
# === BOOLEAN Domain
# 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) 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
# the column as a +BOOLEAN+.
#
# By default, the Firebird adapter will assume that the BOOLEAN domain is
# defined as above. This can be modified if needed. For example, if you
# have a legacy schema with the following +BOOLEAN+ domain defined:
#
# CREATE DOMAIN BOOLEAN AS CHAR(1) CHECK (VALUE IN ('T', 'F'));
#
# ...you can add the following line to your <tt>environment.rb</tt> file:
#
# ActiveRecord::ConnectionAdapters::FirebirdAdapter.boolean_domain = { :true => 'T', :false => 'F' }
#
# === BLOB Elements
# The Firebird adapter currently provides only limited support for +BLOB+
# columns. You cannot currently retrieve or insert a +BLOB+ as an IO stream.
# When selecting a +BLOB+, the entire element is converted into a String.
# When inserting or updating a +BLOB+, the entire value is included in-line
# in the SQL statement, limiting you to values <= 32KB in size.
#
# === Column Name Case Semantics
# Firebird and ActiveRecord have somewhat conflicting case semantics for
# column names.
#
# [*Firebird*]
# The standard practice is to use unquoted column names, which can be
# thought of as case-insensitive. (In fact, Firebird converts them to
# uppercase.) Quoted column names (not typically used) are case-sensitive.
# [*ActiveRecord*]
# Attribute accessors corresponding to column names are case-sensitive.
# The defaults for primary key and inheritance columns are lowercase, and
# in general, people use lowercase attribute names.
#
# In order to map between the differing semantics in a way that conforms
# to common usage for both Firebird and ActiveRecord, uppercase column names
# in Firebird are converted to lowercase attribute names in ActiveRecord,
# and vice-versa. Mixed-case column names retain their case in both
# directions. Lowercase (quoted) Firebird column names are not supported.
# This is similar to the solutions adopted by other adapters.
#
# In general, the best approach is to use unqouted (case-insensitive) column
# names in your Firebird DDL (or if you must quote, use uppercase column
# names). These will correspond to lowercase attributes in ActiveRecord.
#
# For example, a Firebird table based on the following DDL:
#
# CREATE TABLE products (
# id BIGINT NOT NULL PRIMARY KEY,
# "TYPE" VARCHAR(50),
# name VARCHAR(255) );
#
# ...will correspond to an ActiveRecord model class called +Product+ with
# the following attributes: +id+, +type+, +name+.
#
# ==== Quoting <tt>"TYPE"</tt> and other Firebird reserved words:
# In ActiveRecord, the default inheritance column name is +type+. The word
# _type_ is a Firebird reserved word, so it must be quoted in any Firebird
# SQL statements. Because of the case mapping described above, you should
# always reference this column using quoted-uppercase syntax
# (<tt>"TYPE"</tt>) within Firebird DDL or other SQL statements (as in the
# example above). This holds true for any other Firebird reserved words used
# as column names as well.
#
# === Migrations
# 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
# options have default values.
#
# <tt>:database</tt>::
# <i>Required option.</i> Specifies one of: (i) a Firebird database alias;
# (ii) the full path of a database file; _or_ (iii) a full Firebird
# connection string. <i>Do not specify <tt>:host</tt>, <tt>:service</tt>
# or <tt>:port</tt> as separate options when using a full connection
# string.</i>
# <tt>:host</tt>::
# Set to <tt>"remote.host.name"</tt> for remote database connections.
# May be omitted for local connections if a full database path is
# specified for <tt>:database</tt>. Some platforms require a value of
# <tt>"localhost"</tt> for local connections when using a Firebird
# database _alias_.
# <tt>:service</tt>::
# Specifies a service name for the connection. Only used if <tt>:host</tt>
# is provided. Required when connecting to a non-standard service.
# <tt>:port</tt>::
# Specifies the connection port. Only used if <tt>:host</tt> is provided
# and <tt>:service</tt> is not. Required when connecting to a non-standard
# port and <tt>:service</tt> is not defined.
# <tt>:username</tt>::
# Specifies the database user. May be omitted or set to +nil+ (together
# with <tt>:password</tt>) to use the underlying operating system user
# credentials on supported platforms.
# <tt>:password</tt>::
# Specifies the database password. Must be provided if <tt>:username</tt>
# is explicitly specified; should be omitted if OS user credentials are
# are being used.
# <tt>:charset</tt>::
# Specifies the character set to be used by the connection. Refer to
# Firebird documentation for valid options.
class FirebirdAdapter < AbstractAdapter
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)
super(connection, logger)
@connection_params = connection_params
end
def adapter_name # :nodoc:
'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 = nil) # :nodoc:
"#{table_name}_seq"
end
# QUOTING ==================================================
def quote(value, column = nil) # :nodoc:
if [Time, DateTime].include?(value.class)
"CAST('#{value.strftime("%Y-%m-%d %H:%M:%S")}' AS TIMESTAMP)"
else
super
end
end
def quote_string(string) # :nodoc:
string.gsub(/'/, "''")
end
def quote_column_name(column_name) # :nodoc:
%Q("#{ar_to_fb_case(column_name.to_s)}")
end
def quoted_true # :nodoc:
quote(boolean_domain[:true])
end
def quoted_false # :nodoc:
quote(boolean_domain[:false])
end
# CONNECTION MANAGEMENT ====================================
def active? # :nodoc:
not @connection.closed?
end
def disconnect! # :nodoc:
@connection.close rescue nil
end
def reconnect! # :nodoc:
disconnect!
@connection = @connection.database.connect(*@connection_params)
end
# DATABASE STATEMENTS ======================================
def select_all(sql, name = nil) # :nodoc:
select(sql, name)
end
def select_one(sql, name = nil) # :nodoc:
select(sql, name).first
end
def execute(sql, name = nil, &block) # :nodoc:
log(sql, name) do
if @transaction
@connection.execute(sql, @transaction, &block)
else
@connection.execute_immediate(sql, &block)
end
end
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # :nodoc:
execute(sql, name)
id_value
end
alias_method :update, :execute
alias_method :delete, :execute
def begin_db_transaction() # :nodoc:
@transaction = @connection.start_transaction
end
def commit_db_transaction() # :nodoc:
@transaction.commit
ensure
@transaction = nil
end
def rollback_db_transaction() # :nodoc:
@transaction.rollback
ensure
@transaction = nil
end
def add_limit_offset!(sql, options) # :nodoc:
if options[:limit]
limit_string = "FIRST #{options[:limit]}"
limit_string << " SKIP #{options[:offset]}" if options[:offset]
sql.sub!(/\A(\s*SELECT\s)/i, '\&' + limit_string + ' ')
end
end
# 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)
FireRuby::Generator.new(sequence_name, @connection).next(1)
end
# 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
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,
COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
FROM rdb$relation_fields r
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
execute(sql, name).collect do |field|
field_values = field.values.collect do |value|
case value
when String then value.rstrip
when FireRuby::Blob then value.to_s
else value
end
end
FirebirdColumn.new(*field_values)
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 = {}
row.each do |column, value|
value = value.to_s if FireRuby::Blob === value
hashed_row[fb_to_ar_case(column)] = value
end
hashed_row
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)
column_name =~ /[[:lower:]]/ ? column_name : column_name.downcase
end
# Maps lowercase ActiveRecord column names to uppercase for Fierbird;
# mixed-case columns retain their original case.
def ar_to_fb_case(column_name)
column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
end
end
end
end

View file

@ -1,861 +0,0 @@
# Requires FrontBase Ruby bindings (gem install ruby-frontbase)
require 'active_record/connection_adapters/abstract_adapter'
FB_TRACE = false
module ActiveRecord
class Base
class << self
# Establishes a connection to the database that's used by all Active Record objects.
def frontbase_connection(config) # :nodoc:
# FrontBase only supports one unnamed sequence per table
define_attr_method(:set_sequence_name, :sequence_name, &Proc.new {|*args| nil})
config = config.symbolize_keys
database = config[:database]
port = config[:port]
host = config[:host]
username = config[:username]
password = config[:password]
dbpassword = config[:dbpassword]
session_name = config[:session_name]
dbpassword = '' if dbpassword.nil?
# Turn off colorization since it makes tail/less output difficult
self.colorize_logging = false
require_library_or_gem 'frontbase' unless self.class.const_defined? :FBSQL_Connect
# Check bindings version
version = "0.0.0"
version = FBSQL_Connect::FB_BINDINGS_VERSION if defined? FBSQL_Connect::FB_BINDINGS_VERSION
if ActiveRecord::ConnectionAdapters::FrontBaseAdapter.compare_versions(version,"1.0.0") == -1
raise AdapterNotFound,
'The FrontBase adapter requires ruby-frontbase version 1.0.0 or greater; you appear ' <<
"to be running an older version (#{version}) -- please update ruby-frontbase (gem install ruby-frontbase)."
end
connection = FBSQL_Connect.connect(host, port, database, username, password, dbpassword, session_name)
ConnectionAdapters::FrontBaseAdapter.new(connection, logger, [host, port, database, username, password, dbpassword, session_name], config)
end
end
end
module ConnectionAdapters
# From EOF Documentation....
# buffer should have space for EOUniqueBinaryKeyLength (12) bytes.
# Assigns a world-wide unique ID made up of:
# < Sequence [2], ProcessID [2] , Time [4], IP Addr [4] >
class TwelveByteKey < String #:nodoc:
@@mutex = Mutex.new
@@sequence_number = rand(65536)
@@key_cached_pid_component = nil
@@key_cached_ip_component = nil
def initialize(string = nil)
# Generate a unique key
if string.nil?
new_key = replace('_' * 12)
new_key[0..1] = self.class.key_sequence_component
new_key[2..3] = self.class.key_pid_component
new_key[4..7] = self.class.key_time_component
new_key[8..11] = self.class.key_ip_component
new_key
else
if string.size == 24
string.gsub!(/[[:xdigit:]]{2}/) { |x| x.hex.chr }
end
raise "string is not 12 bytes long" unless string.size == 12
super(string)
end
end
def inspect
unpack("H*").first.upcase
end
alias_method :to_s, :inspect
private
class << self
def key_sequence_component
seq = nil
@@mutex.synchronize do
seq = @@sequence_number
@@sequence_number = (@@sequence_number + 1) % 65536
end
sequence_component = "__"
sequence_component[0] = seq >> 8
sequence_component[1] = seq
sequence_component
end
def key_pid_component
if @@key_cached_pid_component.nil?
@@mutex.synchronize do
pid = $$
pid_component = "__"
pid_component[0] = pid >> 8
pid_component[1] = pid
@@key_cached_pid_component = pid_component
end
end
@@key_cached_pid_component
end
def key_time_component
time = Time.new.to_i
time_component = "____"
time_component[0] = (time & 0xFF000000) >> 24
time_component[1] = (time & 0x00FF0000) >> 16
time_component[2] = (time & 0x0000FF00) >> 8
time_component[3] = (time & 0x000000FF)
time_component
end
def key_ip_component
if @@key_cached_ip_component.nil?
@@mutex.synchronize do
old_lookup_flag = BasicSocket.do_not_reverse_lookup
BasicSocket.do_not_reverse_lookup = true
udpsocket = UDPSocket.new
udpsocket.connect("17.112.152.32",1)
ip_string = udpsocket.addr[3]
BasicSocket.do_not_reverse_lookup = old_lookup_flag
packed = Socket.pack_sockaddr_in(0,ip_string)
addr_subset = packed[4..7]
ip = addr_subset[0] << 24 | addr_subset[1] << 16 | addr_subset[2] << 8 | addr_subset[3]
ip_component = "____"
ip_component[0] = (ip & 0xFF000000) >> 24
ip_component[1] = (ip & 0x00FF0000) >> 16
ip_component[2] = (ip & 0x0000FF00) >> 8
ip_component[3] = (ip & 0x000000FF)
@@key_cached_ip_component = ip_component
end
end
@@key_cached_ip_component
end
end
end
class FrontBaseColumn < Column #:nodoc:
attr_reader :fb_autogen
def initialize(base, name, type, typename, limit, precision, scale, default, nullable)
@base = base
@name = name
@type = simplified_type(type,typename,limit)
@limit = limit
@precision = precision
@scale = scale
@default = default
@null = nullable == "YES"
@text = [:string, :text].include? @type
@number = [:float, :integer, :decimal].include? @type
@fb_autogen = false
if @default
@default.gsub!(/^'(.*)'$/,'\1') if @text
@fb_autogen = @default.include?("SELECT UNIQUE FROM")
case @type
when :boolean
@default = @default == "TRUE"
when :binary
if @default != "X''"
buffer = ""
@default.scan(/../) { |h| buffer << h.hex.chr }
@default = buffer
else
@default = ""
end
else
@default = type_cast(@default)
end
end
end
# Casts value (which is a String) to an appropriate instance.
def type_cast(value)
if type == :twelvebytekey
ActiveRecord::ConnectionAdapters::TwelveByteKey.new(value)
else
super(value)
end
end
def type_cast_code(var_name)
if type == :twelvebytekey
"ActiveRecord::ConnectionAdapters::TwelveByteKey.new(#{var_name})"
else
super(var_name)
end
end
private
def simplified_type(field_type, type_name,limit)
ret_type = :string
puts "typecode: [#{field_type}] [#{type_name}]" if FB_TRACE
# 12 byte primary keys are a special case that Apple's EOF
# used heavily. Optimize for this case
if field_type == 11 && limit == 96
ret_type = :twelvebytekey # BIT(96)
else
ret_type = case field_type
when 1 then :boolean # BOOLEAN
when 2 then :integer # INTEGER
when 4 then :float # FLOAT
when 10 then :string # CHARACTER VARYING
when 11 then :bitfield # BIT
when 13 then :date # DATE
when 14 then :time # TIME
when 16 then :timestamp # TIMESTAMP
when 20 then :text # CLOB
when 21 then :binary # BLOB
when 22 then :integer # TINYINT
else
puts "ERROR: Unknown typecode: [#{field_type}] [#{type_name}]"
end
end
puts "ret_type: #{ret_type.inspect}" if FB_TRACE
ret_type
end
end
class FrontBaseAdapter < AbstractAdapter
class << self
def compare_versions(v1, v2)
v1_seg = v1.split(".")
v2_seg = v2.split(".")
0.upto([v1_seg.length,v2_seg.length].min) do |i|
step = (v1_seg[i].to_i <=> v2_seg[i].to_i)
return step unless step == 0
end
return v1_seg.length <=> v2_seg.length
end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@transaction_mode = :pessimistic
# Start out in auto-commit mode
self.rollback_db_transaction
# threaded_connections_test.rb will fail unless we set the session
# to optimistic locking mode
# set_pessimistic_transactions
# execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC"
end
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name #:nodoc:
'FrontBase'
end
# Does this adapter support migrations? Backend specific, as the
# abstract adapter always returns +false+.
def supports_migrations? #:nodoc:
true
end
def native_database_types #:nodoc:
{
:primary_key => "INTEGER DEFAULT UNIQUE PRIMARY KEY",
:string => { :name => "VARCHAR", :limit => 255 },
:text => { :name => "CLOB" },
:integer => { :name => "INTEGER" },
:float => { :name => "FLOAT" },
:decimal => { :name => "DECIMAL" },
:datetime => { :name => "TIMESTAMP" },
:timestamp => { :name => "TIMESTAMP" },
:time => { :name => "TIME" },
:date => { :name => "DATE" },
:binary => { :name => "BLOB" },
:boolean => { :name => "BOOLEAN" },
:twelvebytekey => { :name => "BYTE", :limit => 12}
}
end
# QUOTING ==================================================
# Quotes the column value to help prevent
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
def quote(value, column = nil)
return value.quoted_id if value.respond_to?(:quoted_id)
retvalue = "<INVALID>"
puts "quote(#{value.inspect}(#{value.class}),#{column.type.inspect})" if FB_TRACE
# If a column was passed in, use column type information
unless value.nil?
if column
retvalue = case column.type
when :string
if value.kind_of?(String)
"'#{quote_string(value.to_s)}'" # ' (for ruby-mode)
else
"'#{quote_string(value.to_yaml)}'"
end
when :integer
if value.kind_of?(TrueClass)
'1'
elsif value.kind_of?(FalseClass)
'0'
else
value.to_i.to_s
end
when :float
value.to_f.to_s
when :decimal
value.to_d.to_s("F")
when :datetime, :timestamp
"TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
when :time
"TIME '#{value.strftime("%H:%M:%S")}'"
when :date
"DATE '#{value.strftime("%Y-%m-%d")}'"
when :twelvebytekey
value = value.to_s.unpack("H*").first unless value.kind_of?(TwelveByteKey)
"X'#{value.to_s}'"
when :boolean
value = quoted_true if value.kind_of?(TrueClass)
value = quoted_false if value.kind_of?(FalseClass)
value
when :binary
blob_handle = @connection.create_blob(value.to_s)
puts "SQL -> Insert #{value.to_s.length} byte blob as #{retvalue}" if FB_TRACE
blob_handle.handle
when :text
if value.kind_of?(String)
clobdata = value.to_s # ' (for ruby-mode)
else
clobdata = value.to_yaml
end
clob_handle = @connection.create_clob(clobdata)
puts "SQL -> Insert #{value.to_s.length} byte clob as #{retvalue}" if FB_TRACE
clob_handle.handle
else
raise "*** UNKNOWN TYPE: #{column.type.inspect}"
end # case
# Since we don't have column type info, make a best guess based
# on the Ruby class of the value
else
retvalue = case value
when ActiveRecord::ConnectionAdapters::TwelveByteKey
s = value.unpack("H*").first
"X'#{s}'"
when String
if column && column.type == :binary
s = value.unpack("H*").first
"X'#{s}'"
elsif column && [:integer, :float, :decimal].include?(column.type)
value.to_s
else
"'#{quote_string(value)}'" # ' (for ruby-mode)
end
when NilClass
"NULL"
when TrueClass
(column && column.type == :integer ? '1' : quoted_true)
when FalseClass
(column && column.type == :integer ? '0' : quoted_false)
when Float, Fixnum, Bignum, BigDecimal
value.to_s
when Time, Date, DateTime
if column
case column.type
when :date
"DATE '#{value.strftime("%Y-%m-%d")}'"
when :time
"TIME '#{value.strftime("%H:%M:%S")}'"
when :timestamp
"TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'"
else
raise NotImplementedError, "Unknown column type!"
end # case
else # Column wasn't passed in, so try to guess the right type
if value.kind_of? Date
"DATE '#{value.strftime("%Y-%m-%d")}'"
else
if [:hour, :min, :sec].all? {|part| value.send(:part).zero? }
"TIME '#{value.strftime("%H:%M:%S")}'"
else
"TIMESTAMP '#{quoted_date(value)}'"
end
end
end #if column
else
"'#{quote_string(value.to_yaml)}'"
end #case
end
else
retvalue = "NULL"
end
retvalue
end # def
# Quotes a string, escaping any ' (single quote) characters.
def quote_string(s)
s.gsub(/'/, "''") # ' (for ruby-mode)
end
def quote_column_name(name) #:nodoc:
%( "#{name}" )
end
def quoted_true
"true"
end
def quoted_false
"false"
end
# CONNECTION MANAGEMENT ====================================
def active?
true if @connection.status == 1
rescue => e
false
end
def reconnect!
@connection.close rescue nil
@connection = FBSQL_Connect.connect(*@connection_options.first(7))
end
# Close this connection
def disconnect!
@connection.close rescue nil
@active = false
end
# DATABASE STATEMENTS ======================================
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select_all(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
return_value = []
fbresult = execute(sql, name)
puts "select_all SQL -> #{fbsql}" if FB_TRACE
columns = fbresult.columns
fbresult.each do |row|
puts "SQL <- #{row.inspect}" if FB_TRACE
hashed_row = {}
colnum = 0
row.each do |col|
hashed_row[columns[colnum]] = col
if col.kind_of?(FBSQL_LOB)
hashed_row[columns[colnum]] = col.read
end
colnum += 1
end
puts "raw row: #{hashed_row.inspect}" if FB_TRACE
return_value << hashed_row
end
return_value
end
def select_one(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
return_value = []
fbresult = execute(fbsql, name)
puts "SQL -> #{fbsql}" if FB_TRACE
columns = fbresult.columns
fbresult.each do |row|
puts "SQL <- #{row.inspect}" if FB_TRACE
hashed_row = {}
colnum = 0
row.each do |col|
hashed_row[columns[colnum]] = col
if col.kind_of?(FBSQL_LOB)
hashed_row[columns[colnum]] = col.read
end
colnum += 1
end
return_value << hashed_row
break
end
fbresult.clear
return_value.first
end
def query(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
puts "SQL(query) -> #{fbsql}" if FB_TRACE
log(fbsql, name) { @connection.query(fbsql) }
rescue => e
puts "FB Exception: #{e.inspect}" if FB_TRACE
raise e
end
def execute(sql, name = nil) #:nodoc:
fbsql = cleanup_fb_sql(sql)
puts "SQL(execute) -> #{fbsql}" if FB_TRACE
log(fbsql, name) { @connection.query(fbsql) }
rescue ActiveRecord::StatementInvalid => e
if e.message.scan(/Table name - \w* - exists/).empty?
puts "FB Exception: #{e.inspect}" if FB_TRACE
raise e
end
end
# Returns the last auto-generated ID from the affected table.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
puts "SQL -> #{sql.inspect}" if FB_TRACE
execute(sql, name)
id_value || pk
end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil) #:nodoc:
puts "SQL -> #{sql.inspect}" if FB_TRACE
execute(sql, name).num_rows
end
alias_method :delete, :update #:nodoc:
def set_pessimistic_transactions
if @transaction_mode == :optimistic
execute "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, LOCKING PESSIMISTIC, READ WRITE"
@transaction_mode = :pessimistic
end
end
def set_optimistic_transactions
if @transaction_mode == :pessimistic
execute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, LOCKING OPTIMISTIC"
@transaction_mode = :optimistic
end
end
def begin_db_transaction #:nodoc:
execute "SET COMMIT FALSE" rescue nil
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
ensure
execute "SET COMMIT TRUE"
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
ensure
execute "SET COMMIT TRUE"
end
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
offset = options[:offset] || 0
# Here is the full syntax FrontBase supports:
# (from gclem@frontbase.com)
#
# TOP <limit - unsigned integer>
# TOP ( <offset expr>, <limit expr>)
# "TOP 0" is not allowed, so we have
# to use a cheap trick.
if limit.zero?
case sql
when /WHERE/i
sql.sub!(/WHERE/i, 'WHERE 0 = 1 AND ')
when /ORDER\s+BY/i
sql.sub!(/ORDER\s+BY/i, 'WHERE 0 = 1 ORDER BY')
else
sql << 'WHERE 0 = 1'
end
else
if offset.zero?
sql.replace sql.gsub("SELECT ","SELECT TOP #{limit} ")
else
sql.replace sql.gsub("SELECT ","SELECT TOP(#{offset},#{limit}) ")
end
end
end
end
def prefetch_primary_key?(table_name = nil)
true
end
# 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)
unique = select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value")
# The test cases cannot handle a zero primary key
unique.zero? ? select_value("SELECT UNIQUE FROM #{sequence_name}","Next Sequence Value") : unique
end
def default_sequence_name(table, column)
table
end
# Set the sequence to the max value of the table's column.
def reset_sequence!(table, column, sequence = nil)
klasses = classes_for_table_name(table)
klass = klasses.nil? ? nil : klasses.first
pk = klass.primary_key unless klass.nil?
if pk && klass.columns_hash[pk].type == :integer
execute("SET UNIQUE FOR #{klass.table_name}(#{pk})")
end
end
def classes_for_table_name(table)
ActiveRecord::Base.send(:subclasses).select {|klass| klass.table_name == table}
end
def reset_pk_sequence!(table, pk = nil, sequence = nil)
klasses = classes_for_table_name(table)
klass = klasses.nil? ? nil : klasses.first
pk = klass.primary_key unless klass.nil?
if pk && klass.columns_hash[pk].type == :integer
mpk = select_value("SELECT MAX(#{pk}) FROM #{table}")
execute("SET UNIQUE FOR #{klass.table_name}(#{pk})")
end
end
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
select_all("SHOW TABLES").inject('') do |structure, table|
structure << select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] << ";\n\n"
end
end
def recreate_database(name) #:nodoc:
drop_database(name)
create_database(name)
end
def create_database(name) #:nodoc:
execute "CREATE DATABASE #{name}"
end
def drop_database(name) #:nodoc:
execute "DROP DATABASE #{name}"
end
def current_database
select_value('SELECT "CATALOG_NAME" FROM INFORMATION_SCHEMA.CATALOGS').downcase
end
def tables(name = nil) #:nodoc:
select_values(<<-SQL, nil)
SELECT "TABLE_NAME"
FROM INFORMATION_SCHEMA.TABLES AS T0,
INFORMATION_SCHEMA.SCHEMATA AS T1
WHERE T0.SCHEMA_PK = T1.SCHEMA_PK
AND "SCHEMA_NAME" = CURRENT_SCHEMA
SQL
end
def indexes(table_name, name = nil)#:nodoc:
indexes = []
current_index = nil
sql = <<-SQL
SELECT INDEX_NAME, T2.ORDINAL_POSITION, INDEX_COLUMN_COUNT, INDEX_TYPE,
"COLUMN_NAME", IS_NULLABLE
FROM INFORMATION_SCHEMA.TABLES AS T0,
INFORMATION_SCHEMA.INDEXES AS T1,
INFORMATION_SCHEMA.INDEX_COLUMN_USAGE AS T2,
INFORMATION_SCHEMA.COLUMNS AS T3
WHERE T0."TABLE_NAME" = '#{table_name}'
AND INDEX_TYPE <> 0
AND T0.TABLE_PK = T1.TABLE_PK
AND T0.TABLE_PK = T2.TABLE_PK
AND T0.TABLE_PK = T3.TABLE_PK
AND T1.INDEXES_PK = T2.INDEX_PK
AND T2.COLUMN_PK = T3.COLUMN_PK
ORDER BY INDEX_NAME, T2.ORDINAL_POSITION
SQL
columns = []
query(sql).each do |row|
index_name = row[0]
ord_position = row[1]
ndx_colcount = row[2]
index_type = row[3]
column_name = row[4]
is_unique = index_type == 1
columns << column_name
if ord_position == ndx_colcount
indexes << IndexDefinition.new(table_name, index_name, is_unique , columns)
columns = []
end
end
indexes
end
def columns(table_name, name = nil)#:nodoc:
sql = <<-SQL
SELECT "TABLE_NAME", "COLUMN_NAME", ORDINAL_POSITION, IS_NULLABLE, COLUMN_DEFAULT,
DATA_TYPE, DATA_TYPE_CODE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
NUMERIC_PRECISION_RADIX, NUMERIC_SCALE, DATETIME_PRECISION, DATETIME_PRECISION_LEADING
FROM INFORMATION_SCHEMA.TABLES T0,
INFORMATION_SCHEMA.COLUMNS T1,
INFORMATION_SCHEMA.DATA_TYPE_DESCRIPTOR T3
WHERE "TABLE_NAME" = '#{table_name}'
AND T0.TABLE_PK = T1.TABLE_PK
AND T0.TABLE_PK = T3.TABLE_OR_DOMAIN_PK
AND T1.COLUMN_PK = T3.COLUMN_NAME_PK
ORDER BY T1.ORDINAL_POSITION
SQL
rawresults = query(sql,name)
columns = []
rawresults.each do |field|
args = [base = field[0],
name = field[1],
typecode = field[6],
typestring = field[5],
limit = field[7],
precision = field[8],
scale = field[9],
default = field[4],
nullable = field[3]]
columns << FrontBaseColumn.new(*args)
end
columns
end
def create_table(name, options = {})
table_definition = TableDefinition.new(self)
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
yield table_definition
if options[:force]
drop_table(name) rescue nil
end
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
create_sql << "#{name} ("
create_sql << table_definition.to_sql
create_sql << ") #{options[:options]}"
begin_db_transaction
execute create_sql
commit_db_transaction
rescue ActiveRecord::StatementInvalid => e
raise e unless e.message.match(/Table name - \w* - exists/)
end
def rename_table(name, new_name)
columns = columns(name)
pkcol = columns.find {|c| c.fb_autogen}
execute "ALTER TABLE NAME #{name} TO #{new_name}"
if pkcol
change_column_default(new_name,pkcol.name,"UNIQUE")
begin_db_transaction
mpk = select_value("SELECT MAX(#{pkcol.name}) FROM #{new_name}")
mpk = 0 if mpk.nil?
execute "SET UNIQUE=#{mpk} FOR #{new_name}"
commit_db_transaction
end
end
# Drops a table from the database.
def drop_table(name, options = {})
execute "DROP TABLE #{name} RESTRICT"
rescue ActiveRecord::StatementInvalid => e
raise e unless e.message.match(/Referenced TABLE - \w* - does not exist/)
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 #{column_name} #{type_to_sql(type, options[:limit])}"
options[:type] = type
add_column_options!(add_column_sql, options)
execute(add_column_sql)
end
def add_column_options!(sql, options) #:nodoc:
default_value = quote(options[:default], options[:column])
if options_include_default?(options)
if options[:type] == :boolean
default_value = options[:default] == 0 ? quoted_false : quoted_true
end
sql << " DEFAULT #{default_value}"
end
sql << " NOT NULL" if options[:null] == false
end
# Removes the column from the table definition.
# ===== Examples
# remove_column(:suppliers, :qualification)
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{column_name} RESTRICT"
end
def remove_index(table_name, options = {}) #:nodoc:
if options[:unique]
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{quote_column_name(index_name(table_name, options))} RESTRICT"
else
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
end
end
def change_column_default(table_name, column_name, default) #:nodoc:
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{default}" if default != "NULL"
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
change_column_sql = %( ALTER COLUMN "#{table_name}"."#{column_name}" TO #{type_to_sql(type, options[:limit])} )
execute(change_column_sql)
change_column_sql = %( ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" )
if options_include_default?(options)
default_value = quote(options[:default], options[:column])
if type == :boolean
default_value = options[:default] == 0 ? quoted_false : quoted_true
end
change_column_sql << " SET DEFAULT #{default_value}"
end
execute(change_column_sql)
# change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
# add_column_options!(change_column_sql, options)
# execute(change_column_sql)
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute %( ALTER COLUMN NAME "#{table_name}"."#{column_name}" TO "#{new_column_name}" )
end
private
# Clean up sql to make it something FrontBase can digest
def cleanup_fb_sql(sql) #:nodoc:
# Turn non-standard != into standard <>
cleansql = sql.gsub("!=", "<>")
# Strip blank lines and comments
cleansql.split("\n").reject { |line| line.match(/^(?:\s*|--.*)$/) } * "\n"
end
end
end
end

View file

@ -33,7 +33,8 @@ module MysqlCompat #:nodoc:
end_eval
end
unless target.instance_methods.include?('all_hashes')
unless target.instance_methods.include?('all_hashes') ||
target.instance_methods.include?(:all_hashes)
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
end
end
@ -49,6 +50,11 @@ module ActiveRecord
rescue LoadError => cannot_require_mysql
# Use the bundled Ruby/MySQL driver if no driver is already in place
begin
ActiveRecord::Base.logger.info(
"WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. " +
"Please install the C-based MySQL library instead (gem install mysql)."
) if ActiveRecord::Base.logger
require 'active_record/vendor/mysql'
rescue LoadError
raise cannot_require_mysql
@ -85,12 +91,18 @@ module ActiveRecord
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?
def extract_default(default)
if type == :binary || type == :text
if default.blank?
default
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
elsif missing_default_forged_as_empty_string?(default)
nil
else
super
end
end
private
@ -102,13 +114,13 @@ module ActiveRecord
# 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).
# default (string) 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)
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
end
@ -123,6 +135,7 @@ module ActiveRecord
# * <tt>:username</tt> -- Defaults to root
# * <tt>:password</tt> -- Defaults to nothing
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
# * <tt>:encoding</tt> -- (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection
# * <tt>:sslkey</tt> -- Necessary to use MySQL with an SSL connection
# * <tt>:sslcert</tt> -- Necessary to use MySQL with an SSL connection
# * <tt>:sslcapath</tt> -- Necessary to use MySQL with an SSL connection
@ -195,6 +208,10 @@ module ActiveRecord
"`#{name}`"
end
def quote_table_name(name) #:nodoc:
quote_column_name(name).gsub('.', '`.`')
end
def quote_string(string) #:nodoc:
@connection.quote(string)
end
@ -202,11 +219,23 @@ module ActiveRecord
def quoted_true
"1"
end
def quoted_false
"0"
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity(&block) #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
yield
ensure
update("SET FOREIGN_KEY_CHECKS = #{old}")
end
end
# CONNECTION MANAGEMENT ====================================
@ -231,7 +260,7 @@ module ActiveRecord
disconnect!
connect
end
def disconnect!
@connection.close rescue nil
end
@ -239,6 +268,15 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
def select_rows(sql, name = nil)
@connection.query_with_result = true
result = execute(sql, name)
rows = []
result.each { |row| rows << row }
result.free
rows
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.query(sql) }
rescue ActiveRecord::StatementInvalid => exception
@ -249,13 +287,13 @@ module ActiveRecord
end
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name = nil)
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
super sql, name
id_value || @connection.insert_id
end
def update(sql, name = nil) #:nodoc:
execute(sql, name)
def update_sql(sql, name = nil) #:nodoc:
super
@connection.affected_rows
end
@ -297,10 +335,10 @@ module ActiveRecord
else
sql = "SHOW TABLES"
end
select_all(sql).inject("") do |structure, table|
table.delete('Table_type')
structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
end
end
@ -309,16 +347,37 @@ module ActiveRecord
create_database(name)
end
def create_database(name) #:nodoc:
execute "CREATE DATABASE `#{name}`"
# Create a new MySQL database with optional :charset and :collation.
# Charset defaults to utf8.
#
# Example:
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
# create_database 'matt_development'
# create_database 'matt_development', :charset => :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
else
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
end
end
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS `#{name}`"
end
def current_database
select_one("SELECT DATABASE() as db")["db"]
select_value 'SELECT DATABASE() as db'
end
# Returns the database character set.
def charset
show_variable 'character_set_database'
end
# Returns the database collation strategy.
def collation
show_variable 'collation_database'
end
def tables(name = nil) #:nodoc:
@ -327,10 +386,14 @@ module ActiveRecord
tables
end
def drop_table(table_name, options = {})
super(table_name, options)
end
def indexes(table_name, name = nil)#:nodoc:
indexes = []
current_index = nil
execute("SHOW KEYS FROM #{table_name}", name).each do |row|
execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name).each do |row|
if current_index != row[2]
next if row[2] == "PRIMARY" # skip the primary key
current_index = row[2]
@ -343,42 +406,61 @@ module ActiveRecord
end
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{table_name}"
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = []
execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
columns
end
def create_table(name, options = {}) #:nodoc:
super(name, {:options => "ENGINE=InnoDB"}.merge(options))
def create_table(table_name, options = {}) #:nodoc:
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
def rename_table(name, new_name)
execute "RENAME TABLE #{name} TO #{new_name}"
end
def change_column_default(table_name, column_name, default) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}")
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
unless options_include_default?(options)
options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
else
raise "No such column: #{table_name}.#{column_name}"
end
end
change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_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
def rename_column(table_name, column_name, new_column_name) #:nodoc:
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
variables.first['Value'] unless variables.empty?
end
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table) #:nodoc:
keys = []
execute("describe #{quote_table_name(table)}").each_hash do |h|
keys << h["Field"]if h["Key"] == "PRI"
end
keys.length == 1 ? [keys.first, nil] : nil
end
private
def connect
encoding = @config[:encoding]

View file

@ -1,350 +0,0 @@
require 'active_record/connection_adapters/abstract_adapter'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.openbase_connection(config) # :nodoc:
require_library_or_gem 'openbase' unless self.class.const_defined?(:OpenBase)
config = config.symbolize_keys
host = config[:host]
username = config[:username].to_s
password = config[:password].to_s
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
oba = ConnectionAdapters::OpenBaseAdapter.new(
OpenBase.new(database, host, username, password), logger
)
oba
end
end
module ConnectionAdapters
class OpenBaseColumn < Column #:nodoc:
private
def simplified_type(field_type)
return :integer if field_type.downcase =~ /long/
return :decimal if field_type.downcase == "money"
return :binary if field_type.downcase == "object"
super
end
end
# The OpenBase adapter works with the Ruby/Openbase driver by Tetsuya Suzuki.
# http://www.spice-of-life.net/ruby-openbase/ (needs version 0.7.3+)
#
# Options:
#
# * <tt>:host</tt> -- Defaults to localhost
# * <tt>:username</tt> -- Defaults to nothing
# * <tt>:password</tt> -- Defaults to nothing
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
#
# The OpenBase adapter will make use of OpenBase's ability to generate unique ids
# for any column with an unique index applied. Thus, if the value of a primary
# key is not specified at the time an INSERT is performed, the adapter will prefetch
# a unique id for the primary key. This prefetching is also necessary in order
# to return the id after an insert.
#
# Caveat: Operations involving LIMIT and OFFSET do not yet work!
#
# Maintainer: derrick.spell@gmail.com
class OpenBaseAdapter < AbstractAdapter
def adapter_name
'OpenBase'
end
def native_database_types
{
:primary_key => "integer UNIQUE INDEX DEFAULT _rowid",
:string => { :name => "char", :limit => 4096 },
:text => { :name => "text" },
:integer => { :name => "integer" },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "object" },
:boolean => { :name => "boolean" }
}
end
def supports_migrations?
false
end
def prefetch_primary_key?(table_name = nil)
true
end
def default_sequence_name(table_name, primary_key) # :nodoc:
"#{table_name} #{primary_key}"
end
def next_sequence_value(sequence_name)
ary = sequence_name.split(' ')
if (!ary[1]) then
ary[0] =~ /(\w+)_nonstd_seq/
ary[0] = $1
end
@connection.unique_row_id(ary[0], ary[1])
end
# QUOTING ==================================================
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary
"'#{@connection.insert_binary(value)}'"
else
super
end
end
def quoted_true
"1"
end
def quoted_false
"0"
end
# DATABASE STATEMENTS ======================================
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]
unless offset = options[:offset]
sql << " RETURN RESULTS #{limit}"
else
limit = limit + offset
sql << " RETURN RESULTS #{offset} TO #{limit}"
end
end
end
def select_all(sql, name = nil) #:nodoc:
select(sql, name)
end
def select_one(sql, name = nil) #:nodoc:
add_limit_offset!(sql,{:limit => 1})
results = select(sql, name)
results.first if results
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name)
update_nulls_after_insert(sql, name, pk, id_value, sequence_name)
id_value
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.execute(sql) }
end
def update(sql, name = nil) #:nodoc:
execute(sql, name).rows_affected
end
alias_method :delete, :update #:nodoc:
#=begin
def begin_db_transaction #:nodoc:
execute "START TRANSACTION"
rescue Exception
# Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue Exception
# Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
rescue Exception
# Transactions aren't supported
end
#=end
# SCHEMA STATEMENTS ========================================
# Return the list of all tables in the schema search path.
def tables(name = nil) #:nodoc:
tables = @connection.tables
tables.reject! { |t| /\A_SYS_/ === t }
tables
end
def columns(table_name, name = nil) #:nodoc:
sql = "SELECT * FROM _sys_tables "
sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
sql << "ORDER BY columnNumber"
columns = []
select_all(sql, name).each do |row|
columns << OpenBaseColumn.new(row["fieldname"],
default_value(row["defaultvalue"]),
sql_type_name(row["typename"],row["length"]),
row["notnull"]
)
# breakpoint() if row["fieldname"] == "content"
end
columns
end
def indexes(table_name, name = nil)#:nodoc:
sql = "SELECT fieldname, notnull, searchindex, uniqueindex, clusteredindex FROM _sys_tables "
sql << "WHERE tablename='#{table_name}' AND INDEXOF(fieldname,'_')<>0 "
sql << "AND primarykey=0 "
sql << "AND (searchindex=1 OR uniqueindex=1 OR clusteredindex=1) "
sql << "ORDER BY columnNumber"
indexes = []
execute(sql, name).each do |row|
indexes << IndexDefinition.new(table_name,index_name(row),row[3]==1,[row[0]])
end
indexes
end
private
def select(sql, name = nil)
sql = translate_sql(sql)
results = execute(sql, name)
date_cols = []
col_names = []
results.column_infos.each do |info|
col_names << info.name
date_cols << info.name if info.type == "date"
end
rows = []
if ( results.rows_affected )
results.each do |row| # loop through result rows
hashed_row = {}
row.each_index do |index|
hashed_row["#{col_names[index]}"] = row[index] unless col_names[index] == "_rowid"
end
date_cols.each do |name|
unless hashed_row["#{name}"].nil? or hashed_row["#{name}"].empty?
hashed_row["#{name}"] = Date.parse(hashed_row["#{name}"],false).to_s
end
end
rows << hashed_row
end
end
rows
end
def default_value(value)
# Boolean type values
return true if value =~ /true/
return false if value =~ /false/
# Date / Time magic values
return Time.now.to_s if value =~ /^now\(\)/i
# Empty strings should be set to null
return nil if value.empty?
# Otherwise return what we got from OpenBase
# and hope for the best...
return value
end
def sql_type_name(type_name, length)
return "#{type_name}(#{length})" if ( type_name =~ /char/ )
type_name
end
def index_name(row = [])
name = ""
name << "UNIQUE " if row[3]
name << "CLUSTERED " if row[4]
name << "INDEX"
name
end
def translate_sql(sql)
# Change table.* to list of columns in table
while (sql =~ /SELECT.*\s(\w+)\.\*/)
table = $1
cols = columns(table)
if ( cols.size == 0 ) then
# Maybe this is a table alias
sql =~ /FROM(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/
$1 =~ /[\s|,](\w+)\s+#{table}[\s|,]/ # get the tablename for this alias
cols = columns($1)
end
select_columns = []
cols.each do |col|
select_columns << table + '.' + col.name
end
sql.gsub!(table + '.*',select_columns.join(", ")) if select_columns
end
# Change JOIN clause to table list and WHERE condition
while (sql =~ /JOIN/)
sql =~ /((LEFT )?(OUTER )?JOIN (\w+) ON )(.+?)(?:LEFT|OUTER|JOIN|WHERE|GROUP|HAVING|ORDER|RETURN|$)/
join_clause = $1 + $5
is_outer_join = $3
join_table = $4
join_condition = $5
join_condition.gsub!(/=/,"*") if is_outer_join
if (sql =~ /WHERE/)
sql.gsub!(/WHERE/,"WHERE (#{join_condition}) AND")
else
sql.gsub!(join_clause,"#{join_clause} WHERE #{join_condition}")
end
sql =~ /(FROM .+?)(?:LEFT|OUTER|JOIN|WHERE|$)/
from_clause = $1
sql.gsub!(from_clause,"#{from_clause}, #{join_table} ")
sql.gsub!(join_clause,"")
end
# ORDER BY _rowid if no explicit ORDER BY
# This will ensure that find(:first) returns the first inserted row
if (sql !~ /(ORDER BY)|(GROUP BY)/)
if (sql =~ /RETURN RESULTS/)
sql.sub!(/RETURN RESULTS/,"ORDER BY _rowid RETURN RESULTS")
else
sql << " ORDER BY _rowid"
end
end
sql
end
def update_nulls_after_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
sql =~ /INSERT INTO (\w+) \((.*)\) VALUES\s*\((.*)\)/m
table = $1
cols = $2
values = $3
cols = cols.split(',')
values.gsub!(/'[^']*'/,"''")
values.gsub!(/"[^"]*"/,"\"\"")
values = values.split(',')
update_cols = []
values.each_index { |index| update_cols << cols[index] if values[index] =~ /\s*NULL\s*/ }
update_sql = "UPDATE #{table} SET"
update_cols.each { |col| update_sql << " #{col}=NULL," unless col.empty? }
update_sql.chop!()
update_sql << " WHERE #{pk}=#{quote(id_value)}"
execute(update_sql, name + " NULL Correction") if update_cols.size > 0
end
end
end
end

View file

@ -1,690 +0,0 @@
# oracle_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g
#
# Original author: Graham Jenkins
#
# Current maintainer: Michael Schoen <schoenm@earthlink.net>
#
#########################################################################
#
# Implementation notes:
# 1. Redefines (safely) a method in ActiveRecord to make it possible to
# implement an autonumbering solution for Oracle.
# 2. The OCI8 driver is patched to properly handle values for LONG and
# TIMESTAMP columns. The driver-author has indicated that a future
# release of the driver will obviate this patch.
# 3. LOB support is implemented through an after_save callback.
# 4. Oracle does not offer native LIMIT and OFFSET options; this
# functionality is mimiced through the use of nested selects.
# See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
#
# Do what you want with this code, at your own peril, but if any
# significant portion of my code remains then please acknowledge my
# contribution.
# portions Copyright 2005 Graham Jenkins
require 'active_record/connection_adapters/abstract_adapter'
require 'delegate'
begin
require_library_or_gem 'oci8' unless self.class.const_defined? :OCI8
module ActiveRecord
class Base
def self.oracle_connection(config) #:nodoc:
# Use OCI8AutoRecover instead of normal OCI8 driver.
ConnectionAdapters::OracleAdapter.new OCI8AutoRecover.new(config), logger
end
# for backwards-compatibility
def self.oci_connection(config) #:nodoc:
config[:database] = config[:host]
self.oracle_connection(config)
end
# After setting large objects to empty, select the OCI8::LOB
# and write back the data.
after_save :write_lobs
def write_lobs() #:nodoc:
if connection.is_a?(ConnectionAdapters::OracleAdapter)
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_value(id)}",
'Writable Large Object')[c.name]
lob.write value
}
end
end
private :write_lobs
end
module ConnectionAdapters #:nodoc:
class OracleColumn < Column #:nodoc:
def type_cast(value)
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 /date|time/i then :datetime
else super
end
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
end
end
# This is an Oracle/OCI adapter for the ActiveRecord persistence
# framework. It relies upon the OCI8 driver, which works with Oracle 8i
# and above. Most recent development has been on Debian Linux against
# a 10g database, ActiveRecord 1.12.1 and OCI8 0.1.13.
# See: http://rubyforge.org/projects/ruby-oci8/
#
# Usage notes:
# * Key generation assumes a "${table_name}_seq" sequence is available
# for all tables; the sequence name can be changed using
# ActiveRecord::Base.set_sequence_name. When using Migrations, these
# sequences are created automatically.
# * Oracle uses DATE or TIMESTAMP datatypes for both dates and times.
# Consequently some hacks are employed to map data back to Date or Time
# in Ruby. If the column_name ends in _time it's created as a Ruby Time.
# Else if the hours/minutes/seconds are 0, I make it a Ruby Date. Else
# it's a Ruby Time. This is a bit nasty - but if you use Duck Typing
# you'll probably not care very much. In 9i and up it's tempting to
# map DATE to Date and TIMESTAMP to Time, but too many databases use
# DATE for both. Timezones and sub-second precision on timestamps are
# not supported.
# * Default values that are functions (such as "SYSDATE") are not
# supported. This is a restriction of the way ActiveRecord supports
# default values.
# * Support for Oracle8 is limited by Rails' use of ANSI join syntax, which
# is supported in Oracle9i and later. You will need to use #finder_sql for
# has_and_belongs_to_many associations to run against Oracle8.
#
# Required parameters:
#
# * <tt>:username</tt>
# * <tt>:password</tt>
# * <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
def supports_migrations? #:nodoc:
true
end
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" },
:date => { :name => "DATE" },
:binary => { :name => "BLOB" },
:boolean => { :name => "NUMBER", :limit => 1 }
}
end
def table_alias_length
30
end
# QUOTING ==================================================
#
# see: abstract/quoting.rb
# camelCase column names need to be quoted; not that anyone using Oracle
# would really do this, but handling this case means we pass the test...
def quote_column_name(name) #:nodoc:
name =~ /[A-Z]/ ? "\"#{name}\"" : name
end
def quote_string(s) #:nodoc:
s.gsub(/'/, "''")
end
def quote(value, column = nil) #:nodoc:
if column && [:text, :binary].include?(column.type)
%Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
else
super
end
end
def quoted_true
"1"
end
def quoted_false
"0"
end
# CONNECTION MANAGEMENT ====================================
#
# 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
# last known state, which isn't good enough if the connection has
# gone stale since the last use.
@connection.ping
rescue OCIException
false
end
# Reconnects to the database.
def reconnect!
@connection.reset!
rescue OCIException => e
@logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}"
end
# Disconnects from the database.
def disconnect!
@connection.logoff rescue nil
@connection.active = false
end
# DATABASE STATEMENTS ======================================
#
# see: abstract/database_statements.rb
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.exec sql }
end
# 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
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
end
def commit_db_transaction #:nodoc:
@connection.commit
ensure
@connection.autocommit = true
end
def rollback_db_transaction #:nodoc:
@connection.rollback
ensure
@connection.autocommit = true
end
def add_limit_offset!(sql, options) #:nodoc:
offset = options[:offset] || 0
if limit = options[:limit]
sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
elsif offset > 0
sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
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
# SCHEMA STATEMENTS ========================================
#
# see: abstract/schema_statements.rb
def current_database #:nodoc:
select_one("select sys_context('userenv','db_name') db from dual")["db"]
end
def tables(name = nil) #:nodoc:
select_all("select lower(table_name) from user_tables").inject([]) do | tabs, t |
tabs << t.to_a.first.last
end
end
def indexes(table_name, name = nil) #:nodoc:
result = select_all(<<-SQL, name)
SELECT lower(i.index_name) as index_name, i.uniqueness, lower(c.column_name) as column_name
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 uc.index_name FROM user_constraints uc WHERE uc.constraint_type = 'P')
ORDER BY i.index_name, c.column_position
SQL
current_index = nil
indexes = []
result.each do |row|
if current_index != row['index_name']
indexes << IndexDefinition.new(table_name, row['index_name'], row['uniqueness'] == "UNIQUE", [])
current_index = row['index_name']
end
indexes.last.columns << row['column_name']
end
indexes
end
def columns(table_name, name = nil) #:nodoc:
(owner, table_name) = @connection.describe(table_name)
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,
'CHAR', data_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['name']),
row['data_default'],
row['sql_type'],
row['nullable'] == 'Y')
end
end
def create_table(name, options = {}) #:nodoc:
super(name, options)
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
def drop_table(name, options = {}) #:nodoc:
super(name)
seq_name = options[:sequence_name] || "#{name}_seq"
execute "DROP SEQUENCE #{seq_name}" rescue nil
end
def remove_index(table_name, options = {}) #:nodoc:
execute "DROP INDEX #{index_name(table_name, options)}"
end
def change_column_default(table_name, column_name, default) #:nodoc:
execute "ALTER TABLE #{table_name} MODIFY #{column_name} DEFAULT #{quote(default)}"
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], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
execute(change_column_sql)
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}"
end
def remove_column(table_name, column_name) #:nodoc:
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 "
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}"
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})"
end
col << " default #{row['data_default']}" if !row['data_default'].nil?
col << ' not null' if row['nullable'] == 'N'
col
end
ddl << cols.join(",\n ")
ddl << ");\n\n"
structure << ddl
end
end
def structure_drop #:nodoc:
s = select_all("select sequence_name from user_sequences").inject("") do |drop, seq|
drop << "drop sequence #{seq.to_a.first.last};\n\n"
end
select_all("select table_name from user_tables").inject(s) do |drop, table|
drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
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
def select(sql, name = nil)
cursor = execute(sql, name)
cols = cursor.get_col_names.map { |x| oracle_downcase(x) }
rows = []
while row = cursor.fetch
hash = Hash.new
cols.each_with_index do |col, i|
hash[col] =
case row[i]
when OCI8::LOB
name == 'Writable Large Object' ? row[i]: row[i].read
when OraDate
(row[i].hour == 0 and row[i].minute == 0 and row[i].second == 0) ?
row[i].to_date : row[i].to_time
else row[i]
end unless col == 'raw_rnum_'
end
rows << hash
end
rows
ensure
cursor.close if cursor
end
# Oracle column names by default are case-insensitive, but treated as upcase;
# for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
# their column names when creating Oracle tables, which makes then case-sensitive.
# I don't know anybody who does this, but we'll handle the theoretical case of a
# camelCase column name. I imagine other dbs handle this different, since there's a
# unit test that's currently failing test_oci.
def oracle_downcase(column_name)
column_name =~ /[a-z]/ ? column_name : column_name.downcase
end
end
end
end
class OCI8 #:nodoc:
# This OCI8 patch may not longer be required with the upcoming
# release of version 0.2.
class Cursor #:nodoc:
alias :define_a_column_pre_ar :define_a_column
def define_a_column(i)
case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) }
when 8 : @stmt.defineByPos(i, String, 65535) # Read LONG values
when 187 : @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values
when 108
if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE'
@stmt.defineByPos(i, String, 65535)
else
raise 'unsupported datatype'
end
else define_a_column_pre_ar i
end
end
end
# missing constant from oci8 < 0.1.14
OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK)
# Uses the describeAny OCI call to find the target owner and table_name
# indicated by +name+, parsing through synonynms as necessary. Returns
# an array of [owner, table_name].
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) rescue raise %Q{"DESC #{name}" failed; does it exist?}
info = @desc.attrGet(OCI_ATTR_PARAM)
case info.attrGet(OCI_ATTR_PTYPE)
when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW
owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA)
table_name = info.attrGet(OCI_ATTR_OBJ_NAME)
[owner, table_name]
when OCI_PTYPE_SYN
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
end
# The OracleConnectionFactory factors out the code necessary to connect and
# configure an Oracle/OCI connection.
class OracleConnectionFactory #:nodoc:
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
# 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
# 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
# connection is marked as dead, to be reconnected on it's next use.
class OCI8AutoRecover < DelegateClass(OCI8) #:nodoc:
attr_accessor :active
alias :active? :active
cattr_accessor :auto_retry
class << self
alias :auto_retry? :auto_retry
end
@@auto_retry = false
def initialize(config, factory = OracleConnectionFactory.new)
@active = true
@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, @async, @prefetch_rows, @cursor_sharing
super @connection
end
# Checks connection, returns true if active. Note that ping actively
# checks the connection, while #active? simply returns the last
# known state.
def ping
@connection.exec("select 1 from dual") { |r| nil }
@active = true
rescue
@active = false
raise
end
# Resets connection, by logging off and creating a new connection.
def reset!
logoff rescue nil
begin
@connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
__setobj__ @connection
@active = true
rescue
@active = false
raise
end
end
# ORA-00028: your session has been killed
# 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 ]
# Adds auto-recovery functionality.
#
# See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
def exec(sql, *bindvars, &block)
should_retry = self.class.auto_retry? && autocommit?
begin
@connection.exec(sql, *bindvars, &block)
rescue OCIException => e
raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
@active = false
raise unless should_retry
should_retry = false
reset! rescue nil
retry
end
end
end
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_error_message
end
def self.oci_connection(config) # :nodoc:
# Set up a reasonable error message
raise LoadError, @@oracle_error_message
end
end
end
end

View file

@ -12,29 +12,205 @@ module ActiveRecord
username = config[:username].to_s
password = config[:password].to_s
min_messages = config[:min_messages]
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
pga = ConnectionAdapters::PostgreSQLAdapter.new(
PGconn.connect(host, port, "", "", database, username, password), logger, config
)
PGconn.translate_results = false if PGconn.respond_to? :translate_results=
pga.schema_search_path = config[:schema_search_path] || config[:schema_order]
pga
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
end
end
module ConnectionAdapters
# The PostgreSQL adapter works both with the C-based (http://www.postgresql.jp/interfaces/ruby/) and the Ruby-base
# (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1145) drivers.
# PostgreSQL-specific extensions to column definitions in a table.
class PostgreSQLColumn < Column #:nodoc:
# Instantiates a new PostgreSQL column definition in a table.
def initialize(name, default, sql_type = nil, null = true)
super(name, self.class.extract_value_from_default(default), sql_type, null)
end
private
# Extracts the scale from PostgreSQL-specific data types.
def extract_scale(sql_type)
# Money type has a fixed scale of 2.
sql_type =~ /^money/ ? 2 : super
end
# Extracts the precision from PostgreSQL-specific data types.
def extract_precision(sql_type)
# Actual code is defined dynamically in PostgreSQLAdapter.connect
# depending on the server specifics
super
end
# Escapes binary strings for bytea input to the database.
def self.string_to_binary(value)
if PGconn.respond_to?(:escape_bytea)
self.class.module_eval do
define_method(:string_to_binary) do |value|
PGconn.escape_bytea(value) if value
end
end
else
self.class.module_eval do
define_method(:string_to_binary) do |value|
if value
result = ''
value.each_byte { |c| result << sprintf('\\\\%03o', c) }
result
end
end
end
end
self.class.string_to_binary(value)
end
# Unescapes bytea output from a database to the binary string it represents.
def self.binary_to_string(value)
# In each case, check if the value actually is escaped PostgreSQL bytea output
# or an unescaped Active Record attribute that was just written.
if PGconn.respond_to?(:unescape_bytea)
self.class.module_eval do
define_method(:binary_to_string) do |value|
if value =~ /\\\d{3}/
PGconn.unescape_bytea(value)
else
value
end
end
end
else
self.class.module_eval do
define_method(:binary_to_string) do |value|
if value =~ /\\\d{3}/
result = ''
i, max = 0, value.size
while i < max
char = value[i]
if char == ?\\
if value[i+1] == ?\\
char = ?\\
i += 1
else
char = value[i+1..i+3].oct
i += 3
end
end
result << char
i += 1
end
result
else
value
end
end
end
end
self.class.binary_to_string(value)
end
# Maps PostgreSQL-specific data types to logical Rails types.
def simplified_type(field_type)
case field_type
# Numeric and monetary types
when /^(?:real|double precision)$/
:float
# Monetary types
when /^money$/
:decimal
# Character types
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
:string
# Binary data types
when /^bytea$/
:binary
# Date/time types
when /^timestamp with(?:out)? time zone$/
:datetime
when /^interval$/
:string
# Geometric types
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
:string
# Network address types
when /^(?:cidr|inet|macaddr)$/
:string
# Bit strings
when /^bit(?: varying)?(?:\(\d+\))?$/
:string
# XML type
when /^xml$/
:string
# Arrays
when /^\D+\[\]$/
:string
# Object identifier types
when /^oid$/
:integer
# Pass through all types that are not specific to PostgreSQL.
else
super
end
end
# Extracts the value from a PostgreSQL column default definition.
def self.extract_value_from_default(default)
case default
# Numeric types
when /\A-?\d+(\.\d*)?\z/
default
# Character types
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
$1
# Character types (8.1 formatting)
when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m
$1.gsub(/\\(\d\d\d)/) { $1.oct.chr }
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
# Date/time types
when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
$1
when /\A'(.*)'::interval\z/
$1
# Boolean type
when 'true'
true
when 'false'
false
# Geometric types
when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
$1
# Network address types
when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
$1
# Bit string types
when /\AB'(.*)'::"?bit(?: varying)?"?\z/
$1
# XML type
when /\A'(.*)'::xml\z/m
$1
# Arrays
when /\A'(.*)'::"?\D+"?\[\]\z/
$1
# Object identifier types
when /\A-?\d+\z/
$1
else
# Anything else is blank, some user type, or some function
# and we can't know the value of that, so return nil.
nil
end
end
end
end
module ConnectionAdapters
# The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
# Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
#
# Options:
#
@ -44,19 +220,21 @@ module ActiveRecord
# * <tt>:password</tt> -- Defaults to nothing
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
# * <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>:encoding</tt> -- An optional client encoding that is used in a SET client_encoding TO <encoding> call on the connection.
# * <tt>:min_messages</tt> -- An optional client min messages that is used in a SET client_min_messages TO <min_messages> call on the 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
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
'PostgreSQL'
end
def initialize(connection, logger, config = {})
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
@config = config
@async = config[:allow_concurrency]
configure_connection
@connection_parameters, @config = connection_parameters, config
connect
end
# Is this connection alive and ready for queries?
@ -64,29 +242,32 @@ module ActiveRecord
if @connection.respond_to?(:status)
@connection.status == PGconn::CONNECTION_OK
else
# We're asking the driver, not ActiveRecord, so use @connection.query instead of #query
@connection.query 'SELECT 1'
true
end
# postgres-pr raises a NoMethodError when querying if no conn is available
# postgres-pr raises a NoMethodError when querying if no connection is available.
rescue PGError, NoMethodError
false
end
# Close then reopen the connection.
def reconnect!
# TODO: postgres-pr doesn't have PGconn#reset.
if @connection.respond_to?(:reset)
@connection.reset
configure_connection
else
disconnect!
connect
end
end
# Close the connection.
def disconnect!
# Both postgres and postgres-pr respond to :close
@connection.close rescue nil
end
def native_database_types
def native_database_types #:nodoc:
{
:primary_key => "serial primary key",
:string => { :name => "character varying", :limit => 255 },
@ -103,41 +284,113 @@ module ActiveRecord
}
end
# Does PostgreSQL support migrations?
def supports_migrations?
true
end
# Does PostgreSQL support standard conforming strings?
def supports_standard_conforming_strings?
# Temporarily set the client message level above error to prevent unintentional
# error messages in the logs when working on a PostgreSQL database server that
# does not support standard conforming strings.
client_min_messages_old = client_min_messages
self.client_min_messages = 'panic'
# postgres-pr does not raise an exception when client_min_messages is set higher
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
# PGresult instead.
has_support = execute('SHOW standard_conforming_strings')[0][0] rescue false
self.client_min_messages = client_min_messages_old
has_support
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
63
@table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63)
end
# QUOTING ==================================================
def quote(value, column = nil)
# Quotes PostgreSQL-specific data types for SQL input.
def quote(value, column = nil) #:nodoc:
if value.kind_of?(String) && column && column.type == :binary
"'#{escape_bytea(value)}'"
"#{quoted_string_prefix}'#{column.class.string_to_binary(value)}'"
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
"xml '#{quote_string(value)}'"
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
# Not truly string input, so doesn't require (or allow) escape string syntax.
"'#{value.to_s}'"
elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/
case value
when /^[01]*$/
"B'#{value}'" # Bit-string notation
when /^[0-9A-F]*$/i
"X'#{value}'" # Hexadecimal notation
end
else
super
end
end
def quote_column_name(name)
# Quotes strings for use in SQL input in the postgres driver for better performance.
def quote_string(s) #:nodoc:
if PGconn.respond_to?(:escape)
self.class.instance_eval do
define_method(:quote_string) do |s|
PGconn.escape(s)
end
end
else
# There are some incorrectly compiled postgres drivers out there
# that don't define PGconn.escape.
self.class.instance_eval do
undef_method(:quote_string)
end
end
quote_string(s)
end
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
%("#{name}")
end
def quoted_date(value)
value.strftime("%Y-%m-%d %H:%M:%S.#{sprintf("%06d", value.usec)}")
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
if value.acts_like?(:time) && value.respond_to?(:usec)
"#{super}.#{sprintf("%06d", value.usec)}"
else
super
end
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity(&block) #:nodoc:
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
yield
ensure
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
end
# DATABASE STATEMENTS ======================================
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name)
table = sql.split(" ", 4)[2]
id_value || last_insert_id(table, sequence_name || default_sequence_name(table, pk))
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
def select_rows(sql, name = nil)
select_raw(sql, name).last
end
# Executes an INSERT query and returns the new record's ID
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
table = sql.split(" ", 4)[2]
super || last_insert_id(table, sequence_name || default_sequence_name(table, pk))
end
# Queries the database and returns the results in an Array or nil otherwise.
def query(sql, name = nil) #:nodoc:
log(sql, name) do
if @async
@ -148,7 +401,9 @@ module ActiveRecord
end
end
def execute(sql, name = nil) #:nodoc:
# Executes an SQL statement, returning a PGresult object on success
# or raising a PGError exception otherwise.
def execute(sql, name = nil)
log(sql, name) do
if @async
@connection.async_exec(sql)
@ -158,26 +413,30 @@ module ActiveRecord
end
end
def update(sql, name = nil) #:nodoc:
execute(sql, name).cmdtuples
# Executes an UPDATE query and returns the number of affected tuples.
def update_sql(sql, name = nil)
super.cmdtuples
end
def begin_db_transaction #:nodoc:
# Begins a transaction.
def begin_db_transaction
execute "BEGIN"
end
def commit_db_transaction #:nodoc:
# Commits a transaction.
def commit_db_transaction
execute "COMMIT"
end
def rollback_db_transaction #:nodoc:
# Aborts a transaction.
def rollback_db_transaction
execute "ROLLBACK"
end
# SCHEMA STATEMENTS ========================================
# Return the list of all tables in the schema search path.
def tables(name = nil) #:nodoc:
# Returns the list of all tables in the schema search path or a specified schema.
def tables(name = nil)
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
query(<<-SQL, name).map { |row| row[0] }
SELECT tablename
@ -186,7 +445,8 @@ module ActiveRecord
SQL
end
def indexes(table_name, name = nil) #:nodoc:
# Returns the list of all indexes for a table.
def indexes(table_name, name = nil)
result = query(<<-SQL, name)
SELECT i.relname, d.indisunique, a.attname
FROM pg_class t, pg_class i, pg_index d, pg_attribute a
@ -219,34 +479,49 @@ module ActiveRecord
indexes
end
def columns(table_name, name = nil) #:nodoc:
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")
# Returns the list of all column definitions for a table.
def columns(table_name, name = nil)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).collect do |name, type, default, notnull|
PostgreSQLColumn.new(name, default, type, notnull == 'f')
end
end
# Set the schema search path to a string of comma-separated schema names.
# Names beginning with $ are quoted (e.g. $user => '$user')
# See http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html
def schema_search_path=(schema_csv) #:nodoc:
# Sets the schema search path to a string of comma-separated schema names.
# Names beginning with $ have to be quoted (e.g. $user => '$user').
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
#
# This should be not be called manually but set in database.yml.
def schema_search_path=(schema_csv)
if schema_csv
execute "SET search_path TO #{schema_csv}"
@schema_search_path = nil
@schema_search_path = schema_csv
end
end
def schema_search_path #:nodoc:
# Returns the active schema search path.
def schema_search_path
@schema_search_path ||= query('SHOW search_path')[0][0]
end
def default_sequence_name(table_name, pk = nil)
# Returns the current client message level.
def client_min_messages
query('SHOW client_min_messages')[0][0]
end
# Set the client message level.
def client_min_messages=(level)
execute("SET client_min_messages TO '#{level}'")
end
# Returns the sequence name for a table's primary key or some other specified key.
def default_sequence_name(table_name, pk = nil) #:nodoc:
default_pk, default_seq = pk_and_sequence_for(table_name)
default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq"
end
# Resets sequence to the max value of the table's pk if present.
def reset_pk_sequence!(table, pk = nil, sequence = nil)
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
unless pk and sequence
default_pk, default_sequence = pk_and_sequence_for(table)
pk ||= default_pk
@ -263,19 +538,18 @@ module ActiveRecord
end
end
# Find a table's primary key and sequence.
def pk_and_sequence_for(table)
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'PK and serial sequence')[0]
SELECT attr.attname, name.nspname, seq.relname
SELECT attr.attname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
pg_namespace name,
pg_constraint cons
WHERE seq.oid = dep.objid
AND seq.relnamespace = name.oid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
@ -289,11 +563,9 @@ module ActiveRecord
# If that fails, try parsing the primary key's default value.
# 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 = query(<<-end_sql, 'PK and custom sequence')[0]
SELECT attr.attname, name.nspname, split_part(def.adsrc, '''', 2)
SELECT attr.attname, 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)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
@ -302,68 +574,72 @@ 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, return unqualified sequence
# We cannot qualify unqualified sequences, as rails doesn't qualify any table access, using the search path
# [primary_key, sequence]
[result.first, result.last]
rescue
nil
end
# Renames a table.
def rename_table(name, new_name)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
# Adds a column to a table.
def add_column(table_name, column_name, type, options = {})
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])}")
execute("ALTER TABLE #{table_name} ADD COLUMN #{quote_column_name(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
change_column_default(table_name, column_name, default) if options_include_default?(options)
change_column_null(table_name, column_name, false, default) if notnull
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
begin
execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
execute "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(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.
# This is PostgreSQL 7.x, so we have to 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], options[:precision], options[:scale])})"
tmp_column_name = "#{column_name}_ar_tmp"
add_column(table_name, tmp_column_name, type, options)
execute "UPDATE #{table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(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)
rename_column(table_name, tmp_column_name, column_name)
commit_db_transaction
end
if options_include_default?(options)
change_column_default(table_name, column_name, options[:default])
end
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
end
def change_column_default(table_name, column_name, default) #:nodoc:
# Changes the default value of a table column.
def change_column_default(table_name, column_name, 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:
def change_column_null(table_name, column_name, null, default = nil)
unless null || default.nil?
execute("UPDATE #{table_name} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
execute("ALTER TABLE #{table_name} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
# Renames a column in a table.
def rename_column(table_name, column_name, 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:
# Drops an index from a table.
def remove_index(table_name, options = {})
execute "DROP INDEX #{index_name(table_name, options)}"
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
# Maps logical Rails types to PostgreSQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
if limit.nil? || limit == 4
@ -375,32 +651,32 @@ module ActiveRecord
end
end
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
# Returns a 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)
def distinct(columns, order_by) #:nodoc:
return "DISTINCT #{columns}" if order_by.blank?
# construct a clean list of column names from the ORDER BY clause, removing
# any asc/desc modifiers
# 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
# 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.
# Returns an 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)
def add_order_by_for_association_limiting!(sql, options) #:nodoc:
return sql if options[:order].blank?
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
@ -410,103 +686,135 @@ module ActiveRecord
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
def configure_connection
if @config[:encoding]
execute("SET client_encoding TO '#{@config[:encoding]}'")
end
if @config[:min_messages]
execute("SET client_min_messages TO '#{@config[:min_messages]}'")
end
protected
# Returns the version of the connected PostgreSQL version.
def postgresql_version
@postgresql_version ||=
if @connection.respond_to?(:server_version)
@connection.server_version
else
# Mimic PGconn.server_version behavior
begin
query('SELECT version()')[0][0] =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/
($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i
rescue
0
end
end
end
def last_insert_id(table, sequence_name)
private
# The internal PostgreSQL identifer of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
@connection = PGconn.connect(*@connection_parameters)
PGconn.translate_results = false if PGconn.respond_to?(:translate_results=)
# Ignore async_exec and async_query when using postgres-pr.
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
# Use escape string syntax if available. We cannot do this lazily when encountering
# the first string, because that could then break any transactions in progress.
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
# support escape string syntax. Don't override the inherited quoted_string_prefix.
if supports_standard_conforming_strings?
self.class.instance_eval do
define_method(:quoted_string_prefix) { 'E' }
end
end
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
# should know about this but can't detect it there, so deal with it here.
money_precision = (postgresql_version >= 80300) ? 19 : 10
PostgreSQLColumn.module_eval(<<-end_eval)
def extract_precision(sql_type)
if sql_type =~ /^money$/
#{money_precision}
else
super
end
end
end_eval
configure_connection
end
# Configures the encoding, verbosity, and schema search path of the connection.
# This is called by #connect and should not be called manually.
def configure_connection
if @config[:encoding]
if @connection.respond_to?(:set_client_encoding)
@connection.set_client_encoding(@config[:encoding])
else
execute("SET client_encoding TO '#{@config[:encoding]}'")
end
end
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
end
# Returns the current ID of a table's sequence.
def last_insert_id(table, sequence_name) #:nodoc:
Integer(select_value("SELECT currval('#{sequence_name}')"))
end
# Executes a SELECT query and returns the results, performing any data type
# conversions that are required to be performed here instead of in PostgreSQLColumn.
def select(sql, name = nil)
fields, rows = select_raw(sql, name)
result = []
for row in rows
row_hash = {}
fields.each_with_index do |f, i|
row_hash[f] = row[i]
end
result << row_hash
end
result
end
def select_raw(sql, name = nil)
res = execute(sql, name)
results = res.result
fields = []
rows = []
if results.length > 0
fields = res.fields
results.each do |row|
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)
row.each_index do |cell_index|
# If this is a money type column and there are any currency symbols,
# then strip them off. Indeed it would be prettier to do this in
# PostgreSQLColumn.string_to_decimal but would break form input
# fields that call value_before_type_cast.
if res.type(cell_index) == MONEY_COLUMN_TYPE_OID
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
case column = row[cell_index]
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
row[cell_index] = column.gsub(/[^-\d\.]/, '')
when /^-?\D+[\d\.]+,\d{2}$/ # (2)
row[cell_index] = column.gsub(/[^-\d,]/, '').sub(/,/, '.')
end
end
hashed_row[fields[cel_index]] = column
hashed_row[fields[cell_index]] = column
end
rows << hashed_row
rows << row
end
end
res.clear
return rows
return fields, rows
end
def escape_bytea(s)
if PGconn.respond_to? :escape_bytea
self.class.send(:define_method, :escape_bytea) do |s|
PGconn.escape_bytea(s) if s
end
else
self.class.send(:define_method, :escape_bytea) do |s|
if s
result = ''
s.each_byte { |c| result << sprintf('\\\\%03o', c) }
result
end
end
end
escape_bytea(s)
end
def unescape_bytea(s)
if PGconn.respond_to? :unescape_bytea
self.class.send(:define_method, :unescape_bytea) do |s|
PGconn.unescape_bytea(s) if s
end
else
self.class.send(:define_method, :unescape_bytea) do |s|
if s
result = ''
i, max = 0, s.size
while i < max
char = s[i]
if char == ?\\
if s[i+1] == ?\\
char = ?\\
i += 1
else
char = s[i+1..i+3].oct
i += 3
end
end
result << char
i += 1
end
result
end
end
end
unescape_bytea(s)
end
# Query a table's column names, default values, and types.
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
# SELECT column.name, column.type, default.value
@ -524,7 +832,7 @@ module ActiveRecord
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name)
def column_definitions(table_name) #:nodoc:
query <<-end_sql
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
@ -534,51 +842,6 @@ module ActiveRecord
ORDER BY a.attnum
end_sql
end
# Translate PostgreSQL-specific types into simplified SQL types.
# These are special cases; standard types are handled by
# ConnectionAdapters::Column#simplified_type.
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 /^bytea/i then 'binary'
else field_type # Pass through standard types.
end
end
def default_value(value)
# 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
end
# Only needed for DateTime instances
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, v.usec]
Time.send(Base.default_timezone, *time_array) rescue nil
end
end
end
end

View file

@ -0,0 +1,34 @@
require 'active_record/connection_adapters/sqlite_adapter'
module ActiveRecord
class Base
# sqlite3 adapter reuses sqlite_connection.
def self.sqlite3_connection(config) # :nodoc:
parse_sqlite_config!(config)
unless self.class.const_defined?(:SQLite3)
require_library_or_gem(config[:adapter])
end
db = SQLite3::Database.new(
config[:database],
:results_as_hash => true,
:type_translation => false
)
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
ConnectionAdapters::SQLite3Adapter.new(db, logger)
end
end
module ConnectionAdapters #:nodoc:
class SQLite3Adapter < SQLiteAdapter # :nodoc:
def table_structure(table_name)
returning structure = @connection.table_info(table_name) do
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
end
end
end
end
end

View file

@ -1,33 +1,11 @@
# Author: Luke Holden <lholden@cablelan.net>
# Updated for SQLite3: Jamis Buck <jamis@37signals.com>
require 'active_record/connection_adapters/abstract_adapter'
module ActiveRecord
class Base
class << self
# sqlite3 adapter reuses sqlite_connection.
def sqlite3_connection(config) # :nodoc:
parse_config!(config)
unless self.class.const_defined?(:SQLite3)
require_library_or_gem(config[:adapter])
end
db = SQLite3::Database.new(
config[:database],
:results_as_hash => true,
:type_translation => false
)
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
def sqlite_connection(config) # :nodoc:
parse_config!(config)
parse_sqlite_config!(config)
unless self.class.const_defined?(:SQLite)
require_library_or_gem(config[:adapter])
@ -47,7 +25,7 @@ module ActiveRecord
end
private
def parse_config!(config)
def parse_sqlite_config!(config)
config[:database] ||= config[:dbfile]
# Require database.
unless config[:database]
@ -56,7 +34,7 @@ module ActiveRecord
# Allow database path relative to RAILS_ROOT, but only if
# the database path is not the special path that tells
# Sqlite build a database only in memory.
# Sqlite to build a database only in memory.
if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database]
config[:database] = File.expand_path(config[:database], RAILS_ROOT)
end
@ -73,16 +51,16 @@ module ActiveRecord
when "\0" then "%00"
when "%" then "%25"
end
end
end
end
def binary_to_string(value)
value.gsub(/%00|%25/n) do |b|
case b
when "%00" then "\0"
when "%25" then "%"
end
end
end
end
end
end
@ -105,14 +83,23 @@ module ActiveRecord
def requires_reloading?
true
end
def disconnect!
super
@connection.close rescue nil
end
def supports_count_distinct? #:nodoc:
false
sqlite_version >= '3.2.6'
end
def supports_autoincrement? #:nodoc:
sqlite_version >= '3.1.0'
end
def native_database_types #:nodoc:
{
:primary_key => "INTEGER PRIMARY KEY NOT NULL",
:primary_key => default_primary_key_type,
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "integer" },
@ -145,44 +132,30 @@ module ActiveRecord
catch_schema_changes { log(sql, name) { @connection.execute(sql) } }
end
def update(sql, name = nil) #:nodoc:
execute(sql, name)
def update_sql(sql, name = nil) #:nodoc:
super
@connection.changes
end
def delete(sql, name = nil) #:nodoc:
def delete_sql(sql, name = nil) #:nodoc:
sql += " WHERE 1=1" unless sql =~ /WHERE/i
execute(sql, name)
@connection.changes
super sql, name
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
execute(sql, name = nil)
id_value || @connection.last_insert_row_id
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
super || @connection.last_insert_row_id
end
def select_all(sql, name = nil) #:nodoc:
def select_rows(sql, name = nil)
execute(sql, name).map do |row|
record = {}
row.each_key do |key|
if key.is_a?(String)
record[key.sub(/^\w+\./, '')] = row[key]
end
end
record
(0...(row.size / 2)).map { |i| row[i] }
end
end
def select_one(sql, name = nil) #:nodoc:
result = select_all(sql, name)
result.nil? ? nil : result.first
end
def begin_db_transaction #:nodoc:
catch_schema_changes { @connection.transaction }
end
def commit_db_transaction #:nodoc:
catch_schema_changes { @connection.commit }
end
@ -201,7 +174,13 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
def tables(name = nil) #:nodoc:
execute("SELECT name FROM sqlite_master WHERE type = 'table'", name).map do |row|
sql = <<-SQL
SELECT name
FROM sqlite_master
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
SQL
execute(sql, name).map do |row|
row[0]
end
end
@ -229,7 +208,7 @@ module ActiveRecord
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)
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
@ -239,13 +218,13 @@ module ActiveRecord
# See last paragraph on http://www.sqlite.org/lang_altertable.html
execute "VACUUM"
end
def remove_column(table_name, column_name) #:nodoc:
alter_table(table_name) do |definition|
definition.columns.delete(definition[column_name])
end
end
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|
definition[column_name].default = default
@ -259,60 +238,78 @@ module ActiveRecord
self.type = type
self.limit = options[:limit] if options.include?(:limit)
self.default = options[:default] if include_default
self.null = options[:null] if options.include?(:null)
end
end
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
alter_table(table_name, :rename => {column_name => new_column_name})
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
end
def empty_insert_statement(table_name)
"INSERT INTO #{table_name} VALUES(NULL)"
end
protected
def table_structure(table_name)
returning structure = execute("PRAGMA table_info(#{table_name})") do
raise ActiveRecord::StatementInvalid if structure.empty?
def select(sql, name = nil) #:nodoc:
execute(sql, name).map do |row|
record = {}
row.each_key do |key|
if key.is_a?(String)
record[key.sub(/^\w+\./, '')] = row[key]
end
end
record
end
end
def table_structure(table_name)
returning structure = execute("PRAGMA table_info(#{table_name})") do
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
end
end
def alter_table(table_name, options = {}) #:nodoc:
altered_table_name = "altered_#{table_name}"
caller = lambda {|definition| yield definition if block_given?}
transaction do
move_table(table_name, altered_table_name,
move_table(table_name, altered_table_name,
options.merge(:temporary => true))
move_table(altered_table_name, table_name, &caller)
end
end
def move_table(from, to, options = {}, &block) #:nodoc:
copy_table(from, to, options, &block)
drop_table(from)
end
def copy_table(from, to, options = {}) #:nodoc:
create_table(to, options) do |@definition|
options = options.merge(:id => !columns(from).detect{|c| c.name == 'id'}.nil?)
create_table(to, options) do |definition|
@definition = definition
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
@definition.column(column_name, column.type,
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:null => column.null)
end
@definition.primary_key(primary_key(from))
@definition.primary_key(primary_key(from)) if primary_key(from)
yield @definition if block_given?
end
copy_table_indexes(from, to)
copy_table_contents(from, to,
@definition.columns.map {|column| column.name},
copy_table_contents(from, to,
@definition.columns.map {|column| column.name},
options[:rename] || {})
end
def copy_table_indexes(from, to) #:nodoc:
indexes(from).each do |index|
name = index.name
@ -321,27 +318,29 @@ module ActiveRecord
elsif from == "altered_#{to}"
name = name[5..-1]
end
# 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
end
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])}
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
@connection.execute "SELECT * FROM #{from}" do |row|
sql = "INSERT INTO #{to} ("+columns*','+") VALUES ("
sql = "INSERT INTO #{to} (#{quoted_columns}) VALUES ("
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
sql << ')'
@connection.execute sql
end
end
def catch_schema_changes
return yield
rescue ActiveRecord::StatementInvalid => exception
@ -352,49 +351,34 @@ module ActiveRecord
raise
end
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?
def sqlite_version
@sqlite_version ||= select_value('select sqlite_version(*)')
end
def default_primary_key_type
if supports_autoincrement?
'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze
else
'INTEGER PRIMARY KEY NOT NULL'.freeze
end
end
end
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
# SQLite 2 does not support COUNT(DISTINCT) queries:
#
# select COUNT(DISTINCT ArtistID) from CDs;
#
# In order to get the number of artists we execute the following statement
#
# SELECT COUNT(ArtistID) FROM (SELECT DISTINCT ArtistID FROM CDs);
def execute(sql, name = nil) #:nodoc:
super(rewrite_count_distinct_queries(sql), name)
def supports_count_distinct? #:nodoc:
false
end
def rewrite_count_distinct_queries(sql)
if sql =~ /count\(distinct ([^\)]+)\)( AS \w+)? (.*)/i
distinct_column = $1
distinct_query = $3
column_name = distinct_column.split('.').last
"SELECT COUNT(#{column_name}) FROM (SELECT DISTINCT #{distinct_column} #{distinct_query})"
else
sql
end
end
def rename_table(name, new_name)
move_table(name, new_name)
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:

View file

@ -1,591 +0,0 @@
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>
# Date: 10/14/2004
#
# Modifications: DeLynn Berry <delynnb@megastarfinancial.com>
# Date: 3/22/2005
#
# Modifications (ODBC): Mark Imbriaco <mark.imbriaco@pobox.com>
# Date: 6/26/2005
# 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
def self.sqlserver_connection(config) #:nodoc:
require_library_or_gem 'dbi' unless self.class.const_defined?(:DBI)
config = config.symbolize_keys
mode = config[:mode] ? config[:mode].to_s.upcase : 'ADO'
username = config[:username] ? config[:username].to_s : 'sa'
password = config[:password] ? config[:password].to_s : ''
autocommit = config.key?(:autocommit) ? config[:autocommit] : true
if mode == "ODBC"
raise ArgumentError, "Missing DSN. Argument ':dsn' must be set in order for this adapter to work." unless config.has_key?(:dsn)
dsn = config[:dsn]
driver_url = "DBI:ODBC:#{dsn}"
else
raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database)
database = config[:database]
host = config[:host] ? config[:host].to_s : 'localhost'
driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User Id=#{username};Password=#{password};"
end
conn = DBI.connect(driver_url, username, password)
conn["AutoCommit"] = autocommit
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
end
end # class Base
module ConnectionAdapters
class SQLServerColumn < Column# :nodoc:
attr_reader :identity, :is_special
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 = 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 /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?
case type
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 super
end
end
def cast_to_time(value)
return value if value.is_a?(Time)
time_array = ParseDate.parsedate(value)
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
else
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.
def self.string_to_binary(value)
value.gsub(/(\r|\n|\0|\x1a)/) do
case $1
when "\r" then "%00"
when "\n" then "%01"
when "\0" then "%02"
when "\x1a" then "%03"
end
end
end
def self.binary_to_string(value)
value.gsub(/(%00|%01|%02|%03)/) do
case $1
when "%00" then "\r"
when "%01" then "\n"
when "%02\0" then "\0"
when "%03" then "\x1a"
end
end
end
end
# In ADO mode, this adapter will ONLY work on Windows systems,
# since it relies on Win32OLE, which, to my knowledge, is only
# available on Windows.
#
# This mode also relies on the ADO support in the DBI module. If you are using the
# one-click installer of Ruby, then you already have DBI installed, but
# the ADO module is *NOT* installed. You will need to get the latest
# source distribution of Ruby-DBI from http://ruby-dbi.sourceforge.net/
# unzip it, and copy the file
# <tt>src/lib/dbd_ado/ADO.rb</tt>
# to
# <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt>
# (you will more than likely need to create the ADO directory).
# Once you've installed that file, you are ready to go.
#
# In ODBC mode, the adapter requires the ODBC support in the DBI module which requires
# the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing,
# and it is available at http://www.ch-werner.de/rubyodbc/
#
# Options:
#
# * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO.
# * <tt>:username</tt> -- Defaults to sa.
# * <tt>:password</tt> -- Defaults to empty string.
#
# ADO specific options:
#
# * <tt>:host</tt> -- Defaults to localhost.
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
#
# ODBC specific options:
#
# * <tt>:dsn</tt> -- Defaults to nothing.
#
# ADO code tested on Windows 2000 and higher systems,
# running ruby 1.8.2 (2004-07-29) [i386-mswin32], and SQL Server 2000 SP3.
#
# ODBC code tested on a Fedora Core 4 system, running FreeTDS 0.63,
# unixODBC 2.2.11, Ruby ODBC 0.996, Ruby DBI 0.0.23 and Ruby 1.8.2.
# [Linux strongmad 2.6.11-1.1369_FC4 #1 Thu Jun 2 22:55:56 EDT 2005 i686 i686 i386 GNU/Linux]
class SQLServerAdapter < AbstractAdapter
def initialize(connection, logger, connection_options=nil)
super(connection, logger)
@connection_options = connection_options
end
def native_database_types
{
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int" },
:float => { :name => "float", :limit => 8 },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "datetime" },
:date => { :name => "datetime" },
:binary => { :name => "image"},
:boolean => { :name => "bit"}
}
end
def adapter_name
'SQLServer'
end
def supports_migrations? #:nodoc:
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").finish
true
rescue DBI::DatabaseError, DBI::InterfaceError
false
end
# Reconnects to the database, returns false if no connection could be made.
def reconnect!
disconnect!
@connection = DBI.connect(*@connection_options)
rescue DBI::DatabaseError => e
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
false
end
# Disconnects from the database
def disconnect!
@connection.disconnect rescue nil
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?
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
result = log(sql, name) { @connection.select_all(sql) }
#result = @connection.select_all(sql)
columns = []
result.each do |field|
default = field[:DefaultValue].to_s.gsub!(/[()\']/,"") =~ /null/ ? nil : field[:DefaultValue]
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 << 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)
execute(sql, name)
id_value || select_one("SELECT @@IDENTITY AS Ident")["Ident"]
end
def update(sql, name = nil)
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
@connection["AutoCommit"] = true
end
def commit_db_transaction
@connection.commit
ensure
@connection["AutoCommit"] = true
end
def rollback_db_transaction
@connection.rollback
ensure
@connection["AutoCommit"] = true
end
def quote(value, column = nil)
return value.quoted_id if value.respond_to?(:quoted_id)
case value
when TrueClass then '1'
when FalseClass then '0'
when Time, DateTime then "'#{value.strftime("%Y%m%d %H:%M:%S")}'"
when Date then "'#{value.strftime("%Y%m%d")}'"
else super
end
end
def quote_string(string)
string.gsub(/\'/, "''")
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(\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(\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|
parts = field.split(" ")
tc = parts[0]
if sql =~ /\.\[/ and tc =~ /\./ # if column quoting used in query
tc.gsub!(/\./, '\\.\\[')
tc << '\\]'
end
if sql =~ /#{tc} AS (t\d_r\d\d?)/
parts[0] = $1
elsif parts[0] =~ /\w+\.(\w+)/
parts[0] = $1
end
parts.join(' ')
end.join(', ')
sql << " ORDER BY #{change_order_direction(options[:order])}) AS tmp2 ORDER BY #{options[:order]}"
else
sql << " ) AS tmp2"
end
elsif sql !~ /^\s*SELECT (@@|COUNT\()/i
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i) do
"SELECT#{$1} TOP #{options[:limit]}"
end unless options[:limit].nil?
end
end
def recreate_database(name)
drop_database(name)
create_database(name)
end
def drop_database(name)
execute "DROP DATABASE #{name}"
end
def create_database(name)
execute "CREATE DATABASE #{name}"
end
def current_database
@connection.select_one("select DB_NAME()")[0]
end
def tables(name = nil)
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)
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
# 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], 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 #{quote(options[:default], options[:column])} FOR #{column_name}"
end
sql_commands.each {|c|
execute(c)
}
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}]"
end
def remove_default_constraint(table_name, column_name)
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}.#{quote_column_name(index_name(table_name, options))}"
end
private
def select(sql, name = nil)
repair_special_columns(sql)
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
result << row_hash
end
end
result
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)
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
$1
elsif sql =~ /from\s+([^\(\s]+)\s*/i
$1
else
nil
end
end
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|
return col.name if col.identity
end
return nil
end
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)
order.split(",").collect {|fragment|
case fragment
when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
else String.new(fragment).split(',').join(' DESC,') + ' DESC'
end
}.join(",")
end
def get_special_columns(table_name)
special = []
@table_columns ||= {}
@table_columns[table_name] ||= columns(table_name)
@table_columns[table_name].each do |col|
special << col.name if col.is_special
end
special
end
def repair_special_columns(sql)
special_cols = get_special_columns(get_table_name(sql))
for col in special_cols.to_a
sql.gsub!(Regexp.new(" #{col.to_s} = "), " #{col.to_s} LIKE ")
sql.gsub!(/ORDER BY #{col.to_s}/i, '')
end
sql
end
end #class SQLServerAdapter < AbstractAdapter
end #module ConnectionAdapters
end #module ActiveRecord

View file

@ -1,662 +0,0 @@
# sybase_adaptor.rb
# 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'
begin
require 'sybsql'
module ActiveRecord
class Base
# Establishes a connection to the database that's used by all Active Record objects
def self.sybase_connection(config) # :nodoc:
config = config.symbolize_keys
username = config[:username] ? config[:username].to_s : 'sa'
password = config[:password] ? config[:password].to_s : ''
if config.has_key?(:host)
host = config[:host]
else
raise ArgumentError, "No database server name specified. Missing argument: host."
end
if config.has_key?(:database)
database = config[:database]
else
raise ArgumentError, "No database specified. Missing argument: database."
end
ConnectionAdapters::SybaseAdapter.new(
SybSQL.new({'S' => host, 'U' => username, 'P' => password},
ConnectionAdapters::SybaseAdapterContext), database, config, logger)
end
end # class Base
module ConnectionAdapters
# ActiveRecord connection adapter for Sybase Open Client bindings
# (see http://raa.ruby-lang.org/project/sybase-ctlib).
#
# Options:
#
# * <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>:password</tt> -- Defaults to empty string.
#
# Usage Notes:
#
# * The sybase-ctlib bindings do not support the DATE SQL column type; use DATETIME instead.
# * Table and column names are limited to 30 chars in Sybase 12.5
# * :binary columns not yet supported
# * :boolean columns use the BIT SQL type, which does not allow nulls or
# indexes. If a DEFAULT is not specified for ALTER TABLE commands, the
# column will be declared with DEFAULT 0 (false).
#
# Migrations:
#
# The Sybase adapter supports migrations, but for ALTER TABLE commands to
# work, the database must have the database option 'select into' set to
# 'true' with sp_dboption (see below). The sp_helpdb command lists the current
# options for all databases.
#
# 1> use mydb
# 2> go
# 1> master..sp_dboption mydb, "select into", true
# 2> go
# 1> checkpoint
# 2> go
class SybaseAdapter < AbstractAdapter # :nodoc:
class ColumnWithIdentity < Column
attr_reader :identity
def initialize(name, default, sql_type = nil, nullable = nil, identity = nil, primary = nil)
super(name, default, sql_type, nullable)
@default, @identity, @primary = type_cast(default), identity, primary
end
def simplified_type(field_type)
case field_type
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
def self.string_to_binary(value)
"0x#{value.unpack("H*")[0]}"
end
def self.binary_to_string(value)
# FIXME: sybase-ctlib uses separate sql method for binary columns.
value
end
end # class ColumnWithIdentity
# Sybase adapter
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}"
end
end
def native_database_types
{
:primary_key => "numeric(9,0) IDENTITY PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int" },
:float => { :name => "float", :limit => 8 },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "timestamp" },
:time => { :name => "time" },
:date => { :name => "datetime" },
:binary => { :name => "image"},
:boolean => { :name => "bit" }
}
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
def active?
!(@connection.connection.nil? || @connection.connection_dead?)
end
def disconnect!
@connection.close rescue nil
end
def reconnect!
raise "Sybase Connection Adapter does not yet support reconnect!"
# disconnect!
# connect! # Not yet implemented
end
def table_alias_length
30
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
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
execute(sql, name)
ident = select_one("SELECT @@IDENTITY AS last_id")["last_id"]
id_value || ident
end
ensure
if ii_enabled
begin
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)
raw_execute(sql, name)
@connection.results[0].row_count
end
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 current_database
select_one("select DB_NAME() as name")["name"]
end
def tables(name = nil)
select("select name from sysobjects where type='U'", name).map { |row| row['name'] }
end
def indexes(table_name, name = nil)
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! }
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
end
def quoted_true
"1"
end
def quoted_false
"0"
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 @numconvert && force_numeric?(column) && value =~ /^[+-]?[0-9]+$/o
value
else
"'#{quote_string(value)}'"
end
when NilClass then (column && column.type == :boolean) ? '0' : "NULL"
when TrueClass then '1'
when FalseClass then '0'
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 super
end
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)
s.gsub(/'/, "''") # ' (for ruby-mode)
end
def quote_column_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 use_temp_table?
# Use temp table to hack offset with Sybase
sql.sub!(/ FROM /i, ' INTO #artemp FROM ')
elsif zero_limit?
# "SET ROWCOUNT 0" turns off limits, so we have
# to use a cheap trick.
if sql =~ /WHERE/i
sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ')
elsif sql =~ /ORDER\s+BY/i
sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY')
else
sql << 'WHERE 1 = 2'
end
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
def rename_table(name, new_name)
execute "EXEC sp_rename '#{name}', '#{new_name}'"
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:
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
end
def remove_column(table_name, column_name)
remove_default_constraint(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{column_name}"
end
def remove_default_constraint(table_name, column_name)
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 = {})
execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}"
end
def add_column_options!(sql, options) #:nodoc:
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")
end
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
# if :null option is omitted. Disallow NULLs for boolean.
type = col.nil? ? "" : col[:type]
# Ignore :null if a primary key
return false if type =~ /PRIMARY KEY/i
# Ignore :null if a :boolean or BIT column
if (sql =~ /\s+bit(\s+DEFAULT)?/i) || type == :boolean
# If no default clause found on a boolean column, add one.
sql << " DEFAULT 0" if $1.nil?
return false
end
true
end
# Return the last value of the identity global value.
def last_insert_id
@connection.sql("SELECT @@IDENTITY")
unless @connection.cmd_fail?
id = @connection.top_row_result.rows.first.first
if id
id = id.to_i
id = nil if id == 0
end
else
id = nil
end
id
end
def affected_rows(name = nil)
@connection.sql("SELECT @@ROWCOUNT")
unless @connection.cmd_fail?
count = @connection.top_row_result.rows.first.first
count = count.to_i if count
else
0
end
end
# 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
def raw_execute(sql, name = nil)
log(sql, name) do
@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
@connection.set_rowcount(@offset || 0)
@connection.sql_norow("delete from #artemp") # Delete leading rows
@connection.set_rowcount(0)
@connection.sql("select * from #artemp") # Return the rest
end
end
raise StatementInvalid, "SQL Command Failed for #{name}: #{sql}\nMessage: #{@connection.context.message}" if @connection.context.failed? or @connection.cmd_fail?
rows = []
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 use_temp_table?
@limit = @offset = nil
rows
end
def get_table_name(sql)
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
$1
elsif sql =~ /from\s+([^\(\s]+)\s*/i
$1
else
nil
end
end
def has_identity_column(table_name)
!get_identity_column(table_name).nil?
end
def get_identity_column(table_name)
@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
@id_columns[table_name]
end
def query_contains_identity_column(sql, col)
sql =~ /\[#{col}\]/
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)
has_scale = (!scale.nil? && scale > 0)
type = if field_type =~ /numeric/i and !has_scale
'int'
elsif field_type =~ /money/i
'numeric'
else
resolve_type(field_type.strip)
end
spec = if prec
has_scale ? "(#{prec},#{scale})" : "(#{prec})"
elsif length && !(type =~ /date|time|text/)
"(#{length})"
else
''
end
"#{type}#{spec}"
end
end # class SybaseAdapter
class SybaseAdapterContext < SybSQLContext
DEADLOCK = 1205
attr_reader :message
def init(logger = nil)
@deadlocked = false
@failed = false
@logger = logger
@message = nil
end
def srvmsgCB(con, msg)
# Do not log change of context messages.
if msg['severity'] == 10 or msg['severity'] == 0
return true
end
if msg['msgnumber'] == DEADLOCK
@deadlocked = true
else
@logger.info "SQL Command failed!" if @logger
@failed = true
end
if @logger
@logger.error "** SybSQLContext Server Message: **"
@logger.error " Message number #{msg['msgnumber']} Severity #{msg['severity']} State #{msg['state']} Line #{msg['line']}"
@logger.error " Server #{msg['srvname']}"
@logger.error " Procedure #{msg['proc']}"
@logger.error " Message String: #{msg['text']}"
end
@message = msg['text']
true
end
def deadlocked?
@deadlocked
end
def failed?
@failed
end
def reset
@deadlocked = false
@failed = false
@message = nil
end
def cltmsgCB(con, msg)
return true unless ( msg.kind_of?(Hash) )
unless ( msg[ "severity" ] ) then
return true
end
if @logger
@logger.error "** SybSQLContext Client-Message: **"
@logger.error " Message number: LAYER=#{msg[ 'layer' ]} ORIGIN=#{msg[ 'origin' ]} SEVERITY=#{msg[ 'severity' ]} NUMBER=#{msg[ 'number' ]}"
@logger.error " Message String: #{msg['msgstring']}"
@logger.error " OS Error: #{msg['osstring']}"
@message = msg['msgstring']
end
@failed = true
# Not retry , CS_CV_RETRY_FAIL( probability TimeOut )
if( msg[ 'severity' ] == "RETRY_FAIL" ) then
@timeout_p = true
return false
end
return true
end
end # class SybaseAdapterContext
end # module ConnectionAdapters
end # module ActiveRecord
# Allow identity inserts for fixtures.
require "active_record/fixtures"
class Fixtures
alias :original_insert_fixtures :insert_fixtures
def insert_fixtures
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
end
rescue LoadError => cannot_require_sybase
# Couldn't load sybase adapter
end

View file

@ -1,104 +0,0 @@
module ActiveRecord
module Associations # :nodoc:
module ClassMethods
def deprecated_collection_count_method(collection_name)# :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def #{collection_name}_count(force_reload = false)
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)
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
def deprecated_association_comparison_method(association_name, association_class_name) # :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def #{association_name}?(comparison_object, force_reload = false)
if comparison_object.kind_of?(#{association_class_name})
#{association_name}(force_reload) == comparison_object
else
raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
end
end
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

View file

@ -1,44 +0,0 @@
module ActiveRecord
class Base
class << self
# DEPRECATION NOTICE: This method is deprecated in favor of find with the :conditions option.
#
# Works like find, but the record matching +id+ must also meet the +conditions+.
# +RecordNotFound+ is raised if no record can be found matching the +id+ or meeting the condition.
# Example:
# Person.find_on_conditions 5, "first_name LIKE '%dav%' AND last_name = 'heinemeier'"
def find_on_conditions(ids, conditions) # :nodoc:
find(ids, :conditions => conditions)
end
deprecate :find_on_conditions => "use find(ids, :conditions => conditions)"
# DEPRECATION NOTICE: This method is deprecated in favor of find(:first, options).
#
# Returns the object for the first record responding to the conditions in +conditions+,
# such as "group = 'master'". If more than one record is returned from the query, it's the first that'll
# be used to create the object. In such cases, it might be beneficial to also specify
# +orderings+, like "income DESC, name", to control exactly which record is to be used. Example:
# Employee.find_first "income > 50000", "income DESC, name"
def find_first(conditions = nil, orderings = nil, joins = nil) # :nodoc:
find(:first, :conditions => conditions, :order => orderings, :joins => joins)
end
deprecate :find_first => "use find(:first, ...)"
# DEPRECATION NOTICE: This method is deprecated in favor of find(:all, options).
#
# Returns an array of all the objects that could be instantiated from the associated
# table in the database. The +conditions+ can be used to narrow the selection of objects (WHERE-part),
# such as by "color = 'red'", and arrangement of the selection can be done through +orderings+ (ORDER BY-part),
# such as by "last_name, first_name DESC". A maximum of returned objects and their offset can be specified in
# +limit+ with either just a single integer as the limit or as an array with the first element as the limit,
# the second as the offset. Examples:
# Project.find_all "category = 'accounts'", "last_accessed DESC", 15
# Project.find_all ["category = ?", category_name], "created ASC", [15, 20]
def find_all(conditions = nil, orderings = nil, limit = nil, joins = nil) # :nodoc:
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

View file

@ -9,10 +9,15 @@ module YAML #:nodoc:
end
end
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
if defined? ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
else
class FixtureClassNotFound < StandardError #:nodoc:
end
end
# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours:
# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
#
# 1. YAML fixtures
# 2. CSV fixtures
@ -21,7 +26,7 @@ end
# = YAML fixtures
#
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, humanly-readable format. It ships with Ruby 1.8.1+.
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
#
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
# by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
@ -82,7 +87,7 @@ end
#
# = Single-file fixtures
#
# This type of fixtures was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
# appointed by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
@ -106,7 +111,7 @@ end
# = Using Fixtures
#
# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
# fixtures, but first let's take a look at a sample unit test found:
# fixtures, but first let's take a look at a sample unit test:
#
# require 'web_site'
#
@ -124,8 +129,8 @@ end
# fixtures :web_sites # add more by separating the symbols with commas
# ...
#
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here tho), we trigger
# the testing environment to automatically load the appropriate fixtures into the database before each test.
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
# the testing environment to automatically load the appropriate fixtures into the database before each test.
# To ensure consistent data, the environment deletes the fixtures before running the load.
#
# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
@ -151,7 +156,7 @@ end
# self.use_instantiated_fixtures = false
#
# - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
# self.use_instantiated_fixtures = :no_instances
# self.use_instantiated_fixtures = :no_instances
#
# Even if auto-instantiated fixtures are disabled, you can still access them
# by name via special dynamic methods. Each method has the same name as the
@ -183,7 +188,7 @@ end
#
# = Transactional fixtures
#
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
#
# class FooTest < Test::Unit::TestCase
@ -203,22 +208,269 @@ end
# end
# end
#
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
# When *not* to use transactional fixtures:
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress.)
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
# When *not* to use transactional fixtures:
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
# Use InnoDB, MaxDB, or NDB instead.
#
# = Advanced YAML Fixtures
#
# YAML fixtures that don't specify an ID get some extra features:
#
# * Stable, autogenerated ID's
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
#
# == Stable, autogenerated ID's
#
# Here, have a monkey fixture:
#
# george:
# id: 1
# name: George the Monkey
#
# reginald:
# id: 2
# name: Reginald the Pirate
#
# Each of these fixtures has two unique identifiers: one for the database
# and one for the humans. Why don't we generate the primary key instead?
# Hashing each fixture's label yields a consistent ID:
#
# george: # generated id: 503576764
# name: George the Monkey
#
# reginald: # generated id: 324201669
# name: Reginald the Pirate
#
# ActiveRecord looks at the fixture's model class, discovers the correct
# primary key, and generates it right before inserting the fixture
# into the database.
#
# The generated ID for a given label is constant, so we can discover
# any fixture's ID without loading anything, as long as we know the label.
#
# == Label references for associations (belongs_to, has_one, has_many)
#
# Specifying foreign keys in fixtures can be very fragile, not to
# mention difficult to read. Since ActiveRecord can figure out the ID of
# any fixture from its label, you can specify FK's by label instead of ID.
#
# === belongs_to
#
# Let's break out some more monkeys and pirates.
#
# ### in pirates.yml
#
# reginald:
# id: 1
# name: Reginald the Pirate
# monkey_id: 1
#
# ### in monkeys.yml
#
# george:
# id: 1
# name: George the Monkey
# pirate_id: 1
#
# Add a few more monkeys and pirates and break this into multiple files,
# and it gets pretty hard to keep track of what's going on. Let's
# use labels instead of ID's:
#
# ### in pirates.yml
#
# reginald:
# name: Reginald the Pirate
# monkey: george
#
# ### in monkeys.yml
#
# george:
# name: George the Monkey
# pirate: reginald
#
# Pow! All is made clear. ActiveRecord reflects on the fixture's model class,
# finds all the +belongs_to+ associations, and allows you to specify
# a target *label* for the *association* (monkey: george) rather than
# a target *id* for the *FK* (monkey_id: 1).
#
# ==== Polymorphic belongs_to
#
# Supporting polymorphic relationships is a little bit more complicated, since
# ActiveRecord needs to know what type your association is pointing at. Something
# like this should look familiar:
#
# ### in fruit.rb
#
# belongs_to :eater, :polymorphic => true
#
# ### in fruits.yml
#
# apple:
# id: 1
# name: apple
# eater_id: 1
# eater_type: Monkey
#
# Can we do better? You bet!
#
# apple:
# eater: george (Monkey)
#
# Just provide the polymorphic target type and ActiveRecord will take care of the rest.
#
# === has_and_belongs_to_many
#
# Time to give our monkey some fruit.
#
# ### in monkeys.yml
#
# george:
# id: 1
# name: George the Monkey
# pirate_id: 1
#
# ### in fruits.yml
#
# apple:
# id: 1
# name: apple
#
# orange:
# id: 2
# name: orange
#
# grape:
# id: 3
# name: grape
#
# ### in fruits_monkeys.yml
#
# apple_george:
# fruit_id: 1
# monkey_id: 1
#
# orange_george:
# fruit_id: 2
# monkey_id: 1
#
# grape_george:
# fruit_id: 3
# monkey_id: 1
#
# Let's make the HABTM fixture go away.
#
# ### in monkeys.yml
#
# george:
# name: George the Monkey
# pirate: reginald
# fruits: apple, orange, grape
#
# ### in fruits.yml
#
# apple:
# name: apple
#
# orange:
# name: orange
#
# grape:
# name: grape
#
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
# on George's fixture, but we could've just as easily specified a list
# of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on
# the fixture's model class and discovers the +has_and_belongs_to_many+
# associations.
#
# == Autofilled timestamp columns
#
# If your table/model specifies any of ActiveRecord's
# standard timestamp columns (created_at, created_on, updated_at, updated_on),
# they will automatically be set to Time.now.
#
# If you've set specific values, they'll be left alone.
#
# == Fixture label interpolation
#
# The label of the current fixture is always available as a column value:
#
# geeksomnia:
# name: Geeksomnia's Account
# subdomain: $LABEL
#
# Also, sometimes (like when porting older join table fixtures) you'll need
# to be able to get ahold of the identifier for a given label. ERB
# to the rescue:
#
# george_reginald:
# monkey_id: <%= Fixtures.identify(:reginald) %>
# pirate_id: <%= Fixtures.identify(:george) %>
#
# == Support for YAML defaults
#
# You probably already know how to use YAML to set and reuse defaults in
# your +database.yml+ file,. You can use the same technique in your fixtures:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
#
# first:
# name: Smurf
# <<: *DEFAULTS
#
# second:
# name: Fraggle
# <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class Fixtures < YAML::Omap
DEFAULT_FILTER_RE = /\.ya?ml$/
def self.instantiate_fixtures(object, table_name, fixtures, load_instances=true)
@@all_cached_fixtures = {}
def self.reset_cache(connection = nil)
connection ||= ActiveRecord::Base.connection
@@all_cached_fixtures[connection.object_id] = {}
end
def self.cache_for_connection(connection)
@@all_cached_fixtures[connection.object_id] ||= {}
@@all_cached_fixtures[connection.object_id]
end
def self.fixture_is_cached?(connection, table_name)
cache_for_connection(connection)[table_name]
end
def self.cached_fixtures(connection, keys_to_fetch = nil)
if keys_to_fetch
fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
else
fixtures = cache_for_connection(connection).values
end
fixtures.size > 1 ? fixtures : fixtures.first
end
def self.cache_fixtures(connection, fixtures)
cache_for_connection(connection).update(fixtures.index_by(&:table_name))
end
def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
if load_instances
ActiveRecord::Base.silence do
@ -233,47 +485,63 @@ class Fixtures < YAML::Omap
end
end
def self.instantiate_all_loaded_fixtures(object, load_instances=true)
def self.instantiate_all_loaded_fixtures(object, load_instances = true)
all_loaded_fixtures.each do |table_name, fixtures|
Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
end
end
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
def self.create_fixtures(fixtures_directory, table_names, class_names = {})
table_names = [table_names].flatten.map { |n| n.to_s }
connection = block_given? ? yield : ActiveRecord::Base.connection
ActiveRecord::Base.silence do
fixtures_map = {}
fixtures = table_names.map do |table_name|
fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
end
all_loaded_fixtures.merge! fixtures_map
connection = block_given? ? yield : ActiveRecord::Base.connection
connection.transaction(Thread.current['open_transactions'] == 0) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
# Cap primary key sequences to max(pk).
if connection.respond_to?(:reset_pk_sequence!)
table_names.each do |table_name|
connection.reset_pk_sequence!(table_name)
unless table_names_to_fetch.empty?
ActiveRecord::Base.silence do
connection.disable_referential_integrity do
fixtures_map = {}
fixtures = table_names_to_fetch.map do |table_name|
fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
end
all_loaded_fixtures.update(fixtures_map)
connection.transaction(Thread.current['open_transactions'].to_i == 0) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
# Cap primary key sequences to max(pk).
if connection.respond_to?(:reset_pk_sequence!)
table_names.each do |table_name|
connection.reset_pk_sequence!(table_name)
end
end
end
cache_fixtures(connection, fixtures)
end
end
return fixtures.size > 1 ? fixtures : fixtures.first
end
cached_fixtures(connection, table_names)
end
# Returns a consistent identifier for +label+. This will always
# be a positive integer, and will always be the same for a given
# label, assuming the same OS, platform, and version of Ruby.
def self.identify(label)
label.to_s.hash.abs
end
attr_reader :table_name
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
@class_name = class_name ||
@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)
@ -282,63 +550,132 @@ class Fixtures < YAML::Omap
end
def delete_existing_fixtures
@connection.delete "DELETE FROM #{@table_name}", 'Fixture Delete'
@connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
end
def insert_fixtures
values.each do |fixture|
@connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
delete(assoc("DEFAULTS"))
# track any join tables we need to insert later
habtm_fixtures = Hash.new do |h, habtm|
h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
end
each do |label, fixture|
row = fixture.to_hash
if model_class && model_class < ActiveRecord::Base
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |name|
row[name] = now unless row.key?(name)
end
end
# interpolate the fixture label
row.each do |key, value|
row[key] = label if value == "$LABEL"
end
# generate a primary key if necessary
if has_primary_key_column? && !row.include?(primary_key_name)
row[primary_key_name] = Fixtures.identify(label)
end
# If STI is used, find the correct subclass for association reflection
reflection_class =
if row.include?(inheritance_column_name)
row[inheritance_column_name].constantize rescue model_class
else
model_class
end
reflection_class.reflect_on_all_associations.each do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
if association.options[:polymorphic]
if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
target_type = $1
target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
# support polymorphic belongs_to as "label (Type)"
row[target_type_name] = target_type
end
end
row[fk_name] = Fixtures.identify(value)
end
when :has_and_belongs_to_many
if (targets = row.delete(association.name.to_s))
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
join_fixtures = habtm_fixtures[association]
targets.each do |target|
join_fixtures["#{label}_#{target}"] = Fixture.new(
{ association.primary_key_name => row[primary_key_name],
association.association_foreign_key => Fixtures.identify(target) }, nil)
end
end
end
end
end
@connection.insert_fixture(fixture, @table_name)
end
# insert any HABTM join tables we discovered
habtm_fixtures.values.each do |fixture|
fixture.delete_existing_fixtures
fixture.insert_fixtures
end
end
private
class HabtmFixtures < ::Fixtures #:nodoc:
def read_fixture_files; end
end
def model_class
@model_class ||= @class_name.is_a?(Class) ?
@class_name : @class_name.constantize rescue nil
end
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
def has_primary_key_column?
@has_primary_key_column ||= model_class && primary_key_name &&
model_class.columns.find { |c| c.name == primary_key_name }
end
def timestamp_column_names
@timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
column_names.include?(name)
end
end
def inheritance_column_name
@inheritance_column_name ||= model_class && model_class.inheritance_column
end
def column_names
@column_names ||= @connection.columns(@table_name).collect(&:name)
end
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 = YAML::load(erb_render(yaml_string))
rescue Exception=>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
# If the file is an ordered map, extract its children.
yaml_value =
if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
yaml.value
else
[yaml]
end
yaml_value.each do |fixture|
fixture.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
end
read_yaml_fixture_files
elsif File.file?(csv_file_path)
# CSV fixtures
reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
header = reader.shift
i = 0
reader.each do |row|
data = {}
row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name)
end
elsif File.file?(deprecated_yaml_file_path)
raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}"
read_csv_fixture_files
else
# Standard fixtures
Dir.entries(@fixture_path).each do |file|
@ -350,12 +687,47 @@ class Fixtures < YAML::Omap
end
end
def yaml_file_path
"#{@fixture_path}.yml"
def read_yaml_fixture_files
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 = parse_yaml_string(yaml_string)
# If the file is an ordered map, extract its children.
yaml_value =
if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
yaml.value
else
[yaml]
end
yaml_value.each do |fixture|
fixture.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
end
end
def deprecated_yaml_file_path
"#{@fixture_path}.yaml"
def read_csv_fixture_files
reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
header = reader.shift
i = 0
reader.each do |row|
data = {}
row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name)
end
end
def yaml_file_path
"#{@fixture_path}.yml"
end
def csv_file_path
@ -366,6 +738,12 @@ class Fixtures < YAML::Omap
File.basename(@fixture_path).split(".").first
end
def parse_yaml_string(fixture_content)
YAML::load(erb_render(fixture_content))
rescue => error
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 #{error.class}: #{error}"
end
def erb_render(fixture_content)
ERB.new(fixture_content).result
end
@ -373,11 +751,15 @@ end
class Fixture #:nodoc:
include Enumerable
class FixtureError < StandardError#:nodoc:
class FixtureError < StandardError #:nodoc:
end
class FormatError < FixtureError#:nodoc:
class FormatError < FixtureError #:nodoc:
end
attr_reader :class_name
def initialize(fixture, class_name)
case fixture
when Hash, YAML::Omap
@ -450,36 +832,40 @@ end
module Test #:nodoc:
module Unit #:nodoc:
class TestCase #:nodoc:
cattr_accessor :fixture_path
class_inheritable_accessor :fixture_table_names
class_inheritable_accessor :fixture_class_names
class_inheritable_accessor :use_transactional_fixtures
class_inheritable_accessor :use_instantiated_fixtures # true, false, or :no_instances
class_inheritable_accessor :pre_loaded_fixtures
superclass_delegating_accessor :fixture_path
superclass_delegating_accessor :fixture_table_names
superclass_delegating_accessor :fixture_class_names
superclass_delegating_accessor :use_transactional_fixtures
superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
superclass_delegating_accessor :pre_loaded_fixtures
self.fixture_table_names = []
self.use_transactional_fixtures = false
self.use_instantiated_fixtures = true
self.pre_loaded_fixtures = false
self.fixture_class_names = {}
@@already_loaded_fixtures = {}
self.fixture_class_names = {}
def self.set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names)
end
def self.fixtures(*table_names)
table_names = table_names.flatten.map { |n| n.to_s }
if table_names.first == :all
table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
else
table_names = table_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= table_names
require_fixture_classes(table_names)
setup_fixture_accessors(table_names)
end
def self.require_fixture_classes(table_names=nil)
(table_names || fixture_table_names).each do |table_name|
def self.require_fixture_classes(table_names = nil)
(table_names || fixture_table_names).each do |table_name|
file_name = table_name.to_s
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
begin
@ -490,18 +876,26 @@ module Test #:nodoc:
end
end
def self.setup_fixture_accessors(table_names=nil)
def self.setup_fixture_accessors(table_names = nil)
(table_names || fixture_table_names).each do |table_name|
table_name = table_name.to_s.tr('.','_')
define_method(table_name) do |fixture, *optionals|
force_reload = optionals.shift
@fixture_cache[table_name] ||= Hash.new
@fixture_cache[table_name][fixture] = nil if force_reload
if @loaded_fixtures[table_name][fixture.to_s]
@fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
else
raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
table_name = table_name.to_s.tr('.', '_')
define_method(table_name) do |*fixtures|
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
@fixture_cache[table_name] ||= {}
instances = fixtures.map do |fixture|
@fixture_cache[table_name].delete(fixture) if force_reload
if @loaded_fixtures[table_name][fixture.to_s]
@fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
else
raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
end
end
instances.size == 1 ? instances.first : instances
end
end
end
@ -522,13 +916,15 @@ module Test #:nodoc:
end
def setup_with_fixtures
return if @fixtures_setup
@fixtures_setup = true
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'
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@fixture_cache = Hash.new
@fixture_cache = {}
# Load fixtures once and begin transaction.
if use_transactional_fixtures?
@ -540,9 +936,9 @@ module Test #:nodoc:
end
ActiveRecord::Base.send :increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else
Fixtures.reset_cache
@@already_loaded_fixtures[self.class] = nil
load_fixtures
end
@ -550,12 +946,17 @@ module Test #:nodoc:
# Instantiate fixtures for every test if requested.
instantiate_fixtures if use_instantiated_fixtures
end
alias_method :setup, :setup_with_fixtures
def teardown_with_fixtures
return if @fixtures_teardown
@fixtures_teardown = true
return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
unless use_transactional_fixtures?
Fixtures.reset_cache
end
# Rollback changes if a transaction is active.
if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
ActiveRecord::Base.connection.rollback_db_transaction
@ -563,28 +964,34 @@ module Test #:nodoc:
end
ActiveRecord::Base.verify_active_connections!
end
alias_method :teardown, :teardown_with_fixtures
def self.method_added(method)
return if @__disable_method_added__
@__disable_method_added__ = true
case method.to_s
when 'setup'
unless method_defined?(:setup_without_fixtures)
alias_method :setup_without_fixtures, :setup
define_method(:setup) do
define_method(:full_setup) do
setup_with_fixtures
setup_without_fixtures
end
end
alias_method :setup, :full_setup
when 'teardown'
unless method_defined?(:teardown_without_fixtures)
alias_method :teardown_without_fixtures, :teardown
define_method(:teardown) do
define_method(:full_teardown) do
teardown_without_fixtures
teardown_with_fixtures
end
end
alias_method :teardown, :full_teardown
end
@__disable_method_added__ = false
end
private
@ -623,6 +1030,5 @@ module Test #:nodoc:
use_instantiated_fixtures != :no_instances
end
end
end
end

View file

@ -1,79 +0,0 @@
module ActiveRecord
# Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
# record increments the lock_version column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a StaleObjectError if the first was also updated. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
#
# p1.first_name = "Michael"
# p1.save
#
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
# You must ensure that your database schema defaults the lock_version column to 0.
#
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
# To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
# This method uses the same syntax as <tt>set_table_name</tt>
module Locking
def self.append_features(base) #:nodoc:
super
base.class_eval do
alias_method :update_without_lock, :update
alias_method :update, :update_with_lock
end
end
def update_with_lock #:nodoc:
return update_without_lock unless locking_enabled?
lock_col = self.class.locking_column
previous_value = send(lock_col)
send(lock_col + '=', previous_value + 1)
affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
UPDATE #{self.class.table_name}
SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
WHERE #{self.class.primary_key} = #{quote(id)}
AND #{lock_col} = #{quote(previous_value)}
end_sql
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
end
return true
end
end
class Base
@@lock_optimistically = true
cattr_accessor :lock_optimistically
def locking_enabled? #:nodoc:
lock_optimistically && respond_to?(self.class.locking_column)
end
class << self
def set_locking_column(value = nil, &block)
define_attr_method :locking_column, value, &block
end
def locking_column #:nodoc:
reset_locking_column
end
def reset_locking_column #:nodoc:
default = 'lock_version'
set_locking_column(default)
default
end
end
end
end

View file

@ -1,15 +1,25 @@
module ActiveRecord
module Locking
# == What is Optimistic Locking
#
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
# conflicts with the data. It does this by checking whether another process has made changes to a record since
# it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
#
# Check out ActiveRecord::Locking::Pessimistic for an alternative.
#
# == Usage
#
# Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
# record increments the lock_version column and the locking facilities ensure that records instantiated twice
# will let the last one saved raise a StaleObjectError if the first was also updated. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
#
#
# p1.first_name = "Michael"
# p1.save
#
#
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
@ -23,7 +33,6 @@ module ActiveRecord
# This method uses the same syntax as <tt>set_table_name</tt>
module Optimistic
def self.included(base) #:nodoc:
super
base.extend ClassMethods
base.cattr_accessor :lock_optimistically, :instance_writer => false
@ -31,55 +40,77 @@ module ActiveRecord
base.alias_method_chain :update, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
alias_method :locking_column=, :set_locking_column
end
end
def locking_enabled? #:nodoc:
lock_optimistically && respond_to?(self.class.locking_column)
self.class.locking_enabled?
end
def attributes_from_column_definition_with_lock
result = attributes_from_column_definition_without_lock
# If the locking column has no default value set,
# start the lock version at zero. Note we can't use
# locking_enabled? at this point as @attributes may
# not have been initialized yet
if lock_optimistically && result.include?(self.class.locking_column)
result[self.class.locking_column] ||= 0
end
return result
end
private
def attributes_from_column_definition_with_lock
result = attributes_from_column_definition_without_lock
def update_with_lock #:nodoc:
return update_without_lock unless locking_enabled?
# If the locking column has no default value set,
# start the lock version at zero. Note we can't use
# locking_enabled? at this point as @attributes may
# not have been initialized yet
lock_col = self.class.locking_column
previous_value = send(lock_col)
send(lock_col + '=', previous_value + 1)
if lock_optimistically && result.include?(self.class.locking_column)
result[self.class.locking_column] ||= 0
end
affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
UPDATE #{self.class.table_name}
SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))}
WHERE #{self.class.primary_key} = #{quote_value(id)}
AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
end_sql
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
return result
end
return true
end
def update_with_lock #:nodoc:
return update_without_lock unless locking_enabled?
lock_col = self.class.locking_column
previous_value = send(lock_col).to_i
send(lock_col + '=', previous_value + 1)
begin
affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
UPDATE #{self.class.table_name}
SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false))}
WHERE #{self.class.primary_key} = #{quote_value(id)}
AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
end_sql
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
end
affected_rows
# If something went wrong, revert the version.
rescue Exception
send(lock_col + '=', previous_value)
raise
end
end
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
def self.extended(base)
class <<base
alias_method_chain :update_counters, :lock
end
end
# Is optimistic locking enabled for this table? Returns true if the
# #lock_optimistically flag is set to true (which it is, by default)
# and the table includes the #locking_column column (defaults to
# lock_version).
def locking_enabled?
lock_optimistically && columns_hash[locking_column]
end
# Set the column to use for optimistic locking. Defaults to lock_version.
def set_locking_column(value = nil, &block)
define_attr_method :locking_column, value, &block
@ -100,6 +131,13 @@ module ActiveRecord
def reset_locking_column
set_locking_column DEFAULT_LOCKING_COLUMN
end
# make sure the lock version column gets updated when counters are
# updated.
def update_counters_with_lock(id, counters)
counters = counters.merge(locking_column => 1) if locking_enabled?
update_counters_without_lock(id, counters)
end
end
end
end

View file

@ -1,13 +1,19 @@
module ActiveRecord
class IrreversibleMigration < ActiveRecordError#:nodoc:
end
class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
class IllegalMigrationNameError < ActiveRecordError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
# Migrations can manage the evolution of a schema used by several physical databases. It's a solution
# to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
# push that change to other developers and to the production server. With migrations, you can describe the transformations
@ -26,9 +32,9 @@ module ActiveRecord
# end
# end
#
# This migration will add a boolean flag to the accounts table and remove it again, if you're backing out of the migration.
# This migration will add a boolean flag to the accounts table and remove it if you're backing out of the migration.
# It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
# or remove the migration. These methods can consist of both the migration specific methods, like add_column and remove_column,
# or remove the migration. These methods can consist of both the migration specific methods like add_column and remove_column,
# but may also contain regular Ruby code for generating data needed for the transformations.
#
# Example of a more complex migration that also needs to initialize data:
@ -36,11 +42,11 @@ module ActiveRecord
# class AddSystemSettings < ActiveRecord::Migration
# def self.up
# create_table :system_settings do |t|
# t.column :name, :string
# t.column :label, :string
# t.column :value, :text
# t.column :type, :string
# t.column :position, :integer
# t.string :name
# t.string :label
# t.text :value
# t.string :type
# t.integer :position
# end
#
# SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
@ -72,13 +78,14 @@ module ActiveRecord
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
# parameters as add_column.
# * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, index_type, index_name)</tt>: Add a new index with the name of the column, or +index_name+ (if specified) on the column(s). Specify an optional +index_type+ (e.g. UNIQUE).
# * <tt>remove_index(table_name, index_name)</tt>: Remove the index specified by +index_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index with the name of the column. Other options include
# :name and :unique (e.g. { :name => "users_name_index", :unique => true }).
# * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified by +index_name+.
#
# == Irreversible transformations
#
# Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
# an <tt>IrreversibleMigration</tt> exception in their +down+ method.
# an <tt>ActiveRecord::IrreversibleMigration</tt> exception in their +down+ method.
#
# == Running migrations from within Rails
#
@ -87,18 +94,18 @@ module ActiveRecord
# To generate a new migration, use <tt>script/generate migration MyNewMigration</tt>
# where MyNewMigration is the name of your migration. The generator will
# create a file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
# directory, where <tt>nnn</tt> is the next largest migration number.
# directory where <tt>nnn</tt> is the next largest migration number.
# You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
# n MyNewMigration.
# MyNewMigration.
#
# To run migrations against the currently configured database, use
# <tt>rake migrate</tt>. This will update the database by running all of the
# <tt>rake db:migrate</tt>. This will update the database by running all of the
# pending migrations, creating the <tt>schema_info</tt> table if missing.
#
# To roll the database back to a previous migration version, use
# <tt>rake migrate VERSION=X</tt> where <tt>X</tt> is the version to which
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
# you wish to downgrade. If any of the migrations throw an
# <tt>IrreversibleMigration</tt> exception, that step will fail and you'll
# <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
# have some manual work to do.
#
# == Database support
@ -117,7 +124,7 @@ module ActiveRecord
#
# def self.down
# # not much we can do to restore deleted data
# raise IrreversibleMigration
# raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
# end
# end
#
@ -191,11 +198,11 @@ module ActiveRecord
cattr_accessor :verbose
class << self
def up_using_benchmarks #:nodoc:
def up_with_benchmarks #:nodoc:
migrate(:up)
end
def down_using_benchmarks #:nodoc:
def down_with_benchmarks #:nodoc:
migrate(:down)
end
@ -207,15 +214,15 @@ module ActiveRecord
when :up then announce "migrating"
when :down then announce "reverting"
end
result = nil
time = Benchmark.measure { result = send("real_#{direction}") }
time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
case direction
when :up then announce "migrated (%.4fs)" % time.real; write
when :down then announce "reverted (%.4fs)" % time.real; write
end
result
end
@ -224,15 +231,14 @@ module ActiveRecord
# it is safe for the call to proceed.
def singleton_method_added(sym) #:nodoc:
return if @ignore_new_methods
begin
@ignore_new_methods = true
case sym
when :up, :down
klass = (class << self; self; end)
klass.send(:alias_method, "real_#{sym}", sym)
klass.send(:alias_method, sym, "#{sym}_using_benchmarks")
klass.send(:alias_method_chain, sym, "benchmarks")
end
ensure
@ignore_new_methods = false
@ -244,7 +250,7 @@ module ActiveRecord
end
def announce(message)
text = "#{name}: #{message}"
text = "#{@version} #{name}: #{message}"
length = [0, 75 - text.length].max
write "== %s %s" % [text, "=" * length]
end
@ -258,20 +264,24 @@ module ActiveRecord
result = nil
time = Benchmark.measure { result = yield }
say "%.4fs" % time.real, :subitem
say("#{result} rows", :subitem) if result.is_a?(Integer)
result
end
def suppress_messages
save = verbose
self.verbose = false
save, self.verbose = verbose, false
yield
ensure
self.verbose = save
end
def method_missing(method, *arguments, &block)
say_with_time "#{method}(#{arguments.map { |a| a.inspect }.join(", ")})" do
arguments[0] = Migrator.proper_table_name(arguments.first) unless arguments.empty? || method == :execute
arg_list = arguments.map(&:inspect) * ', '
say_with_time "#{method}(#{arg_list})" do
unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first)
end
ActiveRecord::Base.connection.send(method, *arguments, &block)
end
end
@ -292,30 +302,29 @@ module ActiveRecord
return # You're on the right version
end
end
def up(migrations_path, target_version = nil)
self.new(:up, migrations_path, target_version).migrate
end
def down(migrations_path, target_version = nil)
self.new(:down, migrations_path, target_version).migrate
end
def schema_info_table_name
Base.table_name_prefix + "schema_info" + Base.table_name_suffix
end
def current_version
(Base.connection.select_one("SELECT version FROM #{schema_info_table_name}") || {"version" => 0})["version"].to_i
Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").to_i
end
def proper_table_name(name)
# Use the ActiveRecord objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
end
end
def initialize(direction, migrations_path, target_version = nil)
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
@ -327,66 +336,80 @@ module ActiveRecord
end
def migrate
migration_classes.each do |(version, migration_class)|
Base.logger.info("Reached target version: #{@target_version}") and break if reached_target_version?(version)
next if irrelevant_migration?(version)
migration_classes.each do |migration_class|
if reached_target_version?(migration_class.version)
Base.logger.info("Reached target version: #{@target_version}")
break
end
Base.logger.info "Migrating to #{migration_class} (#{version})"
next if irrelevant_migration?(migration_class.version)
Base.logger.info "Migrating to #{migration_class} (#{migration_class.version})"
migration_class.migrate(@direction)
set_schema_version(version)
set_schema_version(migration_class.version)
end
end
def pending_migrations
migration_classes.select { |m| m.version > current_version }
end
private
def migration_classes
migrations = migration_files.inject([]) do |migrations, migration_file|
load(migration_file)
version, name = migration_version_and_name(migration_file)
assert_unique_migration_version(migrations, version.to_i)
migrations << [ version.to_i, migration_class(name) ]
migrations << migration_class(name, version.to_i)
end
down? ? migrations.sort.reverse : migrations.sort
sorted = migrations.sort_by { |m| m.version }
down? ? sorted.reverse : sorted
end
def assert_unique_migration_version(migrations, version)
if !migrations.empty? && migrations.transpose.first.include?(version)
if !migrations.empty? && migrations.find { |m| m.version == version }
raise DuplicateMigrationVersionError.new(version)
end
end
def migration_files
files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f|
migration_version_and_name(f).first.to_i
m = migration_version_and_name(f)
raise IllegalMigrationNameError.new(f) unless m
m.first.to_i
end
down? ? files.reverse : files
end
def migration_class(migration_name)
migration_name.camelize.constantize
def migration_class(migration_name, version)
klass = migration_name.camelize.constantize
class << klass; attr_accessor :version end
klass.version = version
klass
end
def migration_version_and_name(migration_file)
return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
end
def set_schema_version(version)
Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}")
end
def up?
@direction == :up
end
def down?
@direction == :down
end
def reached_target_version?(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)
(up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
end

View file

@ -84,10 +84,11 @@ module ActiveRecord
#
# Observers will by default be mapped to the class with which they share a name. So CommentObserver will
# be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
# differently than the class you're interested in observing, you can use the Observer.observe class method:
# differently than the class you're interested in observing, you can use the Observer.observe class method which takes
# either the concrete class (Product) or a symbol for that class (:product):
#
# class AuditObserver < ActiveRecord::Observer
# observe Account
# observe :account
#
# def after_update(account)
# AuditTrail.new(account, "UPDATED")
@ -97,7 +98,7 @@ module ActiveRecord
# If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
#
# class AuditObserver < ActiveRecord::Observer
# observe Account, Balance
# observe :account, :balance
#
# def after_update(record)
# AuditTrail.new(record, "UPDATED")
@ -127,20 +128,22 @@ module ActiveRecord
class Observer
include Singleton
# Observer subclasses should be reloaded by the dispatcher in Rails
# when Dependencies.mechanism = :load.
include Reloadable::Deprecated
class << self
# Attaches the observer to the supplied model classes.
def observe(*models)
models.flatten!
models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
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
if observed_class_name = name.scan(/(.*)Observer/)[0]
observed_class_name[0].constantize
else
nil
end
end
end
@ -163,11 +166,11 @@ module ActiveRecord
protected
def observed_classes
Set.new([self.class.observed_class].flatten)
Set.new([self.class.observed_class].compact.flatten)
end
def observed_subclasses
observed_classes.sum(&:subclasses)
observed_classes.collect(&:subclasses).flatten
end
def add_observer!(klass)

View file

@ -1,64 +1,21 @@
module ActiveRecord
class QueryCache #:nodoc:
def initialize(connection)
@connection = connection
@query_cache = {}
end
def clear_query_cache
@query_cache = {}
end
def select_all(sql, name = nil)
(@query_cache[sql] ||= @connection.select_all(sql, name)).dup
end
def select_one(sql, name = nil)
@query_cache[sql] ||= @connection.select_one(sql, name)
end
def columns(table_name, name = nil)
@query_cache["SHOW FIELDS FROM #{table_name}"] ||= @connection.columns(table_name, name)
end
def insert(sql, name = nil, pk = nil, id_value = nil)
clear_query_cache
@connection.insert(sql, name, pk, id_value)
end
def update(sql, name = nil)
clear_query_cache
@connection.update(sql, name)
end
def delete(sql, name = nil)
clear_query_cache
@connection.delete(sql, name)
end
private
def method_missing(method, *arguments, &proc)
@connection.send(method, *arguments, &proc)
end
end
class Base
# Set the connection for the class with caching on
class << self
alias_method :connection_without_query_cache=, :connection=
def connection=(spec)
if spec.is_a?(ConnectionSpecification) and spec.config[:query_cache]
spec = QueryCache.new(self.send(spec.adapter_method, spec.config))
end
self.connection_without_query_cache = spec
module QueryCache
# Enable the query cache within the block if Active Record is configured.
def cache(&block)
if ActiveRecord::Base.configurations.blank?
yield
else
connection.cache(&block)
end
end
end
class AbstractAdapter #:nodoc:
# Stub method to be able to treat the connection the same whether the query cache has been turned on or not
def clear_query_cache
# Disable the query cache within the block if Active Record is configured.
def uncached(&block)
if ActiveRecord::Base.configurations.blank?
yield
else
connection.uncached(&block)
end
end
end
end

View file

@ -44,7 +44,7 @@ module ActiveRecord
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
# Returns an array of AssociationReflection objects for all the associations 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:
#
@ -56,7 +56,7 @@ module ActiveRecord
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
# Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
# Returns the AssociationReflection object for the named +association+ (use the symbol). Example:
#
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
@ -71,6 +71,7 @@ module ActiveRecord
# those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
attr_reader :active_record
def initialize(macro, name, options, active_record)
@macro, @name, @options, @active_record = macro, name, options, active_record
end
@ -81,7 +82,7 @@ module ActiveRecord
@name
end
# Returns the name of the macro, so it would return :composed_of for
# Returns the type of the macro, so it would return :composed_of for
# "composed_of :balance, :class_name => 'Money'" or :has_many for "has_many :clients".
def macro
@macro
@ -93,30 +94,29 @@ module ActiveRecord
@options
end
# Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" would return the Money class and
# "has_many :clients" would return the Client class.
def klass() end
# Returns the class for the macro, so "composed_of :balance, :class_name => 'Money'" returns the Money class and
# "has_many :clients" returns the Client class.
def klass
@klass ||= class_name.constantize
end
def class_name
@class_name ||= name_to_class_name(name.id2name)
@class_name ||= options[:class_name] || derive_class_name
end
def ==(other_aggregation)
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
end
private
def derive_class_name
name.to_s.camelize
end
end
# Holds all the meta-data about an aggregation as it was specified in the Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
def klass
@klass ||= Object.const_get(options[:class_name] || class_name)
end
private
def name_to_class_name(name)
name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
end
end
# Holds all the meta-data about an association as it was specified in the Active Record class.
@ -130,17 +130,9 @@ module ActiveRecord
end
def primary_key_name
return @primary_key_name if @primary_key_name
case
when macro == :belongs_to
@primary_key_name = options[:foreign_key] || class_name.foreign_key
when options[:as]
@primary_key_name = options[:foreign_key] || "#{options[:as]}_id"
else
@primary_key_name = options[:foreign_key] || active_record.name.foreign_key
end
@primary_key_name ||= options[:foreign_key] || derive_primary_key_name
end
def association_foreign_key
@association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
end
@ -202,19 +194,24 @@ module ActiveRecord
end
private
def name_to_class_name(name)
if name =~ /::/
name
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
if through_reflection
options[:source_type] || source_reflection.class_name
else
if options[:class_name]
options[:class_name]
elsif through_reflection # get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
else
class_name = name.to_s.camelize
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
class_name
end
class_name = name.to_s.camelize
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
class_name
end
end
def derive_primary_key_name
if macro == :belongs_to
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.name.foreign_key
end
end
end

View file

@ -8,16 +8,16 @@ module ActiveRecord
#
# ActiveRecord::Schema.define do
# create_table :authors do |t|
# t.column :name, :string, :null => false
# t.string :name, :null => false
# end
#
# add_index :authors, :name, :unique
#
# create_table :posts do |t|
# t.column :author_id, :integer, :null => false
# t.column :subject, :string
# t.column :body, :text
# t.column :private, :boolean, :default => false
# t.integer :author_id, :null => false
# t.string :subject
# t.text :body
# t.boolean :private, :default => false
# end
#
# add_index :posts, :author_id

View file

@ -37,9 +37,16 @@ module ActiveRecord
define_params = @info ? ":version => #{@info['version']}" : ""
stream.puts <<HEADER
# This file is autogenerated. Instead of editing this file, please use the
# migrations feature of ActiveRecord to incrementally modify your database, and
# This file is auto-generated from the current state of the database. Instead of editing this file,
# please use the migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
#
# Note that this schema.rb definition is the authoritative source for your database schema. If you need
# to create the application database on another system, you should be using db:schema:load, not running
# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
# you'll amass, the slower it'll run and the greater likelihood for issues).
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(#{define_params}) do
@ -54,8 +61,8 @@ HEADER
@connection.tables.sort.each do |tbl|
next if ["schema_info", ignore_tables].flatten.any? do |ignored|
case ignored
when String: tbl == ignored
when Regexp: tbl =~ ignored
when String; tbl == ignored
when Regexp; tbl =~ ignored
else
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
end
@ -89,22 +96,37 @@ HEADER
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
next if column.name == pk
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[:name] = column.name.inspect
spec[:type] = column.type.to_s
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[: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 }
# find all migration keys used in this table
keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
# figure out the lengths for each column based on above keys
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
format_string = lengths.map{ |len| "%-#{len}s" }.join("")
# the string we're going to sprintf our values against, with standardized column widths
format_string = lengths.map{ |len| "%-#{len}s" }
# find the max length for the 'type' column, which is special
type_length = column_specs.map{ |column| column[:type].length }.max
# add column type definition to our format string
format_string.unshift " t.%-#{type_length}s "
format_string *= ''
column_specs.each do |colspec|
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
tbl.print " t.column "
values.unshift colspec[:type]
tbl.print((format_string % values).gsub(/,\s*$/, ''))
tbl.puts
end

View file

@ -0,0 +1,98 @@
module ActiveRecord #:nodoc:
module Serialization
class Serializer #:nodoc:
attr_reader :options
def initialize(record, options = {})
@record, @options = record, options.dup
end
# To replicate the behavior in ActiveRecord#attributes,
# :except takes precedence over :only. If :only is not set
# for a N level model but is set for the N+1 level models,
# then because :except is set to a default value, the second
# level model can have both :except and :only set. So if
# :only is set, always delete :except.
def serializable_attribute_names
attribute_names = @record.attribute_names
if options[:only]
options.delete(:except)
attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
else
options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
end
attribute_names
end
def serializable_method_names
Array(options[:methods]).inject([]) do |method_attributes, name|
method_attributes << name if @record.respond_to?(name.to_s)
method_attributes
end
end
def serializable_names
serializable_attribute_names + serializable_method_names
end
# Add associations specified via the :includes option.
# Expects a block that takes as arguments:
# +association+ - name of the association
# +records+ - the association record(s) to be serialized
# +opts+ - options for the association records
def add_includes(&block)
if include_associations = options.delete(:include)
base_only_or_except = { :except => options[:except],
:only => options[:only] }
include_has_options = include_associations.is_a?(Hash)
associations = include_has_options ? include_associations.keys : Array(include_associations)
for association in associations
records = case @record.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
@record.send(association).to_a
when :has_one, :belongs_to
@record.send(association)
end
unless records.nil?
association_options = include_has_options ? include_associations[association] : base_only_or_except
opts = options.merge(association_options)
yield(association, records, opts)
end
end
options[:include] = include_associations
end
end
def serializable_record
returning(serializable_record = {}) do
serializable_names.each { |name| serializable_record[name] = @record.send(name) }
add_includes do |association, records, opts|
if records.is_a?(Enumerable)
serializable_record[association] = records.collect { |r| self.class.new(r, opts).serializable_record }
else
serializable_record[association] = self.class.new(records, opts).serializable_record
end
end
end
end
def serialize
# overwrite to implement
end
def to_s(&block)
serialize(&block)
end
end
end
end
require 'active_record/serializers/xml_serializer'
require 'active_record/serializers/json_serializer'

View file

@ -0,0 +1,71 @@
module ActiveRecord #:nodoc:
module Serialization
# Returns a JSON string representing the model. Some configuration is
# available through +options+.
#
# Without any +options+, the returned JSON string will include all
# the model's attributes. For example:
#
# konata = User.find(1)
# konata.to_json
#
# {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The :only and :except options can be used to limit the attributes
# included, and work similar to the #attributes method. For example:
#
# konata.to_json(:only => [ :id, :name ])
#
# {"id": 1, "name": "Konata Izumi"}
#
# konata.to_json(:except => [ :id, :created_at, :age ])
#
# {"name": "Konata Izumi", "awesome": true}
#
# To include any methods on the model, use :methods.
#
# konata.to_json(:methods => :permalink)
#
# {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "permalink": "1-konata-izumi"}
#
# To include associations, use :include.
#
# konata.to_json(:include => :posts)
#
# {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
#
# 2nd level and higher order associations work as well:
#
# konata.to_json(:include => { :posts => {
# :include => { :comments => {
# :only => :body } },
# :only => :title } })
#
# {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
# "title": "Welcome to the weblog"},
# {"comments": [{"body": "Don't think too hard"}],
# "title": "So I was thinking"}]}
def to_json(options = {})
JsonSerializer.new(self, options).to_s
end
def from_json(json)
self.attributes = ActiveSupport::JSON.decode(json)
self
end
class JsonSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
def serialize
serializable_record.to_json
end
end
end
end

View file

@ -1,12 +1,12 @@
module ActiveRecord #:nodoc:
module XmlSerialization
module Serialization
# Builds an XML document to represent the model. Some configuration is
# availble through +options+, however more complicated cases should use
# available through +options+, however more complicated cases should
# override ActiveRecord's to_xml.
#
# By default the generated XML document will include the processing
# 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>
@ -27,7 +27,7 @@ module ActiveRecord #:nodoc:
# :except options are the same as for the #attributes method.
# The default is to dasherize all column names, to disable this,
# set :dasherize to false. To not have the column type included
# in the XML output, set :skip_types to false.
# in the XML output, set :skip_types to true.
#
# For instance:
#
@ -42,7 +42,7 @@ module ActiveRecord #:nodoc:
# <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 ]
@ -52,7 +52,7 @@ module ActiveRecord #:nodoc:
# <id type="integer">1</id>
# <rating type="integer">1</rating>
# <name>37signals</name>
# <clients>
# <clients type="array">
# <client>
# <rating type="integer">1</rating>
# <name>Summit</name>
@ -90,7 +90,24 @@ module ActiveRecord #:nodoc:
# <abc>def</abc>
# </firm>
#
# You may override the to_xml method in your ActiveRecord::Base
# Alternatively, you can also just yield the builder object as part of the to_xml call:
#
# firm.to_xml do |xml|
# xml.creator do
# xml.first_name "David"
# xml.last_name "Heinemeier Hansson"
# end
# end
#
# <firm>
# # ... normal attributes as shown above ...
# <creator>
# <first_name>David</first_name>
# <last_name>Heinemeier Hansson</last_name>
# </creator>
# </firm>
#
# You can override the to_xml method in your ActiveRecord::Base
# subclasses if you need to. The general form of doing this is
#
# class IHaveMyOwnXML < ActiveRecord::Base
@ -103,18 +120,18 @@ module ActiveRecord #:nodoc:
# end
# end
# end
def to_xml(options = {})
XmlSerializer.new(self, options).to_s
def to_xml(options = {}, &block)
serializer = XmlSerializer.new(self, options)
block_given? ? serializer.to_s(&block) : serializer.to_s
end
def from_xml(xml)
self.attributes = Hash.from_xml(xml).values.first
self
end
end
class XmlSerializer #:nodoc:
attr_reader :options
def initialize(record, options = {})
@record, @options = record, options.dup
end
class XmlSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
def builder
@builder ||= begin
options[:indent] ||= 2
@ -124,7 +141,7 @@ module ActiveRecord #:nodoc:
builder.instruct!
options[:skip_instruct] = true
end
builder
end
end
@ -133,7 +150,7 @@ module ActiveRecord #:nodoc:
root = (options[:root] || @record.class.to_s.underscore).to_s
dasherize? ? root.dasherize : root
end
def dasherize?
!options.has_key?(:dasherize) || options[:dasherize]
end
@ -146,64 +163,22 @@ module ActiveRecord #:nodoc:
# level model can have both :except and :only set. So if
# :only is set, always delete :except.
def serializable_attributes
attribute_names = @record.attribute_names
if options[:only]
options.delete(:except)
attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
else
options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
end
attribute_names.collect { |name| Attribute.new(name, @record) }
serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
end
def serializable_method_attributes
Array(options[:methods]).collect { |name| MethodAttribute.new(name.to_s, @record) }
Array(options[:methods]).inject([]) do |method_attributes, name|
method_attributes << MethodAttribute.new(name.to_s, @record) if @record.respond_to?(name.to_s)
method_attributes
end
end
def add_attributes
(serializable_attributes + serializable_method_attributes).each do |attribute|
add_tag(attribute)
end
end
def add_includes
if include_associations = options.delete(:include)
root_only_or_except = { :except => options[:except],
:only => options[:only] }
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
opts = options.merge(association_options)
case @record.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
records = @record.send(association).to_a
unless records.empty?
tag = records.first.class.to_s.underscore.pluralize
tag = tag.dasherize if dasherize?
builder.tag!(tag) do
records.each { |r| r.to_xml(opts.merge(:root => association.to_s.singularize)) }
end
end
when :has_one, :belongs_to
if record = @record.send(association)
record.to_xml(opts.merge(:root => association))
end
end
end
options[:include] = include_associations
end
end
def add_procs
if procs = options.delete(:procs)
[ *procs ].each do |proc|
@ -212,36 +187,64 @@ module ActiveRecord #:nodoc:
end
end
def add_tag(attribute)
builder.tag!(
dasherize? ? attribute.name.dasherize : attribute.name,
attribute.value.to_s,
dasherize? ? attribute.name.dasherize : attribute.name,
attribute.value.to_s,
attribute.decorations(!options[:skip_types])
)
end
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = association.to_s
tag = tag.dasherize if dasherize?
if records.empty?
builder.tag!(tag, :type => :array)
else
builder.tag!(tag, :type => :array) do
association_name = association.to_s.singularize
records.each do |record|
record.to_xml opts.merge(
:root => association_name,
:type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
)
end
end
end
else
if record = @record.send(association)
record.to_xml(opts.merge(:root => association))
end
end
end
def serialize
args = [root]
if options[:namespace]
args << {:xmlns=>options[:namespace]}
end
if options[:type]
args << {:type=>options[:type]}
end
builder.tag!(*args) do
add_attributes
add_includes
procs = options.delete(:procs)
add_includes { |association, records, opts| add_associations(association, records, opts) }
options[:procs] = procs
add_procs
yield builder if block_given?
end
end
alias_method :to_s, :serialize
end
class Attribute #:nodoc:
attr_reader :name, :value, :type
def initialize(name, record)
@name, @record = name, record
@type = compute_type
@value = compute_value
end
@ -258,24 +261,28 @@ module ActiveRecord #:nodoc:
def needs_encoding?
![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
end
def decorations(include_types = true)
decorations = {}
if type == :binary
decorations[:encoding] = 'base64'
end
if include_types && type != :string
decorations[:type] = type
end
if value.nil?
decorations[:nil] = true
end
decorations
end
protected
def compute_type
type = @record.class.columns_hash[name].type
type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type
case type
when :text
@ -286,10 +293,10 @@ module ActiveRecord #:nodoc:
type
end
end
def compute_value
value = @record.send(name)
if formatter = Hash::XML_FORMATTING[type.to_s]
value ? formatter.call(value) : nil
else

View file

@ -1,6 +1,6 @@
module ActiveRecord
# Active Record automatically timestamps create and update if the table has fields
# created_at/created_on or updated_at/updated_on.
# Active Record automatically timestamps create and update operations if the table has fields
# named created_at/created_on or updated_at/updated_on.
#
# Timestamping can be turned off by setting
# <tt>ActiveRecord::Base.record_timestamps = false</tt>
@ -9,34 +9,33 @@ module ActiveRecord
# <tt>ActiveRecord::Base.default_timezone = :utc</tt>
module Timestamp
def self.included(base) #:nodoc:
super
base.alias_method_chain :create, :timestamps
base.alias_method_chain :update, :timestamps
base.cattr_accessor :record_timestamps, :instance_writer => false
base.class_inheritable_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
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?
private
def create_with_timestamps #:nodoc:
if record_timestamps
t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
write_attribute('updated_at', t) if respond_to?(:updated_at)
write_attribute('updated_on', t) if respond_to?(:updated_on)
write_attribute('updated_at', t) if respond_to?(:updated_at)
write_attribute('updated_on', t) if respond_to?(:updated_on)
end
create_without_timestamps
end
create_without_timestamps
end
def update_with_timestamps #:nodoc:
if record_timestamps
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)
def update_with_timestamps #:nodoc:
if record_timestamps
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
update_without_timestamps
end
end
end

View file

@ -1,5 +1,3 @@
require 'active_record/vendor/simple.rb'
Transaction::Simple.send(:remove_method, :transaction)
require 'thread'
module ActiveRecord
@ -32,6 +30,19 @@ module ActiveRecord
# Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
# that the objects by default will _not_ have their instance data returned to their pre-transactional state.
#
# == Different ActiveRecord classes in a single transaction
#
# Though the transaction class method is called on some ActiveRecord class,
# the objects within the transaction block need not all be instances of
# that class.
# In this example a <tt>Balance</tt> record is transactionally saved even
# though <tt>transaction</tt> is called on the <tt>Account</tt> class:
#
# Account.transaction do
# balance.save!
# account.save!
# end
#
# == Transactions are not distributed across database connections
#
# A transaction acts on a single database connection. If you have
@ -53,52 +64,20 @@ module ActiveRecord
#
# Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
# will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
# depend on or you can raise exceptions in the callbacks to rollback.
#
# == Object-level transactions (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:
#
# Account.transaction(david, mary) do
# david.withdrawal(100)
# mary.deposit(100)
# end
#
# If the transaction fails, David and Mary will be returned to their
# pre-transactional state. No money will have changed hands in neither
# object nor database.
#
# 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.
# depends on or you can raise exceptions in the callbacks to rollback.
#
# == Exception handling
#
# Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
# should be ready to catch those in your application code.
#
# Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
# should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
# trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
module ClassMethods
def transaction(*objects, &block)
def transaction(&block)
previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" }
increment_open_transactions
begin
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)
objects.each { |o| o.commit_transaction }
return result
rescue Exception => object_transaction_rollback
objects.each { |o| o.abort_transaction }
raise
connection.transaction(Thread.current['start_db_transaction'], &block)
ensure
decrement_open_transactions
trap('TERM', previous_handler)
@ -117,8 +96,8 @@ module ActiveRecord
end
end
def transaction(*objects, &block)
self.class.transaction(*objects, &block)
def transaction(&block)
self.class.transaction(&block)
end
def destroy_with_transactions #:nodoc:
@ -126,11 +105,28 @@ module ActiveRecord
end
def save_with_transactions(perform_validation = true) #:nodoc:
transaction { save_without_transactions(perform_validation) }
rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
end
def save_with_transactions! #:nodoc:
transaction { save_without_transactions! }
rollback_active_record_state! { transaction { save_without_transactions! } }
end
# Reset id and @new_record if the transaction rolls back.
def rollback_active_record_state!
id_present = has_attribute?(self.class.primary_key)
previous_id = id
previous_new_record = @new_record
yield
rescue Exception
@new_record = previous_new_record
if id_present
self.id = previous_id
else
@attributes.delete(self.class.primary_key)
@attributes_cache.delete(self.class.primary_key)
end
raise
end
end
end

View file

@ -15,7 +15,7 @@ module ActiveRecord
end
# Active Record validation is reported to and from this object, which is used by Base#save to
# determine whether the object in a valid state to be saved. See usage example in Validations.
# determine whether the object is in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
@ -35,10 +35,17 @@ module ActiveRecord
:too_short => "is too short (minimum is %d characters)",
:wrong_length => "is the wrong length (should be %d characters)",
:taken => "has already been taken",
:not_a_number => "is not a number"
:not_a_number => "is not a number",
:greater_than => "must be greater than %d",
:greater_than_or_equal_to => "must be greater than or equal to %d",
:equal_to => "must be equal to %d",
:less_than => "must be less than %d",
:less_than_or_equal_to => "must be less than or equal to %d",
:odd => "must be odd",
:even => "must be even"
}
# Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
# Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
cattr_accessor :default_error_messages
@ -76,27 +83,33 @@ module ActiveRecord
end
end
# Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
# If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
def add_on_boundary_breaking(attributes, range, too_long_msg = @@default_error_messages[:too_long], too_short_msg = @@default_error_messages[:too_short])
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
add(attr, too_short_msg % range.begin) if value && value.length < range.begin
add(attr, too_long_msg % range.end) if value && value.length > range.end
end
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.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.invalid?(:name) # => true
# company.errors.invalid?(:address) # => false
def invalid?(attribute)
!@errors[attribute.to_s].nil?
end
# * Returns nil, if no errors are associated with the specified +attribute+.
# * Returns the error message, if one error is associated with the specified +attribute+.
# * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
# Returns nil, if no errors are associated with the specified +attribute+.
# Returns the error message, if one error is associated with the specified +attribute+.
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"]
# company.errors.on(:email) # => "can't be blank"
# company.errors.on(:address) # => nil
def on(attribute)
errors = @errors[attribute.to_s]
return nil if errors.nil?
@ -105,23 +118,54 @@ module ActiveRecord
alias :[] :on
# Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
# Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
def on_base
on(:base)
end
# Yields each attribute and associated message per error added.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
# name - is too short (minimum is 5 characters)
# name - can't be blank
# address - can't be blank
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
# through iteration as "First name can't be empty".
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.each_full{|msg| puts msg } # =>
# Name is too short (minimum is 5 characters)
# Name can't be blank
# Address can't be blank
def each_full
full_messages.each { |msg| yield msg }
end
# Returns all the full error messages in an array.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.full_messages # =>
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
def full_messages
full_messages = []
@ -143,22 +187,35 @@ module ActiveRecord
def empty?
@errors.empty?
end
# Removes all the errors that have been added.
# Removes all errors that have been added.
def clear
@errors = {}
end
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
# with this as well.
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
def size
@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.
#
# class Company < ActiveRecord::Base
# validates_presence_of :name, :address, :email
# validates_length_of :name, :in => 5..30
# end
#
# company = Company.create(:address => '123 First St.')
# company.errors.to_xml # =>
# <?xml version="1.0" encoding="UTF-8"?>
# <errors>
# <error>Name is too short (minimum is 5 characters)</error>
# <error>Name can't be blank</error>
# <error>Address can't be blank</error>
# </errors>
def to_xml(options={})
options[:root] ||= "errors"
options[:indent] ||= 2
@ -231,11 +288,42 @@ module ActiveRecord
DEFAULT_VALIDATION_OPTIONS = {
:on => :save,
:allow_nil => false,
:allow_blank => false,
:message => nil
}.freeze
ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
:odd => 'odd?', :even => 'even?' }.freeze
# Adds a validation method or block to the class. This is useful when
# overriding the #validate instance method becomes too unwieldly and
# you're looking for more descriptive declaration of your validations.
#
# This can be done with a symbol pointing to a method:
#
# class Comment < ActiveRecord::Base
# validate :must_be_friends
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# Or with a block which is passed the current record to be validated:
#
# class Comment < ActiveRecord::Base
# validate do |comment|
# comment.must_be_friends
# end
#
# def must_be_friends
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
# end
# end
#
# This usage applies to #validate_on_create and #validate_on_update as well.
def validate(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate, methods)
@ -259,8 +347,8 @@ module ActiveRecord
# whether or not to validate the record. See #validates_each.
def evaluate_condition(condition, record)
case condition
when Symbol: record.send(condition)
when String: eval(condition, binding)
when Symbol; record.send(condition)
when String; eval(condition, record.send(:binding))
else
if condition_block?(condition)
condition.call(record)
@ -285,20 +373,24 @@ module ActiveRecord
# Options:
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>allow_nil</tt> - Skip validation if attribute is nil.
# * <tt>allow_blank</tt> - Skip validation if attribute is blank.
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# 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 : {}
options = attrs.extract_options!.symbolize_keys
attrs = attrs.flatten
# Declare the validation.
send(validation_method(options[:on] || :save)) do |record|
# Don't validate when there is an :if condition and that condition is false
unless options[:if] && !evaluate_condition(options[:if], record)
# Don't validate when there is an :if condition and that condition is false or there is an :unless condition and that condition is true
unless (options[:if] && !evaluate_condition(options[:if], record)) || (options[:unless] && evaluate_condition(options[:unless], record))
attrs.each do |attr|
value = record.send(attr)
next if value.nil? && options[:allow_nil]
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
yield record, attr, value
end
end
@ -317,21 +409,27 @@ module ActiveRecord
# <%= password_field "person", "password" %>
# <%= password_field "person", "password_confirmation" %>
#
# The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
# It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
# is not nil and by default on save.
# The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password.
# To achieve this, the validation adds acccessors to the model for the confirmation attribute. NOTE: This check is performed
# only if +password_confirmation+ is not nil, and by default only on save. To require confirmation, make sure to add a presence
# check for the confirmation attribute:
#
# validates_presence_of :password_confirmation, :if => :password_changed?
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_confirmation_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
attr_accessor *(attr_names.map { |n| "#{n}_confirmation" })
attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
@ -345,22 +443,33 @@ module ActiveRecord
# validates_acceptance_of :eula, :message => "must be abided"
# end
#
# The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
# terms_of_service is not nil and by default on save.
# If the database column does not exist, the terms_of_service attribute is entirely virtual. This check is
# performed only if terms_of_service is not nil and by default on save.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "must be accepted")
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>allow_nil</tt> - Skip validation if attribute is nil. (default is true)
# * <tt>accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
# makes it easy to relate to an HTML checkbox.
# makes it easy to relate to an HTML checkbox. This should be set to 'true' if you are validating a database
# column, since the attribute is typecast from "1" to <tt>true</tt> before validation.
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_acceptance_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
attr_accessor *attr_names
db_cols = begin
column_names
rescue ActiveRecord::StatementInvalid
[]
end
names = attr_names.reject { |name| db_cols.include?(name.to_s) }
attr_accessor(*names)
validates_each(attr_names,configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
@ -374,7 +483,7 @@ module ActiveRecord
# end
#
# 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
@ -383,33 +492,34 @@ module ActiveRecord
# * <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)
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
#
# === Warning
# Validate the presence of the foreign key, not the instance variable itself.
# Do this:
# validate_presence_of :invoice_id
# validates_presence_of :invoice_id
#
# Not this:
# validate_presence_of :invoice
# validates_presence_of :invoice
#
# If you validate the presence of the associated object, you will get
# failures on saves when both the parent object and the child object are
# new.
def validates_presence_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
# can't use validates_each here, because it cannot cope with nonexistent attributes,
# while errors.add_on_empty can
attr_names.each do |attr_name|
send(validation_method(configuration[:on])) do |record|
unless configuration[:if] and not evaluate_condition(configuration[:if], record)
record.errors.add_on_blank(attr_name,configuration[:message])
end
end
end
send(validation_method(configuration[:on])) do |record|
unless (configuration[:if] && !evaluate_condition(configuration[:if], record)) || (configuration[:unless] && evaluate_condition(configuration[:unless], record))
record.errors.add_on_blank(attr_names, configuration[:message])
end
end
end
# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
@ -418,6 +528,7 @@ module ActiveRecord
# validates_length_of :first_name, :maximum=>30
# validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
# validates_length_of :fax, :in => 7..32, :allow_nil => true
# validates_length_of :phone, :in => 7..32, :allow_blank => true
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
# validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
# validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
@ -430,6 +541,7 @@ module ActiveRecord
# * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
# * <tt>in</tt> - A synonym(or alias) for :within
# * <tt>allow_nil</tt> - Attribute may be nil; skip validation.
# * <tt>allow_blank</tt> - Attribute may be blank; skip validation.
#
# * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)")
# * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)")
@ -437,8 +549,11 @@ module ActiveRecord
# * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_length_of(*attrs)
# Merge given options with defaults.
options = {
@ -446,7 +561,7 @@ module ActiveRecord
:too_short => ActiveRecord::Errors.default_error_messages[:too_short],
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
options.update(attrs.extract_options!.symbolize_keys)
# Ensure that one and only one range option is specified.
range_options = ALL_RANGE_OPTIONS & options.keys
@ -507,27 +622,34 @@ module ActiveRecord
# end
#
# It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
# making sure that a teacher can only be on the schedule once per semester for a particular class.
# making sure that a teacher can only be on the schedule once per semester for a particular class.
#
# class TeacherSchedule < ActiveRecord::Base
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
# end
#
# When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
# attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
#
# Because this check is performed outside the database there is still a chance that duplicate values
# will be inserted in two parallel transactions. To guarantee against this you should create a
# unique index on the field. See +create_index+ for more information.
#
# 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>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => 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], :case_sensitive => true }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
validates_each(attr_names,configuration) do |record, attr_name, value|
if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
@ -537,6 +659,7 @@ module ActiveRecord
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)
@ -544,17 +667,32 @@ module ActiveRecord
condition_params << scope_value
end
end
unless record.new_record?
condition_sql << " AND #{record.class.table_name}.#{record.class.primary_key} <> ?"
condition_params << record.send(:id)
end
if record.class.find(:first, :conditions => [condition_sql, *condition_params])
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
# their subclasses, we have to build the hierarchy between self and
# the record's class.
class_hierarchy = [record.class]
while class_hierarchy.first != self
class_hierarchy.insert(0, class_hierarchy.first.superclass)
end
# Now we can work our way down the tree to the first non-abstract
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
if finder_class.find(:first, :conditions => [condition_sql, *condition_params])
record.errors.add(attr_name, configuration[:message])
end
end
end
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
# provided.
@ -572,11 +710,14 @@ module ActiveRecord
# * <tt>with</tt> - The regular expression used to validate the format with (note: must be supplied!)
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
@ -588,27 +729,32 @@ module ActiveRecord
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
# validates_inclusion_of :gender, :in=>%w( m f ), :message=>"woah! what are you then!??!!"
# validates_inclusion_of :age, :in=>0..99
# validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %s is not included in the list"
# end
#
# Configuration options:
# * <tt>in</tt> - An enumerable object of available items
# * <tt>message</tt> - Specifies a customer error message (default is: "is not included in the list")
# * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
# * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_inclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless enum.include?(value)
record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
end
end
@ -617,25 +763,30 @@ module ActiveRecord
# class Person < ActiveRecord::Base
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %s is not allowed"
# end
#
# Configuration options:
# * <tt>in</tt> - An enumerable object of items that the value shouldn't be part of
# * <tt>message</tt> - Specifies a customer error message (default is: "is reserved")
# * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
# * <tt>allow_blank</tt> - If set to true, skips this validation if the attribute is blank (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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_exclusion_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) if enum.include?(value)
record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
end
end
@ -662,17 +813,21 @@ module ActiveRecord
# is both present and guaranteed to be valid, you also need to use validates_presence_of.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "is invalid")
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration.update(attr_names.extract_options!)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) unless
(value.is_a?(Array) ? value : [value]).all? { |r| r.nil? or r.valid? }
(value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
end
end
@ -689,40 +844,67 @@ module ActiveRecord
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
# * <tt>allow_nil</tt> Skip validation if attribute is nil (default is false). Notice that for fixnum and float columns empty strings are converted to nil
# * <tt>greater_than</tt> Specifies the value must be greater than the supplied value
# * <tt>greater_than_or_equal_to</tt> Specifies the value must be greater than or equal the supplied value
# * <tt>equal_to</tt> Specifies the value must be equal to the supplied value
# * <tt>less_than</tt> Specifies the value must be less than the supplied value
# * <tt>less_than_or_equal_to</tt> Specifies the value must be less than or equal the supplied value
# * <tt>odd</tt> Specifies the value must be an odd number
# * <tt>even</tt> Specifies the value must be an even number
# * <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.
# 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.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_numericality_of(*attr_names)
configuration = { :message => ActiveRecord::Errors.default_error_messages[:not_a_number], :on => :save,
:only_integer => false, :allow_nil => false }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
configuration = { :on => :save, :only_integer => false, :allow_nil => false }
configuration.update(attr_names.extract_options!)
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 =~ /\A[+-]?\d+\Z/
end
else
validates_each(attr_names,configuration) do |record, attr_name,value|
next if configuration[:allow_nil] and record.send("#{attr_name}_before_type_cast").nil?
begin
Kernel.Float(record.send("#{attr_name}_before_type_cast").to_s)
numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
(numericality_options - [ :odd, :even ]).each do |option|
raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric)
end
validates_each(attr_names,configuration) do |record, attr_name, value|
raw_value = record.send("#{attr_name}_before_type_cast") || value
next if configuration[:allow_nil] and raw_value.nil?
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
next
end
raw_value = raw_value.to_i
else
begin
raw_value = Kernel.Float(raw_value.to_s)
rescue ArgumentError, TypeError
record.errors.add(attr_name, configuration[:message])
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
next
end
end
numericality_options.each do |option|
case option
when :odd, :even
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
else
record.errors.add(attr_name, configuration[:message] || (ActiveRecord::Errors.default_error_messages[option] % configuration[option])) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
end
end
end
end
# Creates an object just like Base.create but calls save! instead of save
# so an exception is raised if the record is invalid.
def create!(attributes = nil)
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr) }
else
attributes ||= {}
attributes.reverse_merge!(scope(:create)) if scoped?(:create)
object = new(attributes)
object.save!
object
@ -733,7 +915,7 @@ module ActiveRecord
private
def write_inheritable_set(key, methods)
existing_methods = read_inheritable_attribute(key) || []
write_inheritable_attribute(key, methods | existing_methods)
write_inheritable_attribute(key, existing_methods | methods)
end
def validation_method(on)

View file

@ -1,693 +0,0 @@
# :title: Transaction::Simple -- Active Object Transaction Support for Ruby
# :main: Transaction::Simple
#
# == Licence
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#--
# Transaction::Simple
# Simple object transaction support for Ruby
# Version 1.3.0
#
# Copyright (c) 2003 - 2005 Austin Ziegler
#
# $Id: simple.rb,v 1.5 2005/05/05 16:16:49 austin Exp $
#++
# The "Transaction" namespace can be used for additional transaction
# support objects and modules.
module Transaction
# A standard exception for transaction errors.
class TransactionError < StandardError; end
# The TransactionAborted exception is used to indicate when a
# transaction has been aborted in the block form.
class TransactionAborted < Exception; end
# The TransactionCommitted exception is used to indicate when a
# transaction has been committed in the block form.
class TransactionCommitted < Exception; end
te = "Transaction Error: %s"
Messages = {
:bad_debug_object =>
te % "the transaction debug object must respond to #<<.",
:unique_names =>
te % "named transactions must be unique.",
:no_transaction_open =>
te % "no transaction open.",
:cannot_rewind_no_transaction =>
te % "cannot rewind; there is no current transaction.",
:cannot_rewind_named_transaction =>
te % "cannot rewind to transaction %s because it does not exist.",
:cannot_rewind_transaction_before_block =>
te % "cannot rewind a transaction started before the execution block.",
:cannot_abort_no_transaction =>
te % "cannot abort; there is no current transaction.",
:cannot_abort_transaction_before_block =>
te % "cannot abort a transaction started before the execution block.",
:cannot_abort_named_transaction =>
te % "cannot abort nonexistant transaction %s.",
:cannot_commit_no_transaction =>
te % "cannot commit; there is no current transaction.",
:cannot_commit_transaction_before_block =>
te % "cannot commit a transaction started before the execution block.",
:cannot_commit_named_transaction =>
te % "cannot commit nonexistant transaction %s.",
:cannot_start_empty_block_transaction =>
te % "cannot start a block transaction with no objects.",
:cannot_obtain_transaction_lock =>
te % "cannot obtain transaction lock for #%s.",
}
# = Transaction::Simple for Ruby
# Simple object transaction support for Ruby
#
# == Introduction
# Transaction::Simple provides a generic way to add active transaction
# support to objects. The transaction methods added by this module will
# work with most objects, excluding those that cannot be
# <i>Marshal</i>ed (bindings, procedure objects, IO instances, or
# singleton objects).
#
# The transactions supported by Transaction::Simple are not backed
# transactions; they are not associated with any sort of data store.
# They are "live" transactions occurring in memory and in the object
# itself. This is to allow "test" changes to be made to an object
# before making the changes permanent.
#
# Transaction::Simple can handle an "infinite" number of transaction
# levels (limited only by memory). If I open two transactions, commit
# the second, but abort the first, the object will revert to the
# original version.
#
# Transaction::Simple supports "named" transactions, so that multiple
# levels of transactions can be committed, aborted, or rewound by
# referring to the appropriate name of the transaction. Names may be any
# object *except* +nil+. As with Hash keys, String names will be
# duplicated and frozen before using.
#
# Copyright:: Copyright © 2003 - 2005 by Austin Ziegler
# Version:: 1.3.0
# Licence:: MIT-Style
#
# Thanks to David Black for help with the initial concept that led to
# this library.
#
# == Usage
# include 'transaction/simple'
#
# v = "Hello, you." # -> "Hello, you."
# v.extend(Transaction::Simple) # -> "Hello, you."
#
# v.start_transaction # -> ... (a Marshal string)
# v.transaction_open? # -> true
# v.gsub!(/you/, "world") # -> "Hello, world."
#
# v.rewind_transaction # -> "Hello, you."
# v.transaction_open? # -> true
#
# v.gsub!(/you/, "HAL") # -> "Hello, HAL."
# v.abort_transaction # -> "Hello, you."
# v.transaction_open? # -> false
#
# v.start_transaction # -> ... (a Marshal string)
# v.start_transaction # -> ... (a Marshal string)
#
# v.transaction_open? # -> true
# v.gsub!(/you/, "HAL") # -> "Hello, HAL."
#
# v.commit_transaction # -> "Hello, HAL."
# v.transaction_open? # -> true
# v.abort_transaction # -> "Hello, you."
# v.transaction_open? # -> false
#
# == Named Transaction Usage
# v = "Hello, you." # -> "Hello, you."
# v.extend(Transaction::Simple) # -> "Hello, you."
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
# v.gsub!(/you/, "world") # -> "Hello, world."
#
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.rewind_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
#
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.transaction_name # -> :second
# v.abort_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> false
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
#
# v.commit_transaction(:first) # -> "Hello, HAL."
# v.transaction_open? # -> false
#
# == Block Usage
# v = "Hello, you." # -> "Hello, you."
# Transaction::Simple.start(v) do |tv|
# # v has been extended with Transaction::Simple and an unnamed
# # transaction has been started.
# tv.transaction_open? # -> true
# tv.gsub!(/you/, "world") # -> "Hello, world."
#
# tv.rewind_transaction # -> "Hello, you."
# tv.transaction_open? # -> true
#
# tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
# # The following breaks out of the transaction block after
# # aborting the transaction.
# tv.abort_transaction # -> "Hello, you."
# end
# # v still has Transaction::Simple applied from here on out.
# v.transaction_open? # -> false
#
# Transaction::Simple.start(v) do |tv|
# tv.start_transaction # -> ... (a Marshal string)
#
# tv.transaction_open? # -> true
# tv.gsub!(/you/, "HAL") # -> "Hello, HAL."
#
# # If #commit_transaction were called without having started a
# # second transaction, then it would break out of the transaction
# # block after committing the transaction.
# tv.commit_transaction # -> "Hello, HAL."
# tv.transaction_open? # -> true
# tv.abort_transaction # -> "Hello, you."
# end
# v.transaction_open? # -> false
#
# == Named Transaction Usage
# v = "Hello, you." # -> "Hello, you."
# v.extend(Transaction::Simple) # -> "Hello, you."
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
# v.gsub!(/you/, "world") # -> "Hello, world."
#
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.rewind_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> true
# v.transaction_open?(:first) # -> true
# v.transaction_open?(:second) # -> false
#
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
# v.transaction_name # -> :second
# v.abort_transaction(:first) # -> "Hello, you."
# v.transaction_open? # -> false
#
# v.start_transaction(:first) # -> ... (a Marshal string)
# v.gsub!(/you/, "world") # -> "Hello, world."
# v.start_transaction(:second) # -> ... (a Marshal string)
# v.gsub!(/world/, "HAL") # -> "Hello, HAL."
#
# v.commit_transaction(:first) # -> "Hello, HAL."
# v.transaction_open? # -> false
#
# == Thread Safety
# Threadsafe version of Transaction::Simple and
# Transaction::Simple::Group exist; these are loaded from
# 'transaction/simple/threadsafe' and
# 'transaction/simple/threadsafe/group', respectively, and are
# represented in Ruby code as Transaction::Simple::ThreadSafe and
# Transaction::Simple::ThreadSafe::Group, respectively.
#
# == Contraindications
# While Transaction::Simple is very useful, it has some severe
# limitations that must be understood. Transaction::Simple:
#
# * uses Marshal. Thus, any object which cannot be <i>Marshal</i>ed
# cannot use Transaction::Simple. In my experience, this affects
# singleton objects more often than any other object. It may be that
# Ruby 2.0 will solve this problem.
# * does not manage resources. Resources external to the object and its
# instance variables are not managed at all. However, all instance
# variables and objects "belonging" to those instance variables are
# managed. If there are object reference counts to be handled,
# Transaction::Simple will probably cause problems.
# * is not inherently thread-safe. In the ACID ("atomic, consistent,
# isolated, durable") test, Transaction::Simple provides CD, but it is
# up to the user of Transaction::Simple to provide isolation and
# atomicity. Transactions should be considered "critical sections" in
# multi-threaded applications. If thread safety and atomicity is
# absolutely required, use Transaction::Simple::ThreadSafe, which uses
# a Mutex object to synchronize the accesses on the object during the
# transaction operations.
# * does not necessarily maintain Object#__id__ values on rewind or
# abort. This may change for future versions that will be Ruby 1.8 or
# better *only*. Certain objects that support #replace will maintain
# Object#__id__.
# * Can be a memory hog if you use many levels of transactions on many
# objects.
#
module Simple
TRANSACTION_SIMPLE_VERSION = '1.3.0'
# Sets the Transaction::Simple debug object. It must respond to #<<.
# Sets the transaction debug object. Debugging will be performed
# automatically if there's a debug object. The generic transaction
# error class.
def self.debug_io=(io)
if io.nil?
@tdi = nil
@debugging = false
else
unless io.respond_to?(:<<)
raise TransactionError, Messages[:bad_debug_object]
end
@tdi = io
@debugging = true
end
end
# Returns +true+ if we are debugging.
def self.debugging?
@debugging
end
# Returns the Transaction::Simple debug object. It must respond to
# #<<.
def self.debug_io
@tdi ||= ""
@tdi
end
# If +name+ is +nil+ (default), then returns +true+ if there is
# currently a transaction open.
#
# If +name+ is specified, then returns +true+ if there is currently a
# transaction that responds to +name+ open.
def transaction_open?(name = nil)
if name.nil?
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "Transaction " <<
"[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n"
end
return (not @__transaction_checkpoint__.nil?)
else
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "Transaction(#{name.inspect}) " <<
"[#{(@__transaction_checkpoint__.nil?) ? 'closed' : 'open'}]\n"
end
return ((not @__transaction_checkpoint__.nil?) and @__transaction_names__.include?(name))
end
end
# Returns the current name of the transaction. Transactions not
# explicitly named are named +nil+.
def transaction_name
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:no_transaction_open]
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
"Transaction Name: #{@__transaction_names__[-1].inspect}\n"
end
if @__transaction_names__[-1].kind_of?(String)
@__transaction_names__[-1].dup
else
@__transaction_names__[-1]
end
end
# Starts a transaction. Stores the current object state. If a
# transaction name is specified, the transaction will be named.
# Transaction names must be unique. Transaction names of +nil+ will be
# treated as unnamed transactions.
def start_transaction(name = nil)
@__transaction_level__ ||= 0
@__transaction_names__ ||= []
if name.nil?
@__transaction_names__ << nil
ss = "" if Transaction::Simple.debugging?
else
if @__transaction_names__.include?(name)
raise TransactionError, Messages[:unique_names]
end
name = name.dup.freeze if name.kind_of?(String)
@__transaction_names__ << name
ss = "(#{name.inspect})" if Transaction::Simple.debugging?
end
@__transaction_level__ += 1
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'>' * @__transaction_level__} " <<
"Start Transaction#{ss}\n"
end
@__transaction_checkpoint__ = Marshal.dump(self)
end
# Rewinds the transaction. If +name+ is specified, then the
# intervening transactions will be aborted and the named transaction
# will be rewound. Otherwise, only the current transaction is rewound.
def rewind_transaction(name = nil)
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:cannot_rewind_no_transaction]
end
# Check to see if we are trying to rewind a transaction that is
# outside of the current transaction block.
if @__transaction_block__ and name
nix = @__transaction_names__.index(name) + 1
if nix < @__transaction_block__
raise TransactionError, Messages[:cannot_rewind_transaction_before_block]
end
end
if name.nil?
__rewind_this_transaction
ss = "" if Transaction::Simple.debugging?
else
unless @__transaction_names__.include?(name)
raise TransactionError, Messages[:cannot_rewind_named_transaction] % name.inspect
end
ss = "(#{name})" if Transaction::Simple.debugging?
while @__transaction_names__[-1] != name
@__transaction_checkpoint__ = __rewind_this_transaction
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
"Rewind Transaction#{ss}\n"
end
@__transaction_level__ -= 1
@__transaction_names__.pop
end
__rewind_this_transaction
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'|' * @__transaction_level__} " <<
"Rewind Transaction#{ss}\n"
end
self
end
# Aborts the transaction. Resets the object state to what it was
# before the transaction was started and closes the transaction. If
# +name+ is specified, then the intervening transactions and the named
# transaction will be aborted. Otherwise, only the current transaction
# is aborted.
#
# If the current or named transaction has been started by a block
# (Transaction::Simple.start), then the execution of the block will be
# halted with +break+ +self+.
def abort_transaction(name = nil)
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:cannot_abort_no_transaction]
end
# Check to see if we are trying to abort a transaction that is
# outside of the current transaction block. Otherwise, raise
# TransactionAborted if they are the same.
if @__transaction_block__ and name
nix = @__transaction_names__.index(name) + 1
if nix < @__transaction_block__
raise TransactionError, Messages[:cannot_abort_transaction_before_block]
end
raise TransactionAborted if @__transaction_block__ == nix
end
raise TransactionAborted if @__transaction_block__ == @__transaction_level__
if name.nil?
__abort_transaction(name)
else
unless @__transaction_names__.include?(name)
raise TransactionError, Messages[:cannot_abort_named_transaction] % name.inspect
end
__abort_transaction(name) while @__transaction_names__.include?(name)
end
self
end
# If +name+ is +nil+ (default), the current transaction level is
# closed out and the changes are committed.
#
# If +name+ is specified and +name+ is in the list of named
# transactions, then all transactions are closed and committed until
# the named transaction is reached.
def commit_transaction(name = nil)
if @__transaction_checkpoint__.nil?
raise TransactionError, Messages[:cannot_commit_no_transaction]
end
@__transaction_block__ ||= nil
# Check to see if we are trying to commit a transaction that is
# outside of the current transaction block. Otherwise, raise
# TransactionCommitted if they are the same.
if @__transaction_block__ and name
nix = @__transaction_names__.index(name) + 1
if nix < @__transaction_block__
raise TransactionError, Messages[:cannot_commit_transaction_before_block]
end
raise TransactionCommitted if @__transaction_block__ == nix
end
raise TransactionCommitted if @__transaction_block__ == @__transaction_level__
if name.nil?
ss = "" if Transaction::Simple.debugging?
__commit_transaction
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Commit Transaction#{ss}\n"
end
else
unless @__transaction_names__.include?(name)
raise TransactionError, Messages[:cannot_commit_named_transaction] % name.inspect
end
ss = "(#{name})" if Transaction::Simple.debugging?
while @__transaction_names__[-1] != name
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Commit Transaction#{ss}\n"
end
__commit_transaction
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Commit Transaction#{ss}\n"
end
__commit_transaction
end
self
end
# Alternative method for calling the transaction methods. An optional
# name can be specified for named transaction support.
#
# #transaction(:start):: #start_transaction
# #transaction(:rewind):: #rewind_transaction
# #transaction(:abort):: #abort_transaction
# #transaction(:commit):: #commit_transaction
# #transaction(:name):: #transaction_name
# #transaction:: #transaction_open?
def transaction(action = nil, name = nil)
case action
when :start
start_transaction(name)
when :rewind
rewind_transaction(name)
when :abort
abort_transaction(name)
when :commit
commit_transaction(name)
when :name
transaction_name
when nil
transaction_open?(name)
end
end
# Allows specific variables to be excluded from transaction support.
# Must be done after extending the object but before starting the
# first transaction on the object.
#
# vv.transaction_exclusions << "@io"
def transaction_exclusions
@transaction_exclusions ||= []
end
class << self
def __common_start(name, vars, &block)
if vars.empty?
raise TransactionError, Messages[:cannot_start_empty_block_transaction]
end
if block
begin
vlevel = {}
vars.each do |vv|
vv.extend(Transaction::Simple)
vv.start_transaction(name)
vlevel[vv.__id__] = vv.instance_variable_get(:@__transaction_level__)
vv.instance_variable_set(:@__transaction_block__, vlevel[vv.__id__])
end
yield(*vars)
rescue TransactionAborted
vars.each do |vv|
if name.nil? and vv.transaction_open?
loop do
tlevel = vv.instance_variable_get(:@__transaction_level__) || -1
vv.instance_variable_set(:@__transaction_block__, -1)
break if tlevel < vlevel[vv.__id__]
vv.abort_transaction if vv.transaction_open?
end
elsif vv.transaction_open?(name)
vv.instance_variable_set(:@__transaction_block__, -1)
vv.abort_transaction(name)
end
end
rescue TransactionCommitted
nil
ensure
vars.each do |vv|
if name.nil? and vv.transaction_open?
loop do
tlevel = vv.instance_variable_get(:@__transaction_level__) || -1
break if tlevel < vlevel[vv.__id__]
vv.instance_variable_set(:@__transaction_block__, -1)
vv.commit_transaction if vv.transaction_open?
end
elsif vv.transaction_open?(name)
vv.instance_variable_set(:@__transaction_block__, -1)
vv.commit_transaction(name)
end
end
end
else
vars.each do |vv|
vv.extend(Transaction::Simple)
vv.start_transaction(name)
end
end
end
private :__common_start
def start_named(name, *vars, &block)
__common_start(name, vars, &block)
end
def start(*vars, &block)
__common_start(nil, vars, &block)
end
end
def __abort_transaction(name = nil) #:nodoc:
@__transaction_checkpoint__ = __rewind_this_transaction
if name.nil?
ss = "" if Transaction::Simple.debugging?
else
ss = "(#{name.inspect})" if Transaction::Simple.debugging?
end
if Transaction::Simple.debugging?
Transaction::Simple.debug_io << "#{'<' * @__transaction_level__} " <<
"Abort Transaction#{ss}\n"
end
@__transaction_level__ -= 1
@__transaction_names__.pop
if @__transaction_level__ < 1
@__transaction_level__ = 0
@__transaction_names__ = []
end
end
TRANSACTION_CHECKPOINT = "@__transaction_checkpoint__" #:nodoc:
SKIP_TRANSACTION_VARS = [TRANSACTION_CHECKPOINT, "@__transaction_level__"] #:nodoc:
def __rewind_this_transaction #:nodoc:
rr = Marshal.restore(@__transaction_checkpoint__)
begin
self.replace(rr) if respond_to?(:replace)
rescue
nil
end
rr.instance_variables.each do |vv|
next if SKIP_TRANSACTION_VARS.include?(vv)
next if self.transaction_exclusions.include?(vv)
if respond_to?(:instance_variable_get)
instance_variable_set(vv, rr.instance_variable_get(vv))
else
instance_eval(%q|#{vv} = rr.instance_eval("#{vv}")|)
end
end
new_ivar = instance_variables - rr.instance_variables - SKIP_TRANSACTION_VARS
new_ivar.each do |vv|
if respond_to?(:instance_variable_set)
instance_variable_set(vv, nil)
else
instance_eval(%q|#{vv} = nil|)
end
end
if respond_to?(:instance_variable_get)
rr.instance_variable_get(TRANSACTION_CHECKPOINT)
else
rr.instance_eval(TRANSACTION_CHECKPOINT)
end
end
def __commit_transaction #:nodoc:
if respond_to?(:instance_variable_get)
@__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_variable_get(TRANSACTION_CHECKPOINT)
else
@__transaction_checkpoint__ = Marshal.restore(@__transaction_checkpoint__).instance_eval(TRANSACTION_CHECKPOINT)
end
@__transaction_level__ -= 1
@__transaction_names__.pop
if @__transaction_level__ < 1
@__transaction_level__ = 0
@__transaction_names__ = []
end
end
private :__abort_transaction
private :__rewind_this_transaction
private :__commit_transaction
end
end

View file

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

View file

@ -1,15 +0,0 @@
require 'yaml'
module ActiveRecord
module Wrappings #:nodoc:
class YamlWrapper < AbstractWrapper #:nodoc:
def wrap(attribute) attribute.to_yaml end
def unwrap(attribute) YAML::load(attribute) end
end
module ClassMethods #:nodoc:
# Wraps the attribute in Yaml encoding
def wrap_in_yaml(*attributes) wrap_with(YamlWrapper, attributes) end
end
end
end

View file

@ -1,58 +0,0 @@
module ActiveRecord
# A plugin framework for wrapping attribute values before they go in and unwrapping them after they go out of the database.
# This was intended primarily for YAML wrapping of arrays and hashes, but this behavior is now native in the Base class.
# So for now this framework is laying dormant until a need pops up.
module Wrappings #:nodoc:
module ClassMethods #:nodoc:
def wrap_with(wrapper, *attributes)
[ attributes ].flat.each { |attribute| wrapper.wrap(attribute) }
end
end
def self.included(base)
base.extend(ClassMethods)
end
class AbstractWrapper #:nodoc:
def self.wrap(attribute, record_binding) #:nodoc:
%w( before_save after_save after_initialize ).each do |callback|
eval "#{callback} #{name}.new('#{attribute}')", record_binding
end
end
def initialize(attribute) #:nodoc:
@attribute = attribute
end
def save_wrapped_attribute(record) #:nodoc:
if record.attribute_present?(@attribute)
record.send(
"write_attribute",
@attribute,
wrap(record.send("read_attribute", @attribute))
)
end
end
def load_wrapped_attribute(record) #:nodoc:
if record.attribute_present?(@attribute)
record.send(
"write_attribute",
@attribute,
unwrap(record.send("read_attribute", @attribute))
)
end
end
alias_method :before_save, :save_wrapped_attribute #:nodoc:
alias_method :after_save, :load_wrapped_attribute #:nodoc:
alias_method :after_initialize, :after_save #:nodoc:
# Overwrite to implement the logic that'll take the regular attribute and wrap it.
def wrap(attribute) end
# Overwrite to implement the logic that'll take the wrapped attribute and unwrap it.
def unwrap(attribute) end
end
end
end

View file

@ -0,0 +1 @@
require 'active_record'