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:
parent
0f6889e09f
commit
6873fc8026
1083 changed files with 52810 additions and 41058 deletions
35
vendor/rails/activerecord/lib/active_record.rb
vendored
35
vendor/rails/activerecord/lib/active_record.rb
vendored
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
1370
vendor/rails/activerecord/lib/active_record/base.rb
vendored
1370
vendor/rails/activerecord/lib/active_record/base.rb
vendored
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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." ) }
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
87
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
vendored
Normal file
87
vendor/rails/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
vendored
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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 * ', '
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
34
vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
vendored
Normal file
34
vendor/rails/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
vendored
Normal 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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
98
vendor/rails/activerecord/lib/active_record/serialization.rb
vendored
Normal file
98
vendor/rails/activerecord/lib/active_record/serialization.rb
vendored
Normal 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'
|
71
vendor/rails/activerecord/lib/active_record/serializers/json_serializer.rb
vendored
Normal file
71
vendor/rails/activerecord/lib/active_record/serializers/json_serializer.rb
vendored
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
1
vendor/rails/activerecord/lib/activerecord.rb
vendored
Normal file
1
vendor/rails/activerecord/lib/activerecord.rb
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
require 'active_record'
|
Loading…
Add table
Add a link
Reference in a new issue