Freezing composite primary key so that we can apply TomH's patch to it, and that way Potlatch will work in Rails mode without a problem. Please test.

This commit is contained in:
Shaun McDonald 2008-11-15 01:11:27 +00:00
parent 0a57413d3e
commit d74e2196a0
122 changed files with 6081 additions and 1 deletions

View file

@ -3,4 +3,4 @@ POTLATCH_PRESETS = Potlatch::Potlatch.get_presets()
# Use SQL (faster) or Rails (more elegant) for common Potlatch reads
# getway speedup is approximately x2, whichways approximately x7
POTLATCH_USE_SQL = true
POTLATCH_USE_SQL = false

View file

@ -0,0 +1,148 @@
== 1.1.0 2008-10-29
* fixes to get cpk working for Rails 2.1.2
== 1.0.10 2008-10-22
* add composite key where clause creator method [timurv]
== 1.0.9 2008-09-08
* fix postgres tests
* fix for delete_records when has_many association has composite keys [darxriggs]
* more consistent table/column name quoting [pbrant]
== 1.0.8 2008-08-27
* fix has_many :through for non composite models [thx rcarver]
== 1.0.7 2008-08-12
* fix for the last fix -- when has_many is composite and belongs_to is single
== 1.0.6 2008-08-06
* fix associations create
== 1.0.5 2008-07-25
* fix for calculations with a group by clause [thx Sirius Black]
== 1.0.4 2008-07-15
* support for oracle_enhanced adapter [thx Raimonds Simanovskis]
== 1.0.3 2008-07-13
* more fixes and tests for has many through [thx Menno van der Sman]
== 1.0.2 2008-06-07
* fix for has many through when through association has composite keys
== 1.0.1 2008-06-06
* Oracle fixes
== 1.0.0 2008-06-05
* Support for Rails 2.1
== 0.9.93 2008-06-01
* set fixed dependency on activerecord 2.0.2
== 0.9.92 2008-02-22
* Support for has_and_belongs_to_many
== 0.9.91 2008-01-27
* Incremented activerecord dependency to 2.0.2 [thx emmanuel.pirsch]
== 0.9.90 2008-01-27
* Trial release for rails/activerecord 2.0.2 supported
== 0.9.1 2007-10-28
* Migrations fix - allow :primary_key => [:name] to work [no unit test] [thx Shugo Maeda]
== 0.9.0 2007-09-28
* Added support for polymorphs [thx nerdrew]
* init.rb file so gem can be installed as a plugin for Rails [thx nerdrew]
* Added ibm_db support [thx K Venkatasubramaniyan]
* Support for cleaning dependents [thx K Venkatasubramaniyan]
* Rafactored db rake tasks into namespaces
* Added namespaced tests (e.g. mysql:test for test_mysql)
== 0.8.6 / 2007-6-12
* 1 emergency fix due to Rails Core change
* Rails v7004 removed #quote; fixed with connection.quote_column_name [thx nerdrew]
== 0.8.5 / 2007-6-5
* 1 change due to Rails Core change
* Can no longer use RAILS_CONNECTION_ADAPTERS from Rails core
* 7 dev improvement:
* Changed History.txt syntax to rdoc format
* Added deploy tasks
* Removed CHANGELOG + migrated into History.txt
* Changed PKG_NAME -> GEM_NAME in Rakefile
* Renamed README -> README.txt for :publish_docs task
* Added :check_version task
* VER => VERS in rakefile
* 1 website improvement:
* website/index.txt includes link to "8 steps to fix other ppls code"
== 0.8.4 / 2007-5-3
* 1 bugfix
* Corrected ids_list => ids in the exception message. That'll teach me for not adding unit tests before fixing bugs.
== 0.8.3 / 2007-5-3
* 1 bugfix
* Explicit reference to ::ActiveRecord::RecordNotFound
* 1 website addition:
* Added routing help [Pete Sumskas]
== 0.8.2 / 2007-4-11
* 1 major enhancement:
* Oracle unit tests!! [Darrin Holst]
* And they work too
== 0.8.1 / 2007-4-10
* 1 bug fix:
* Fixed the distinct(count) for oracle (removed 'as')
== 0.8.0 / 2007-4-6
* 1 major enhancement:
* Support for calcualtions on associations
* 2 new DB supported:
* Tests run on sqlite
* Tests run on postgresql
* History.txt to keep track of changes like these
* Using Hoe for Rakefile
* Website generator rake tasks
== 0.3.3
* id=
* create now work
== 0.1.4
* it was important that #{primary_key} for composites --> 'key1,key2' and not 'key1key2' so created PrimaryKeys class
== 0.0.1
* Initial version
* set_primary_keys(*keys) is the activation class method to transform an ActiveRecord into a composite primary key AR
* find(*ids) supports the passing of
* id sets: Foo.find(2,1),
* lists of id sets: Foo.find([2,1], [7,3], [8,12]),
* and even stringified versions of the above:
* Foo.find '2,1' or Foo.find '2,1;7,3'

View file

@ -0,0 +1,121 @@
History.txt
Manifest.txt
README.txt
README_DB2.txt
Rakefile
init.rb
install.rb
lib/adapter_helper/base.rb
lib/adapter_helper/mysql.rb
lib/adapter_helper/oracle.rb
lib/adapter_helper/postgresql.rb
lib/adapter_helper/sqlite3.rb
lib/composite_primary_keys.rb
lib/composite_primary_keys/association_preload.rb
lib/composite_primary_keys/associations.rb
lib/composite_primary_keys/attribute_methods.rb
lib/composite_primary_keys/base.rb
lib/composite_primary_keys/calculations.rb
lib/composite_primary_keys/composite_arrays.rb
lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb
lib/composite_primary_keys/connection_adapters/oracle_adapter.rb
lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb
lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb
lib/composite_primary_keys/fixtures.rb
lib/composite_primary_keys/migration.rb
lib/composite_primary_keys/reflection.rb
lib/composite_primary_keys/version.rb
loader.rb
local/database_connections.rb.sample
local/paths.rb.sample
local/tasks.rb.sample
scripts/console.rb
scripts/txt2html
scripts/txt2js
tasks/activerecord_selection.rake
tasks/databases.rake
tasks/databases/mysql.rake
tasks/databases/oracle.rake
tasks/databases/postgresql.rake
tasks/databases/sqlite3.rake
tasks/deployment.rake
tasks/local_setup.rake
tasks/website.rake
test/README_tests.txt
test/abstract_unit.rb
test/connections/native_ibm_db/connection.rb
test/connections/native_mysql/connection.rb
test/connections/native_oracle/connection.rb
test/connections/native_postgresql/connection.rb
test/connections/native_sqlite/connection.rb
test/fixtures/article.rb
test/fixtures/articles.yml
test/fixtures/comment.rb
test/fixtures/comments.yml
test/fixtures/db_definitions/db2-create-tables.sql
test/fixtures/db_definitions/db2-drop-tables.sql
test/fixtures/db_definitions/mysql.sql
test/fixtures/db_definitions/oracle.drop.sql
test/fixtures/db_definitions/oracle.sql
test/fixtures/db_definitions/postgresql.sql
test/fixtures/db_definitions/sqlite.sql
test/fixtures/department.rb
test/fixtures/departments.yml
test/fixtures/employee.rb
test/fixtures/employees.yml
test/fixtures/group.rb
test/fixtures/groups.yml
test/fixtures/hack.rb
test/fixtures/hacks.yml
test/fixtures/membership.rb
test/fixtures/membership_status.rb
test/fixtures/membership_statuses.yml
test/fixtures/memberships.yml
test/fixtures/product.rb
test/fixtures/product_tariff.rb
test/fixtures/product_tariffs.yml
test/fixtures/products.yml
test/fixtures/reading.rb
test/fixtures/readings.yml
test/fixtures/reference_code.rb
test/fixtures/reference_codes.yml
test/fixtures/reference_type.rb
test/fixtures/reference_types.yml
test/fixtures/street.rb
test/fixtures/streets.yml
test/fixtures/suburb.rb
test/fixtures/suburbs.yml
test/fixtures/tariff.rb
test/fixtures/tariffs.yml
test/fixtures/user.rb
test/fixtures/users.yml
test/hash_tricks.rb
test/plugins/pagination.rb
test/plugins/pagination_helper.rb
test/test_associations.rb
test/test_attribute_methods.rb
test/test_attributes.rb
test/test_clone.rb
test/test_composite_arrays.rb
test/test_create.rb
test/test_delete.rb
test/test_dummy.rb
test/test_find.rb
test/test_ids.rb
test/test_miscellaneous.rb
test/test_pagination.rb
test/test_polymorphic.rb
test/test_santiago.rb
test/test_tutorial_examle.rb
test/test_update.rb
tmp/test.db
website/index.html
website/index.txt
website/javascripts/rounded_corners_lite.inc.js
website/stylesheets/screen.css
website/template.js
website/template.rhtml
website/version-raw.js
website/version-raw.txt
website/version.js
website/version.txt

View file

@ -0,0 +1,41 @@
= Composite Primary Keys for ActiveRecords
== Summary
ActiveRecords/Rails famously doesn't support composite primary keys.
This RubyGem extends the activerecord gem to provide CPK support.
== Installation
gem install composite_primary_keys
== Usage
require 'composite_primary_keys'
class ProductVariation
set_primary_keys :product_id, :variation_seq
end
pv = ProductVariation.find(345, 12)
It even supports composite foreign keys for associations.
See http://compositekeys.rubyforge.org for more.
== Running Tests
See test/README.tests.txt
== Url
http://compositekeys.rubyforge.org
== Questions, Discussion and Contributions
http://groups.google.com/compositekeys
== Author
Written by Dr Nic Williams, drnicwilliams@gmail
Contributions by many!

View file

@ -0,0 +1,33 @@
Composite Primary key support for db2
== Driver Support ==
DB2 support requires the IBM_DB driver provided by http://rubyforge.org/projects/rubyibm/
project. Install using gem install ibm_db. Tested against version 0.60 of the driver.
This rubyforge project appears to be permenant location for the IBM adapter.
Older versions of the driver available from IBM Alphaworks will not work.
== Driver Bug and workaround provided as part of this plugin ==
Unlike the basic quote routine available for Rails AR, the DB2 adapter's quote
method doesn't return " column_name = 1 " when string values (integers in string type variable)
are passed for quoting numeric column. Rather it returns "column_name = '1'.
DB2 doesn't accept single quoting numeric columns in SQL. Currently, as part of
this plugin a fix is provided for the DB2 adapter since this plugin does
pass string values like this. Perhaps a patch should be sent to the DB2 adapter
project for a permanant fix.
== Database Setup ==
Database must be manually created using a separate command. Read the rake task
for creating tables and change the db name, user and passwords accordingly.
== Tested Database Server version ==
This is tested against DB2 v9.1 in Ubuntu Feisty Fawn (7.04)
== Tested Database Client version ==
This is tested against DB2 v9.1 in Ubuntu Feisty Fawn (7.04)

View file

@ -0,0 +1,65 @@
require 'rubygems'
require 'rake'
require 'rake/clean'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
require 'fileutils'
require 'hoe'
include FileUtils
require File.join(File.dirname(__FILE__), 'lib', 'composite_primary_keys', 'version')
AUTHOR = "Dr Nic Williams"
EMAIL = "drnicwilliams@gmail.com"
DESCRIPTION = "Composite key support for ActiveRecords"
GEM_NAME = "composite_primary_keys" # what ppl will type to install your gem
if File.exists?("~/.rubyforge/user-config.yml")
# TODO this should prob go in a local/ file
config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
RUBYFORGE_USERNAME = config["username"]
end
RUBYFORGE_PROJECT = "compositekeys"
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
REV = nil #File.read(".svn/entries")[/committed-rev="(\d+)"/, 1] rescue nil
VERS = ENV['VERSION'] || (CompositePrimaryKeys::VERSION::STRING + (REV ? ".#{REV}" : ""))
CLEAN.include ['**/.*.sw?', '*.gem', '.config','debug.log','*.db','logfile','log/**/*','**/.DS_Store', '.project']
RDOC_OPTS = ['--quiet', '--title', "newgem documentation",
"--opname", "index.html",
"--line-numbers",
"--main", "README",
"--inline-source"]
class Hoe
def extra_deps
@extra_deps.reject { |x| Array(x).first == 'hoe' }
end
end
# Generate all the Rake tasks
# Run 'rake -T' to see list of generated tasks (from gem root directory)
hoe = Hoe.new(GEM_NAME, VERS) do |p|
p.author = AUTHOR
p.description = DESCRIPTION
p.email = EMAIL
p.summary = DESCRIPTION
p.url = HOMEPATH
p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
p.test_globs = ["test/**/test*.rb"]
p.clean_globs |= CLEAN #An array of file patterns to delete on clean.
# == Optional
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
p.extra_deps = [['activerecord', '>= 2.1.2']] #An array of rubygem dependencies.
#p.spec_extras - A hash of extra values to set in the gemspec.
end
CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")
PATH = RUBYFORGE_PROJECT
hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
PROJECT_ROOT = File.expand_path(".")
require 'loader'

View file

@ -0,0 +1,2 @@
# Include hook code here
require_dependency 'composite_primary_keys'

View file

@ -0,0 +1,30 @@
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by ways of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
# the acual gruntwork
Dir.chdir("lib")
Find.find("composite_primary_keys", "composite_primary_keys.rb") { |f|
if f[-3..-1] == ".rb"
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
else
File::makedirs(File.join($sitedir, *f.split(/\//)))
end
}

View file

@ -0,0 +1,63 @@
module AdapterHelper
class Base
class << self
attr_accessor :adapter
def load_connection_from_env(adapter)
self.adapter = adapter
unless ENV['cpk_adapters']
puts error_msg_setup_helper
exit
end
ActiveRecord::Base.configurations = YAML.load(ENV['cpk_adapters'])
unless spec = ActiveRecord::Base.configurations[adapter]
puts error_msg_adapter_helper
exit
end
spec[:adapter] = adapter
spec
end
def error_msg_setup_helper
<<-EOS
Setup Helper:
CPK now has a place for your individual testing configuration.
That is, instead of hardcoding it in the Rakefile and test/connections files,
there is now a local/database_connections.rb file that is NOT in the
repository. Your personal DB information (username, password etc) can
be stored here without making it difficult to submit patches etc.
Installation:
i) cp locals/database_connections.rb.sample locals/database_connections.rb
ii) For #{adapter} connection details see "Adapter Setup Helper" below.
iii) Rerun this task
#{error_msg_adapter_helper}
Current ENV:
#{ENV.inspect}
EOS
end
def error_msg_adapter_helper
<<-EOS
Adapter Setup Helper:
To run #{adapter} tests, you need to setup your #{adapter} connections.
In your local/database_connections.rb file, within the ENV['cpk_adapter'] hash, add:
"#{adapter}" => { adapter settings }
That is, it will look like:
ENV['cpk_adapters'] = {
"#{adapter}" => {
:adapter => "#{adapter}",
:username => "root",
:password => "root",
# ...
}
}.to_yaml
EOS
end
end
end
end

View file

@ -0,0 +1,13 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class MySQL < Base
class << self
def load_connection_from_env
spec = super('mysql')
spec[:database] ||= 'composite_primary_keys_unittest'
spec
end
end
end
end

View file

@ -0,0 +1,12 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class Oracle < Base
class << self
def load_connection_from_env
spec = super('oracle')
spec
end
end
end
end

View file

@ -0,0 +1,13 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class Postgresql < Base
class << self
def load_connection_from_env
spec = super('postgresql')
spec[:database] ||= 'composite_primary_keys_unittest'
spec
end
end
end
end

View file

@ -0,0 +1,13 @@
require File.join(File.dirname(__FILE__), 'base')
module AdapterHelper
class Sqlite3 < Base
class << self
def load_connection_from_env
spec = super('sqlite3')
spec[:dbfile] ||= "tmp/test.db"
spec
end
end
end
end

View file

@ -0,0 +1,55 @@
#--
# Copyright (c) 2006 Nic Williams
#
# 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.
#++
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
unless defined?(ActiveRecord)
begin
require 'active_record'
rescue LoadError
require 'rubygems'
require_gem 'activerecord'
end
end
require 'composite_primary_keys/fixtures'
require 'composite_primary_keys/composite_arrays'
require 'composite_primary_keys/associations'
require 'composite_primary_keys/association_preload'
require 'composite_primary_keys/reflection'
require 'composite_primary_keys/base'
require 'composite_primary_keys/calculations'
require 'composite_primary_keys/migration'
require 'composite_primary_keys/attribute_methods'
ActiveRecord::Base.class_eval do
include CompositePrimaryKeys::ActiveRecord::Base
end
Dir[File.dirname(__FILE__) + '/composite_primary_keys/connection_adapters/*.rb'].each do |adapter|
begin
require adapter.gsub('.rb','')
rescue MissingSourceFile
end
end

View file

@ -0,0 +1,236 @@
module CompositePrimaryKeys
module ActiveRecord
module AssociationPreload
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
# Composite key versions of Association functions
module ClassMethods
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
table_name = reflection.klass.quoted_table_name
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
if composite?
primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
"(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
end.join(" OR ")
conditions = [where, ids].flatten
joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection, reflection.klass.table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}"
parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{connection.quote_column_name(k)}"}
parent_record_id = connection.concat(*parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact)
else
conditions = ["t0.#{connection.quote_column_name(reflection.primary_key_name)} IN (?)", ids]
joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{connection.quote_column_name(reflection.klass.primary_key)} = t0.#{connection.quote_column_name(reflection.association_foreign_key)})"
parent_record_id = reflection.primary_key_name
end
conditions.first << append_conditions(reflection, preload_options)
associated_records = reflection.klass.find(:all,
:conditions => conditions,
:include => options[:include],
:joins => joins,
:select => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
:order => options[:order])
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
end
def preload_has_many_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
unless through_records.empty?
source = reflection.source_reflection.name
#add conditions from reflection!
through_records.first.class.preload_associations(through_records, source, reflection.options)
through_records.each do |through_record|
key = through_primary_key.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| through_record.send(k)}.join(CompositePrimaryKeys::ID_SEP)
add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source))
end
end
else
associated_records = find_associated_records(ids, reflection, preload_options)
set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP))
end
end
def preload_through_records(records, reflection, through_association)
through_reflection = reflections[through_association]
through_primary_key = through_reflection.primary_key_name
if reflection.options[:source_type]
interface = reflection.source_reflection.options[:foreign_type]
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
records.compact!
records.first.class.preload_associations(records, through_association, preload_options)
# Dont cache the association - we would only be caching a subset
through_records = []
records.each do |record|
proxy = record.send(through_association)
if proxy.respond_to?(:target)
through_records << proxy.target
proxy.reset
else # this is a has_one :through reflection
through_records << proxy if proxy
end
end
through_records.flatten!
else
records.first.class.preload_associations(records, through_association)
through_records = records.map {|record| record.send(through_association)}.flatten
end
through_records.compact!
through_records
end
def preload_belongs_to_association(records, reflection, preload_options={})
options = reflection.options
primary_key_name = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
if options[:polymorphic]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
# I need to keep the original ids for each record (as opposed to the stringified) so
# that they get properly converted for each db so the id_map ends up looking like:
#
# { '1,2' => {:id => [1,2], :records => [...records...]}}
id_map = {}
records.each do |record|
key = primary_key_name.map{|k| record.attributes[k]}
key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
if key_as_string
mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
mapped_records[:records] << record
end
end
klasses_and_ids = [[reflection.klass.name, id_map]]
end
klasses_and_ids.each do |klass_and_id|
klass_name, id_map = *klass_and_id
klass = klass_name.constantize
table_name = klass.quoted_table_name
connection = reflection.active_record.connection
if composite?
primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
"(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
end.join(" OR ")
conditions = [where, ids].flatten
else
conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
end
conditions.first << append_conditions(reflection, preload_options)
associated_records = klass.find(:all,
:conditions => conditions,
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
associated_records.each do |associated_record|
associated_record_key = associated_record[key]
associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
mapped_records = id_to_record_map[associated_record_key]
add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
end
end
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
seen_keys = {}
associated_records.each do |associated_record|
associated_record_key = associated_record[key]
associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
#this is a has_one or belongs_to: there should only be one record.
#Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
# only one row per distinct foo_id' so this where we enforce that
next if seen_keys[associated_record_key]
seen_keys[associated_record_key] = true
mapped_records = id_to_record_map[associated_record_key][:records]
mapped_records.each do |mapped_record|
mapped_record.send("set_#{reflection_name}_target", associated_record)
end
end
end
def find_associated_records(ids, reflection, preload_options)
options = reflection.options
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
connection = reflection.active_record.connection
foreign_key = reflection.primary_key_name
conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids]
if composite?
foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys|
"(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
end.join(" OR ")
conditions = [where, ids].flatten
end
end
conditions.first << append_conditions(reflection, preload_options)
reflection.klass.find(:all,
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
:include => preload_options[:include] || options[:include],
:conditions => conditions,
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
:order => preload_options[:order] || options[:order])
end
def full_composite_join_clause(reflection, table1, full_keys1, table2, full_keys2)
connection = reflection.active_record.connection
full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
quoted1 = connection.quote_table_name(table1)
quoted2 = connection.quote_table_name(table2)
"#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
end.join(" AND ")
"(#{where_clause})"
end
end
end
end
end

View file

@ -0,0 +1,428 @@
module CompositePrimaryKeys
module ActiveRecord
module Associations
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
# Composite key versions of Association functions
module ClassMethods
def construct_counter_sql_with_included_associations(options, join_dependency)
scope = scope(:find)
sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
if !self.connection.supports_count_distinct?
sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
end
sql << " FROM #{quoted_table_name} "
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
if !self.connection.supports_count_distinct?
sql << ")"
end
return sanitize_sql(sql)
end
def construct_finder_sql_with_included_associations(options, join_dependency)
scope = scope(:find)
sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
sql << "ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
return sanitize_sql(sql)
end
def table_columns(columns)
columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"}
end
def quoted_table_columns(columns)
table_columns(columns).join(ID_SEP)
end
end
end
end
end
module ActiveRecord::Associations::ClassMethods
class JoinDependency
def construct_association(record, join, row)
case join.reflection.macro
when :has_many, :has_and_belongs_to_many
collection = record.send(join.reflection.name)
collection.loaded
join_aliased_primary_keys = join.active_record.composite? ?
join.aliased_primary_key : [join.aliased_primary_key]
return nil if
record.id.to_s != join.parent.record_id(row).to_s or not
join_aliased_primary_keys.select {|key| row[key].nil?}.blank?
association = join.instantiate(row)
collection.target.push(association) unless collection.target.include?(association)
when :has_one, :belongs_to
return if record.id.to_s != join.parent.record_id(row).to_s or
[*join.aliased_primary_key].any? { |key| row[key].nil? }
association = join.instantiate(row)
record.send("set_#{join.reflection.name}_target", association)
else
raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
end
return association
end
class JoinBase
def aliased_primary_key
active_record.composite? ?
primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
"#{ aliased_prefix }_r0"
end
def record_id(row)
active_record.composite? ?
aliased_primary_key.map {|key| row[key]}.to_composite_ids :
row[aliased_primary_key]
end
def column_names_with_alias
unless @column_names_with_alias
@column_names_with_alias = []
keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
(keys + (column_names - keys)).each_with_index do |column_name, i|
@column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
end
end
return @column_names_with_alias
end
end
class JoinAssociation < JoinBase
alias single_association_join association_join
def association_join
reflection.active_record.composite? ? composite_association_join : single_association_join
end
def composite_association_join
join = case reflection.macro
when :has_and_belongs_to_many
" LEFT OUTER JOIN %s ON %s " % [
table_alias_for(options[:join_table], aliased_join_table_name),
composite_join_clause(
full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
)
] +
" LEFT OUTER JOIN %s ON %s " % [
table_name_and_alias,
composite_join_clause(
full_keys(aliased_table_name, klass.primary_key),
full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
)
]
when :has_many, :has_one
case
when reflection.macro == :has_many && reflection.options[:through]
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
if through_reflection.options[:as] # has_many :through against a polymorphic join
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
if source_reflection.macro == :has_many && source_reflection.options[:as]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
case source_reflection.macro
when :belongs_to
first_key = primary_key
second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
when :has_many
first_key = through_reflection.klass.to_s.classify.foreign_key
second_key = options[:foreign_key] || primary_key
end
" LEFT OUTER JOIN %s ON %s " % [
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
composite_join_clause(
full_keys(aliased_join_table_name, through_reflection.primary_key_name),
full_keys(parent.aliased_table_name, parent.primary_key)
)
] +
" LEFT OUTER JOIN %s ON %s " % [
table_name_and_alias,
composite_join_clause(
full_keys(aliased_table_name, first_key),
full_keys(aliased_join_table_name, second_key)
)
]
end
end
when reflection.macro == :has_many && reflection.options[:as]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
when reflection.macro == :has_one && reflection.options[:as]
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
" LEFT OUTER JOIN %s ON %s " % [
table_name_and_alias,
composite_join_clause(
full_keys(aliased_table_name, foreign_key),
full_keys(parent.aliased_table_name, parent.primary_key)),
]
end
when :belongs_to
" LEFT OUTER JOIN %s ON %s " % [
table_name_and_alias,
composite_join_clause(
full_keys(aliased_table_name, reflection.klass.primary_key),
full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
]
else
""
end || ''
join << %(AND %s.%s = %s ) % [
aliased_table_name,
reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
join
end
def full_keys(table_name, keys)
connection = reflection.active_record.connection
quoted_table_name = connection.quote_table_name(table_name)
if keys.is_a?(Array)
keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
else
"#{quoted_table_name}.#{connection.quote_column_name(keys)}"
end
end
def composite_join_clause(full_keys1, full_keys2)
full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
"#{key1}=#{key2}"
end.join(" AND ")
"(#{where_clause})"
end
end
end
end
module ActiveRecord::Associations
class AssociationProxy #:nodoc:
def composite_where_clause(full_keys, ids)
full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
if ids.is_a?(String)
ids = [[ids]]
elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
ids = [ids.to_composite_ids]
end
where_clause = ids.map do |id_set|
transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
transposed.map do |full_key, id|
"#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
end.join(" AND ")
end.join(") OR (")
"(#{where_clause})"
end
def composite_join_clause(full_keys1, full_keys2)
full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
"#{key1}=#{key2}"
end.join(" AND ")
"(#{where_clause})"
end
def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
connection = @reflection.active_record.connection
full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
quoted1 = connection.quote_table_name(table1)
quoted2 = connection.quote_table_name(table2)
where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
"#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
end.join(" AND ")
"(#{where_clause})"
end
def full_keys(table_name, keys)
connection = @reflection.active_record.connection
quoted_table_name = connection.quote_table_name(table_name)
keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
if keys.is_a?(Array)
keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
else
"#{quoted_table_name}.#{connection.quote_column_name(keys)}"
end
end
def full_columns_equals(table_name, keys, quoted_ids)
connection = @reflection.active_record.connection
quoted_table_name = connection.quote_table_name(table_name)
if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}"
end
keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
keys_ids = [keys, quoted_ids].transpose
keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ')
end
def set_belongs_to_association_for(record)
if @reflection.options[:as]
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
else
key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
key_values.each{|key, value| record[key] = value} unless @owner.new_record?
end
end
end
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
def construct_sql
@reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
if @reflection.options[:finder_sql]
@finder_sql = @reflection.options[:finder_sql]
else
@finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
@finder_sql << " AND (#{conditions})" if conditions
end
@join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " +
full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
end
end
class HasManyAssociation < AssociationCollection #:nodoc:
def construct_sql
case
when @reflection.options[:finder_sql]
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
when @reflection.options[:as]
@finder_sql =
"#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
@finder_sql << " AND (#{conditions})" if conditions
else
@finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
@finder_sql << " AND (#{conditions})" if conditions
end
if @reflection.options[:counter_sql]
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
elsif @reflection.options[:finder_sql]
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
end
end
def delete_records(records)
if @reflection.options[:dependent]
records.each { |r| r.destroy }
else
connection = @reflection.active_record.connection
field_names = @reflection.primary_key_name.split(',')
field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"}
records.each do |r|
where_clause = nil
if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP)
where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair|
"(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
end
where_clause = where_clause_terms.join(" AND ")
else
where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' + r.quoted_id
end
@reflection.klass.update_all( field_names.join(',') , where_clause)
end
end
end
end
class HasOneAssociation < BelongsToAssociation #:nodoc:
def construct_sql
case
when @reflection.options[:as]
@finder_sql =
"#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
"#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
else
@finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
end
@finder_sql << " AND (#{conditions})" if conditions
end
end
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
def construct_conditions_with_composite_keys
if @reflection.through_reflection.options[:as]
construct_conditions_without_composite_keys
else
conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
conditions << " AND (#{sql_conditions})" if sql_conditions
conditions
end
end
alias_method_chain :construct_conditions, :composite_keys
def construct_joins_with_composite_keys(custom_joins = nil)
if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
construct_joins_without_composite_keys(custom_joins)
else
if @reflection.source_reflection.macro == :belongs_to
reflection_primary_key = @reflection.klass.primary_key
source_primary_key = @reflection.source_reflection.primary_key_name
else
reflection_primary_key = @reflection.source_reflection.primary_key_name
source_primary_key = @reflection.klass.primary_key
end
"INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
@reflection.through_reflection.quoted_table_name,
composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
]
end
end
alias_method_chain :construct_joins, :composite_keys
end
end

View file

@ -0,0 +1,84 @@
module CompositePrimaryKeys
module ActiveRecord
module AttributeMethods #:nodoc:
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
module ClassMethods
# 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
cast_code = "::#{cast_code}" if cast_code && cast_code.match('ActiveRecord::.*')
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
unless self.primary_keys.include?(attr_name.to_sym)
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
# Evaluate the definition for an attribute related method
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
unless primary_keys.include?(method_name.to_sym)
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
# 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_keys.include?(method_name.to_sym)
ids[self.class.primary_keys.index(method_name.to_sym)]
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
end
end
end

View file

@ -0,0 +1,337 @@
module CompositePrimaryKeys
module ActiveRecord #:nodoc:
class CompositeKeyError < StandardError #:nodoc:
end
module Base #:nodoc:
INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
NOT_IMPLEMENTED_YET = 'Not implemented for composite primary keys yet'
def self.append_features(base)
super
base.send(:include, InstanceMethods)
base.extend(ClassMethods)
end
module ClassMethods
def set_primary_keys(*keys)
keys = keys.first if keys.first.is_a?(Array)
keys = keys.map { |k| k.to_sym }
cattr_accessor :primary_keys
self.primary_keys = keys.to_composite_keys
class_eval <<-EOV
extend CompositeClassMethods
include CompositeInstanceMethods
include CompositePrimaryKeys::ActiveRecord::Associations
include CompositePrimaryKeys::ActiveRecord::AssociationPreload
include CompositePrimaryKeys::ActiveRecord::Calculations
include CompositePrimaryKeys::ActiveRecord::AttributeMethods
EOV
end
def composite?
false
end
end
module InstanceMethods
def composite?; self.class.composite?; end
end
module CompositeInstanceMethods
# A model instance's primary keys is always available as model.ids
# whether you name it the default 'id' or set it to something else.
def id
attr_names = self.class.primary_keys
CompositeIds.new(attr_names.map { |attr_name| read_attribute(attr_name) })
end
alias_method :ids, :id
def to_param
id.to_s
end
def id_before_type_cast #:nodoc:
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::NOT_IMPLEMENTED_YET
end
def quoted_id #:nodoc:
[self.class.primary_keys, ids].
transpose.
map {|attr_name,id| quote_value(id, column_for_attribute(attr_name))}.
to_composite_ids
end
# Sets the primary ID.
def id=(ids)
ids = ids.split(ID_SEP) if ids.is_a?(String)
ids.flatten!
unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length
raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"
end
[primary_keys, ids].transpose.each {|key, an_id| write_attribute(key , an_id)}
id
end
# Returns a clone of the record that hasn't been assigned an id yet and
# is treated as a new record. Note that this is a "shallow" clone:
# it copies the object's attributes only, not its associations.
# The extent of a "deep" clone is application-specific and is therefore
# left to the application to implement according to its need.
def clone
attrs = self.attributes_before_type_cast
self.class.primary_keys.each {|key| attrs.delete(key.to_s)}
self.class.new do |record|
record.send :instance_variable_set, '@attributes', attrs
end
end
private
# The xx_without_callbacks methods are overwritten as that is the end of the alias chain
# Creates a new record with values matching those of the instance attributes.
def create_without_callbacks
unless self.id
raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"
end
attributes_minus_pks = attributes_with_quotes(false)
quoted_pk_columns = self.class.primary_key.map { |col| connection.quote_column_name(col) }
cols = quoted_column_names(attributes_minus_pks) << quoted_pk_columns
vals = attributes_minus_pks.values << quoted_id
connection.insert(
"INSERT INTO #{self.class.quoted_table_name} " +
"(#{cols.join(', ')}) " +
"VALUES (#{vals.join(', ')})",
"#{self.class.name} Create",
self.class.primary_key,
self.id
)
@new_record = false
return true
end
# Updates the associated record with values matching those of the instance attributes.
def update_without_callbacks
where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
"(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
end
where_clause = where_clause_terms.join(" AND ")
connection.update(
"UPDATE #{self.class.quoted_table_name} " +
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
"WHERE #{where_clause}",
"#{self.class.name} Update"
)
return true
end
# Deletes the record in the database and freezes this instance to reflect that no changes should
# be made (since they can't be persisted).
def destroy_without_callbacks
where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
"(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
end
where_clause = where_clause_terms.join(" AND ")
unless new_record?
connection.delete(
"DELETE FROM #{self.class.quoted_table_name} " +
"WHERE #{where_clause}",
"#{self.class.name} Destroy"
)
end
freeze
end
end
module CompositeClassMethods
def primary_key; primary_keys; end
def primary_key=(keys); primary_keys = keys; end
def composite?
true
end
#ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
#ids_to_s([[1,2],[7,3]], ',', ';') -> "1,2;7,3"
def ids_to_s(many_ids, id_sep = CompositePrimaryKeys::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
many_ids.map {|ids| "#{left_bracket}#{ids}#{right_bracket}"}.join(list_sep)
end
# Creates WHERE condition from list of composited ids
# User.update_all({:role => 'admin'}, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> UPDATE admins SET admin.role='admin' WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)
# User.find(:all, :conditions => composite_where_clause([[1, 2], [2, 2]])) #=> SELECT * FROM admins WHERE (admin.type=1 AND admin.type2=2) OR (admin.type=2 AND admin.type2=2)
def composite_where_clause(ids)
if ids.is_a?(String)
ids = [[ids]]
elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
ids = [ids.to_composite_ids]
end
ids.map do |id_set|
[primary_keys, id_set].transpose.map do |key, id|
"#{table_name}.#{key.to_s}=#{sanitize(id)}"
end.join(" AND ")
end.join(") OR (")
end
# Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
# Example:
# Person.exists?(5,7)
def exists?(ids)
obj = find(ids) rescue false
!obj.nil? and obj.is_a?(self)
end
# Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
# If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
# are deleted.
def delete(*ids)
unless ids.is_a?(Array); raise "*ids must be an Array"; end
ids = [ids.to_composite_ids] if not ids.first.is_a?(Array)
where_clause = ids.map do |id_set|
[primary_keys, id_set].transpose.map do |key, id|
"#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{sanitize(id)}"
end.join(" AND ")
end.join(") OR (")
delete_all([ "(#{where_clause})" ])
end
# Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
# If an array of ids is provided, all of them are destroyed.
def destroy(*ids)
unless ids.is_a?(Array); raise "*ids must be an Array"; end
if ids.first.is_a?(Array)
ids = ids.map{|compids| compids.to_composite_ids}
else
ids = ids.to_composite_ids
end
ids.first.is_a?(CompositeIds) ? ids.each { |id_set| find(id_set).destroy } : find(ids).destroy
end
# Returns an array of column objects for the table associated with this class.
# Each column that matches to one of the primary keys has its
# primary attribute set to true
def columns
unless @columns
@columns = connection.columns(table_name, "#{name} Columns")
@columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
end
@columns
end
## DEACTIVATED METHODS ##
public
# Lazy-set the sequence name to the connection's default. This method
# is only ever called once since set_sequence_name overrides it.
def sequence_name #:nodoc:
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
end
def reset_sequence_name #:nodoc:
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
end
def set_primary_key(value = nil, &block)
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
end
private
def find_one(id, options)
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
end
def find_some(ids, options)
raise CompositeKeyError, CompositePrimaryKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
end
def find_from_ids(ids, options)
ids = ids.first if ids.last == nil
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
# if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
# if ids is list of lists, then each inner list must follow rule above
if ids.first.is_a? String
# find '2,1' -> ids = ['2,1']
# find '2,1;7,3' -> ids = ['2,1;7,3']
ids = ids.first.split(ID_SET_SEP).map {|id_set| id_set.split(ID_SEP).to_composite_ids}
# find '2,1;7,3' -> ids = [['2','1'],['7','3']], inner [] are CompositeIds
end
ids = [ids.to_composite_ids] if not ids.first.kind_of?(Array)
ids.each do |id_set|
unless id_set.is_a?(Array)
raise "Ids must be in an Array, instead received: #{id_set.inspect}"
end
unless id_set.length == primary_keys.length
raise "#{id_set.inspect}: Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
end
end
# Let keys = [:a, :b]
# If ids = [[10, 50], [11, 51]], then :conditions =>
# "(#{quoted_table_name}.a, #{quoted_table_name}.b) IN ((10, 50), (11, 51))"
conditions = ids.map do |id_set|
[primary_keys, id_set].transpose.map do |key, id|
col = columns_hash[key.to_s]
val = quote_value(id, col)
"#{quoted_table_name}.#{connection.quote_column_name(key.to_s)}=#{val}"
end.join(" AND ")
end.join(") OR (")
options.update :conditions => "(#{conditions})"
result = find_every(options)
if result.size == ids.size
ids.size == 1 ? result[0] : result
else
raise ::ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect})#{conditions}"
end
end
end
end
end
end
module ActiveRecord
ID_SEP = ','
ID_SET_SEP = ';'
class Base
# Allows +attr_name+ to be the list of primary_keys, and returns the id
# of the object
# e.g. @object[@object.class.primary_key] => [1,1]
def [](attr_name)
if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
attr_name = attr_name.split(ID_SEP)
end
attr_name.is_a?(Array) ?
attr_name.map {|name| read_attribute(name)} :
read_attribute(attr_name)
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
# (Alias for the protected write_attribute method).
def []=(attr_name, value)
if attr_name.is_a?(String) and attr_name != attr_name.split(ID_SEP).first
attr_name = attr_name.split(ID_SEP)
end
if attr_name.is_a? Array
value = value.split(ID_SEP) if value.is_a? String
unless value.length == attr_name.length
raise "Number of attr_names and values do not match"
end
#breakpoint
[attr_name, value].transpose.map {|name,val| write_attribute(name.to_s, val)}
else
write_attribute(attr_name, value)
end
end
end
end

View file

@ -0,0 +1,68 @@
module CompositePrimaryKeys
module ActiveRecord
module Calculations
def self.append_features(base)
super
base.send(:extend, ClassMethods)
end
module ClassMethods
def construct_calculation_sql(operation, column_name, options) #:nodoc:
operation = operation.to_s.downcase
options = options.symbolize_keys
scope = scope(:find)
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
aggregate_alias = column_alias_for(operation, column_name)
use_workaround = !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
use_workaround = !connection.supports_count_distinct?
column_name = options[:select] || primary_key.map{ |part| "#{quoted_table_name}.#{connection.quote_column_name(part)}"}.join(',')
end
sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
sql << ", #{connection.quote_column_name(options[:group_field])} AS #{options[:group_alias]}" if options[:group]
sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround
sql << " FROM #{quoted_table_name} "
if merged_includes.any?
join_dependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
add_joins!(sql, options, scope)
add_conditions!(sql, options[:conditions], scope)
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 = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
sql << " GROUP BY #{connection.quote_column_name(options[group_key])} "
end
if options[:group] && options[:having]
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
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
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
sql << ') w1' if use_workaround # assign a dummy table name as required for postgresql
sql
end
end
end
end
end

View file

@ -0,0 +1,30 @@
module CompositePrimaryKeys
ID_SEP = ','
ID_SET_SEP = ';'
module ArrayExtension
def to_composite_keys
CompositeKeys.new(self)
end
def to_composite_ids
CompositeIds.new(self)
end
end
class CompositeArray < Array
def to_s
join(ID_SEP)
end
end
class CompositeKeys < CompositeArray
end
class CompositeIds < CompositeArray
end
end
Array.send(:include, CompositePrimaryKeys::ArrayExtension)

View file

@ -0,0 +1,21 @@
module ActiveRecord
module ConnectionAdapters
class IBM_DBAdapter < AbstractAdapter
# This mightn't be in Core, but count(distinct x,y) doesn't work for me
def supports_count_distinct? #:nodoc:
false
end
alias_method :quote_original, :quote
def quote(value, column = nil)
if value.kind_of?(String) && column && [:integer, :float].include?(column.type)
value = column.type == :integer ? value.to_i : value.to_f
value.to_s
else
quote_original(value, column)
end
end
end
end
end

View file

@ -0,0 +1,15 @@
module ActiveRecord
module ConnectionAdapters
class OracleAdapter < AbstractAdapter
# This mightn't be in Core, but count(distinct x,y) doesn't work for me
def supports_count_distinct? #:nodoc:
false
end
def concat(*columns)
"(#{columns.join('||')})"
end
end
end
end

View file

@ -0,0 +1,53 @@
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
# This mightn't be in Core, but count(distinct x,y) doesn't work for me
def supports_count_distinct? #:nodoc:
false
end
def concat(*columns)
columns = columns.map { |c| "CAST(#{c} AS varchar)" }
"(#{columns.join('||')})"
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)
# Extract the table from the insert sql. Yuck.
table = sql.split(" ", 4)[2].gsub('"', '')
# Try an insert with 'returning id' if available (PG >= 8.2)
if supports_insert_with_returning?
pk, sequence_name = *pk_and_sequence_for(table) unless pk
if pk
quoted_pk = if pk.is_a?(Array)
pk.map { |col| quote_column_name(col) }.join(ID_SEP)
else
quote_column_name(pk)
end
id = select_value("#{sql} RETURNING #{quoted_pk}")
clear_query_cache
return id
end
end
# Otherwise, insert then grab last_insert_id.
if insert_id = super
insert_id
else
# If neither pk nor sequence name is given, look them up.
unless pk || sequence_name
pk, sequence_name = *pk_and_sequence_for(table)
end
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
last_insert_id(table, sequence_name)
end
end
end
end
end
end

View file

@ -0,0 +1,15 @@
require 'active_record/connection_adapters/sqlite_adapter'
module ActiveRecord
module ConnectionAdapters #:nodoc:
class SQLite3Adapter < SQLiteAdapter # :nodoc:
def supports_count_distinct? #:nodoc:
false
end
def concat(*columns)
"(#{columns.join('||')})"
end
end
end
end

View file

@ -0,0 +1,8 @@
class Fixture #:nodoc:
def [](key)
if key.is_a? Array
return key.map { |a_key| self[a_key.to_s] }.to_composite_ids.to_s
end
@fixture[key]
end
end

View file

@ -0,0 +1,20 @@
ActiveRecord::ConnectionAdapters::ColumnDefinition.send(:alias_method, :to_s_without_composite_keys, :to_s)
ActiveRecord::ConnectionAdapters::ColumnDefinition.class_eval <<-'EOF'
def to_s
if name.is_a? Array
"PRIMARY KEY (#{name.join(',')})"
else
to_s_without_composite_keys
end
end
EOF
ActiveRecord::ConnectionAdapters::TableDefinition.class_eval <<-'EOF'
def [](name)
@columns.find { |column|
!column.name.is_a?(Array) && column.name.to_s == name.to_s
}
end
EOF

View file

@ -0,0 +1,19 @@
module ActiveRecord
module Reflection
class AssociationReflection
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 = @primary_key_name.to_composite_keys.to_s if @primary_key_name.is_a? Array
@primary_key_name
end
end
end
end

View file

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

View file

@ -0,0 +1,24 @@
# Load local config files in /local
begin
local_file_supported = Dir[File.join(PROJECT_ROOT, 'local/*.sample')].map { |path| File.basename(path).sub(".sample","") }
local_file_supported.each do |file|
require "local/#{file}"
end
rescue LoadError
puts <<-EOS
This Gem supports local developer extensions in local/ folder.
Supported files:
#{local_file_supported.map { |f| "local/#{f}"}.join(', ')}
Setup default sample files:
rake local:setup
Current warning: #{$!}
EOS
end
# Now load Rake tasks from /tasks
rakefiles = Dir[File.join(File.dirname(__FILE__), "tasks/**/*.rake")]
rakefiles.each { |rakefile| load File.expand_path(rakefile) }

View file

@ -0,0 +1,10 @@
require 'yaml'
ENV['cpk_adapters'] = {
"mysql" => {
:adapter => "mysql",
:username => "root",
:password => "root",
# ...
}
}.to_yaml

View file

@ -0,0 +1,2 @@
# location of folder containing activerecord, railties, etc folders for each Rails gem
ENV['EDGE_RAILS_DIR'] ||= "/path/to/copy/of/edge/rails"

View file

@ -0,0 +1,2 @@
# This file loaded into Rakefile
# Place any extra development tasks you want here

View file

@ -0,0 +1,48 @@
#!/usr/bin/env ruby
#
# if run as script, load the file as library while starting irb
#
if __FILE__ == $0
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
ENV['ADAPTER'] = ARGV[0]
exec "#{irb} -f -r #{$0} --simple-prompt"
end
#
# check if the given adapter is supported (default: mysql)
#
adapters = %w[mysql sqlite oracle oracle_enhanced postgresql ibm_db]
adapter = ENV['ADAPTER'] || 'mysql'
unless adapters.include? adapter
puts "Usage: #{__FILE__} <adapter>"
puts ''
puts 'Adapters: '
puts adapters.map{ |adapter| " #{adapter}" }.join("\n")
exit 1
end
#
# load all necessary libraries
#
require 'rubygems'
require 'local/database_connections'
$LOAD_PATH.unshift 'lib'
begin
require 'local/paths'
$LOAD_PATH.unshift "#{ENV['EDGE_RAILS_DIR']}/activerecord/lib" if ENV['EDGE_RAILS_DIR']
$LOAD_PATH.unshift "#{ENV['EDGE_RAILS_DIR']}/activesupport/lib" if ENV['EDGE_RAILS_DIR']
rescue
end
require 'active_support'
require 'active_record'
require "test/connections/native_#{adapter}/connection"
require 'composite_primary_keys'
PROJECT_ROOT = File.join(File.dirname(__FILE__), '..')
Dir[File.join(PROJECT_ROOT,'test/fixtures/*.rb')].each { |model| require model }

View file

@ -0,0 +1,67 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'redcloth'
require 'syntax/convertors/html'
require 'erb'
require File.dirname(__FILE__) + '/../lib/composite_primary_keys/version.rb'
version = CompositePrimaryKeys::VERSION::STRING
download = 'http://rubyforge.org/projects/compositekeys'
class Fixnum
def ordinal
# teens
return 'th' if (10..19).include?(self % 100)
# others
case self % 10
when 1: return 'st'
when 2: return 'nd'
when 3: return 'rd'
else return 'th'
end
end
end
class Time
def pretty
return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
end
end
def convert_syntax(syntax, source)
return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
end
if ARGV.length >= 1
src, template = ARGV
template ||= File.dirname(__FILE__) + '/../website/template.rhtml'
else
puts("Usage: #{File.split($0).last} source.txt [template.rhtml] > output.html")
exit!
end
template = ERB.new(File.open(template).read)
title = nil
body = nil
File.open(src) do |fsrc|
title_text = fsrc.readline
body_text = fsrc.read
syntax_items = []
body_text.gsub!(%r!<(pre|code)[^>]*?syntax=['"]([^'"]+)[^>]*>(.*?)</\1>!m){
ident = syntax_items.length
element, syntax, source = $1, $2, $3
syntax_items << "<#{element} class=\"syntax\">#{convert_syntax(syntax, source)}</#{element}>"
"syntax-temp-#{ident}"
}
title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
body = RedCloth.new(body_text).to_html
body.gsub!(%r!(?:<pre><code>)?syntax-temp-(\d+)(?:</code></pre>)?!){ syntax_items[$1.to_i] }
end
stat = File.stat(src)
created = stat.ctime
modified = stat.mtime
$stdout << template.result(binding)

View file

@ -0,0 +1,59 @@
#!/usr/bin/env ruby
require 'rubygems'
require 'redcloth'
require 'syntax/convertors/html'
require 'erb'
require 'active_support'
require File.dirname(__FILE__) + '/../lib/composite_primary_keys/version.rb'
version = CompositePrimaryKeys::VERSION::STRING
download = 'http://rubyforge.org/projects/compositekeys'
class Fixnum
def ordinal
# teens
return 'th' if (10..19).include?(self % 100)
# others
case self % 10
when 1: return 'st'
when 2: return 'nd'
when 3: return 'rd'
else return 'th'
end
end
end
class Time
def pretty
return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
end
end
def convert_syntax(syntax, source)
return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^<pre>|</pre>$!,'')
end
if ARGV.length >= 1
src, template = ARGV
template ||= File.dirname(__FILE__) + '/../website/template.js'
else
puts("Usage: #{File.split($0).last} source.txt [template.js] > output.html")
exit!
end
template = ERB.new(File.open(template).read)
title = nil
body = nil
File.open(src) do |fsrc|
title_text = fsrc.readline
body_text = fsrc.read
title = RedCloth.new(title_text).to_html.gsub(%r!<.*?>!,'').strip
body = RedCloth.new(body_text)
end
stat = File.stat(src)
created = stat.ctime
modified = stat.mtime
$stdout << template.result(binding)

View file

@ -0,0 +1,43 @@
namespace :ar do
desc 'Pre-load edge rails ActiveRecord'
task :edge do
unless path = ENV['EDGE_RAILS_DIR'] || ENV['EDGE_RAILS']
puts <<-EOS
Need to define env var EDGE_RAILS_DIR or EDGE_RAILS- root of edge rails on your machine.
i) Get copy of Edge Rails - http://dev.rubyonrails.org
ii) Set EDGE_RAILS_DIR to this folder in local/paths.rb - see local/paths.rb.sample for example
or
a) Set folder from environment or command line (rake ar:edge EDGE_RAILS_DIR=/path/to/rails)
EOS
exit
end
ENV['AR_LOAD_PATH'] = File.join(path, "activerecord/lib")
end
desc 'Pre-load ActiveRecord using VERSION=X.Y.Z, instead of latest'
task :set do
unless version = ENV['VERSION']
puts <<-EOS
Usage: rake ar:get_version VERSION=1.15.3
Specify the version number with VERSION=X.Y.Z; and make sure you have that activerecord gem version installed.
EOS
end
version = nil if version == "" || version == []
begin
version ? gem('activerecord', version) : gem('activerecord')
require 'active_record'
ENV['AR_LOAD_PATH'] = $:.reverse.find { |path| /activerecord/ =~ path }
rescue LoadError
puts <<-EOS
Missing: Cannot find activerecord #{version} installed.
Install: gem install activerecord -v #{version}
EOS
exit
end
end
end

View file

@ -0,0 +1,12 @@
require 'active_record'
# UNTESTED - firebird sqlserver sqlserver_odbc db2 sybase openbase
for adapter in %w( mysql sqlite oracle oracle_enhanced postgresql ibm_db )
Rake::TestTask.new("test_#{adapter}") { |t|
t.libs << "test" << "test/connections/native_#{adapter}"
t.pattern = "test/test_*.rb"
t.verbose = true
}
end
SCHEMA_PATH = File.join(PROJECT_ROOT, *%w(test fixtures db_definitions))

View file

@ -0,0 +1,30 @@
namespace :mysql do
desc 'Build the MySQL test databases'
task :build_databases => :load_connection do
puts File.join(SCHEMA_PATH, 'mysql.sql')
options_str = ENV['cpk_adapter_options_str']
# creates something like "-u#{username} -p#{password} -S#{socket}"
sh %{ mysqladmin #{options_str} create "#{GEM_NAME}_unittest" }
sh %{ mysql #{options_str} "#{GEM_NAME}_unittest" < #{File.join(SCHEMA_PATH, 'mysql.sql')} }
end
desc 'Drop the MySQL test databases'
task :drop_databases => :load_connection do
options_str = ENV['cpk_adapter_options_str']
sh %{ mysqladmin #{options_str} -f drop "#{GEM_NAME}_unittest" }
end
desc 'Rebuild the MySQL test databases'
task :rebuild_databases => [:drop_databases, :build_databases]
task :load_connection do
require File.join(PROJECT_ROOT, %w[lib adapter_helper mysql])
spec = AdapterHelper::MySQL.load_connection_from_env
options = {}
options['u'] = spec[:username] if spec[:username]
options['p'] = spec[:password] if spec[:password]
options['S'] = spec[:sock] if spec[:sock]
options_str = options.map { |key, value| "-#{key}#{value}" }.join(" ")
ENV['cpk_adapter_options_str'] = options_str
end
end

View file

@ -0,0 +1,25 @@
namespace :oracle do
desc 'Build the Oracle test databases'
task :build_databases => :load_connection do
puts File.join(SCHEMA_PATH, 'oracle.sql')
options_str = ENV['cpk_adapter_options_str']
sh %( sqlplus #{options_str} < #{File.join(SCHEMA_PATH, 'oracle.sql')} )
end
desc 'Drop the Oracle test databases'
task :drop_databases => :load_connection do
puts File.join(SCHEMA_PATH, 'oracle.drop.sql')
options_str = ENV['cpk_adapter_options_str']
sh %( sqlplus #{options_str} < #{File.join(SCHEMA_PATH, 'oracle.drop.sql')} )
end
desc 'Rebuild the Oracle test databases'
task :rebuild_databases => [:drop_databases, :build_databases]
task :load_connection do
require File.join(PROJECT_ROOT, %w[lib adapter_helper oracle])
spec = AdapterHelper::Oracle.load_connection_from_env
ENV['cpk_adapter_options_str'] = "#{spec[:username]}/#{spec[:password]}@#{spec[:host]}"
end
end

View file

@ -0,0 +1,26 @@
namespace :postgresql do
desc 'Build the PostgreSQL test databases'
task :build_databases => :load_connection do
sh %{ createdb "#{GEM_NAME}_unittest" }
sh %{ psql "#{GEM_NAME}_unittest" -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} }
end
desc 'Drop the PostgreSQL test databases'
task :drop_databases => :load_connection do
sh %{ dropdb "#{GEM_NAME}_unittest" }
end
desc 'Rebuild the PostgreSQL test databases'
task :rebuild_databases => [:drop_databases, :build_databases]
task :load_connection do
require File.join(PROJECT_ROOT, %w[lib adapter_helper postgresql])
spec = AdapterHelper::Postgresql.load_connection_from_env
options = {}
options['u'] = spec[:username] if spec[:username]
options['p'] = spec[:password] if spec[:password]
options_str = options.map { |key, value| "-#{key}#{value}" }.join(" ")
ENV['cpk_adapter_options_str'] = options_str
end
end

View file

@ -0,0 +1,28 @@
namespace :sqlite3 do
desc 'Build the sqlite test databases'
task :build_databases => :load_connection do
file = File.join(SCHEMA_PATH, 'sqlite.sql')
dbfile = File.join(PROJECT_ROOT, ENV['cpk_adapter_options_str'])
cmd = "mkdir -p #{File.dirname(dbfile)}"
puts cmd
sh %{ #{cmd} }
cmd = "sqlite3 #{dbfile} < #{file}"
puts cmd
sh %{ #{cmd} }
end
desc 'Drop the sqlite test databases'
task :drop_databases => :load_connection do
dbfile = ENV['cpk_adapter_options_str']
sh %{ rm -f #{dbfile} }
end
desc 'Rebuild the sqlite test databases'
task :rebuild_databases => [:drop_databases, :build_databases]
task :load_connection do
require File.join(PROJECT_ROOT, %w[lib adapter_helper sqlite3])
spec = AdapterHelper::Sqlite3.load_connection_from_env
ENV['cpk_adapter_options_str'] = spec[:dbfile]
end
end

View file

@ -0,0 +1,22 @@
desc 'Release the website and new gem version'
task :deploy => [:check_version, :website, :release] do
puts "Remember to create SVN tag:"
puts "svn copy svn+ssh://#{RUBYFORGE_USERNAME}@rubyforge.org/var/svn/#{PATH}/trunk " +
"svn+ssh://#{RUBYFORGE_USERNAME}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
puts "Suggested comment:"
puts "Tagging release #{CHANGES}"
end
desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
task :local_deploy => [:website_generate, :install_gem]
task :check_version do
unless ENV['VERSION']
puts 'Must pass a VERSION=x.y.z release version'
exit
end
unless ENV['VERSION'] == VERS
puts "Please update your version.rb to match the release version, currently #{VERS}"
exit
end
end

View file

@ -0,0 +1,13 @@
namespace :local do
desc 'Copies over the same local files ready for editing'
task :setup do
sample_files = Dir[File.join(PROJECT_ROOT, "local/*.rb.sample")]
sample_files.each do |sample_file|
file = sample_file.sub(".sample","")
unless File.exists?(file)
puts "Copying #{sample_file} -> #{file}"
sh %{ cp #{sample_file} #{file} }
end
end
end
end

View file

@ -0,0 +1,18 @@
desc 'Generate website files'
task :website_generate do
sh %{ ruby scripts/txt2html website/index.txt > website/index.html }
sh %{ ruby scripts/txt2js website/version.txt > website/version.js }
sh %{ ruby scripts/txt2js website/version-raw.txt > website/version-raw.js }
end
desc 'Upload website files to rubyforge'
task :website_upload do
config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
host = "#{config["username"]}@rubyforge.org"
remote_dir = "/var/www/gforge-projects/#{RUBYFORGE_PROJECT}/"
local_dir = 'website'
sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
end
desc 'Generate and upload website files'
task :website => [:website_generate, :website_upload, :publish_docs]

View file

@ -0,0 +1,67 @@
= Composite Primary Keys - Testing Readme
== Testing an adapter
There are tests available for the following adapters:
* ibmdb
* mysql
* oracle
* postgresql
* sqlite
To run the tests for on of the adapters, follow these steps (using mysql in the example):
* rake -T | grep mysql
rake mysql:build_databases # Build the MySQL test databases
rake mysql:drop_databases # Drop the MySQL test databases
rake mysql:rebuild_databases # Rebuild the MySQL test databases
rake test_mysql # Run tests for test_mysql
* rake mysql:build_databases
* rake test_mysql
== Testing against different ActiveRecord versions (or Edge Rails)
ActiveRecord is a RubyGem within Rails, and is constantly being improved/changed on
its repository (http://dev.rubyonrails.org). These changes may create errors for the CPK
gem. So, we need a way to test CPK against Edge Rails, as well as officially released RubyGems.
The default test (as above) uses the latest RubyGem in your cache.
You can select an older RubyGem version by running the following:
* rake ar:set VERSION=1.14.4 test_mysql
== Edge Rails
Before you can test CPK against Edge Rails, you must checkout a copy of edge rails somewhere (see http://dev.rubyonrails.org for for examples)
* cd /path/to/gems
* svn co http://svn.rubyonrails.org/rails/trunk rails
Say the rails folder is /path/to/gems/rails
Three ways to run CPK tests for Edge Rails:
i) Run:
EDGE_RAILS_DIR=/path/to/gems/rails rake ar:edge test_mysql
ii) In your .profile, set the environment variable EDGE_RAILS_DIR=/path/to/gems/rails,
and once you reload your profile, run:
rake ar:edge test_mysql
iii) Store the path in local/paths.rb. Run:
cp local/paths.rb.sample local/paths.rb
# Now set ENV['EDGE_RAILS_DIR']=/path/to/gems/rails
rake ar:edge test_mysql
These are all variations of the same theme:
* Set the environment variable EDGE_RAILS_DIR to the path to Rails (which contains the activerecord/lib folder)
* Run: rake ar:edge test_<adapter>

View file

@ -0,0 +1,94 @@
$:.unshift(ENV['AR_LOAD_PATH']) if ENV['AR_LOAD_PATH']
require 'test/unit'
require 'hash_tricks'
require 'rubygems'
require 'active_record'
require 'active_record/fixtures'
begin
require 'connection'
rescue MissingSourceFile => e
adapter = 'postgresql' #'sqlite'
require "#{File.dirname(__FILE__)}/connections/native_#{adapter}/connection"
end
require 'composite_primary_keys'
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type') unless Object.const_defined?(:QUOTED_TYPE)
class Test::Unit::TestCase #:nodoc:
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
self.use_instantiated_fixtures = false
self.use_transactional_fixtures = true
def assert_date_from_db(expected, actual, message = nil)
# SQL Server doesn't have a separate column type just for dates,
# so the time is in the string and incorrectly formatted
if current_adapter?(:SQLServerAdapter)
assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00")
elsif current_adapter?(:SybaseAdapter)
assert_equal expected.to_s, actual.to_date.to_s, message
else
assert_equal expected.to_s, actual.to_s, message
end
end
def assert_queries(num = 1)
ActiveRecord::Base.connection.class.class_eval do
self.query_count = 0
alias_method :execute, :execute_with_query_counting
end
yield
ensure
ActiveRecord::Base.connection.class.class_eval do
alias_method :execute, :execute_without_query_counting
end
assert_equal num, ActiveRecord::Base.connection.query_count, "#{ActiveRecord::Base.connection.query_count} instead of #{num} queries were executed."
end
def assert_no_queries(&block)
assert_queries(0, &block)
end
cattr_accessor :classes
protected
def testing_with(&block)
classes.keys.each do |@key_test|
@klass_info = classes[@key_test]
@klass, @primary_keys = @klass_info[:class], @klass_info[:primary_keys]
order = @klass.primary_key.is_a?(String) ? @klass.primary_key : @klass.primary_key.join(',')
@first = @klass.find(:first, :order => order)
yield
end
end
def first_id
ids = (1..@primary_keys.length).map {|num| 1}
composite? ? ids.to_composite_ids : ids.first
end
def first_id_str
composite? ? first_id.join(CompositePrimaryKeys::ID_SEP) : first_id.to_s
end
def composite?
@key_test != :single
end
end
def current_adapter?(type)
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters.const_get(type))
end
ActiveRecord::Base.connection.class.class_eval do
cattr_accessor :query_count
alias_method :execute_without_query_counting, :execute
def execute_with_query_counting(sql, name = nil)
self.query_count += 1
execute_without_query_counting(sql, name)
end
end
#ActiveRecord::Base.logger = Logger.new(STDOUT)
#ActiveRecord::Base.colorize_logging = false

View file

@ -0,0 +1,23 @@
print "Using IBM2 \n"
require 'logger'
gem 'ibm_db'
require 'IBM_DB'
RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase frontbase ibm_db )
ActiveRecord::Base.logger = Logger.new("debug.log")
db1 = 'composite_primary_keys_unittest'
connection_options = {
:adapter => "ibm_db",
:database => "ocdpdev",
:username => "db2inst1",
:password => "password",
:host => '192.168.2.21'
}
ActiveRecord::Base.configurations = { db1 => connection_options }
ActiveRecord::Base.establish_connection(connection_options)

View file

@ -0,0 +1,13 @@
print "Using native MySQL\n"
require 'fileutils'
require 'logger'
require 'adapter_helper/mysql'
log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log]))
FileUtils.mkdir_p log_path
puts "Logging to #{log_path}/debug.log"
ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log")
# Adapter config setup in locals/database_connections.rb
connection_options = AdapterHelper::MySQL.load_connection_from_env
ActiveRecord::Base.establish_connection(connection_options)

View file

@ -0,0 +1,14 @@
print "Using native Oracle\n"
require 'fileutils'
require 'logger'
require 'adapter_helper/oracle'
log_path = File.expand_path(File.join(File.dirname(__FILE__), %w[.. .. .. log]))
FileUtils.mkdir_p log_path
puts "Logging to #{log_path}/debug.log"
ActiveRecord::Base.logger = Logger.new("#{log_path}/debug.log")
# Adapter config setup in locals/database_connections.rb
connection_options = AdapterHelper::Oracle.load_connection_from_env
puts connection_options.inspect
ActiveRecord::Base.establish_connection(connection_options)

View file

@ -0,0 +1,9 @@
print "Using native Postgresql\n"
require 'logger'
require 'adapter_helper/postgresql'
ActiveRecord::Base.logger = Logger.new("debug.log")
# Adapter config setup in locals/database_connections.rb
connection_options = AdapterHelper::Postgresql.load_connection_from_env
ActiveRecord::Base.establish_connection(connection_options)

View file

@ -0,0 +1,9 @@
print "Using native Sqlite3\n"
require 'logger'
require 'adapter_helper/sqlite3'
ActiveRecord::Base.logger = Logger.new("debug.log")
# Adapter config setup in locals/database_connections.rb
connection_options = AdapterHelper::Sqlite3.load_connection_from_env
ActiveRecord::Base.establish_connection(connection_options)

View file

@ -0,0 +1,5 @@
class Article < ActiveRecord::Base
has_many :readings
has_many :users, :through => :readings
end

View file

@ -0,0 +1,6 @@
first:
id: 1
name: Article One
second:
id: 2
name: Article Two

View file

@ -0,0 +1,6 @@
class Comment < ActiveRecord::Base
set_primary_keys :id
belongs_to :person, :polymorphic => true
belongs_to :hack
end

View file

@ -0,0 +1,16 @@
comment1:
id: 1
person_id: 1
person_type: Employee
comment2:
id: 2
person_id: 1
person_type: User
hack_id: andrew
comment3:
id: 3
person_id: andrew
person_type: Hack

View file

@ -0,0 +1,113 @@
CREATE TABLE reference_types (
reference_type_id integer NOT NULL generated by default as identity (start with 100, increment by 1, no cache),
type_label varchar(50) default NULL,
abbreviation varchar(50) default NULL,
description varchar(50) default NULL,
PRIMARY KEY (reference_type_id)
);
CREATE TABLE reference_codes (
reference_type_id integer,
reference_code integer NOT NULL,
code_label varchar(50) default NULL,
abbreviation varchar(50) default NULL,
description varchar(50) default NULL,
PRIMARY KEY (reference_type_id,reference_code)
);
CREATE TABLE products (
id integer NOT NULL,
name varchar(50) default NULL,
PRIMARY KEY (id)
);
CREATE TABLE tariffs (
tariff_id integer NOT NULL,
start_date date NOT NULL,
amount integer default NULL,
PRIMARY KEY (tariff_id,start_date)
);
CREATE TABLE product_tariffs (
product_id integer NOT NULL,
tariff_id integer NOT NULL,
tariff_start_date date NOT NULL,
PRIMARY KEY (product_id,tariff_id,tariff_start_date)
);
CREATE TABLE suburbs (
city_id integer NOT NULL,
suburb_id integer NOT NULL,
name varchar(50) NOT NULL,
PRIMARY KEY (city_id,suburb_id)
);
CREATE TABLE streets (
id integer NOT NULL ,
city_id integer NOT NULL,
suburb_id integer NOT NULL,
name varchar(50) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE users (
id integer NOT NULL ,
name varchar(50) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE articles (
id integer NOT NULL ,
name varchar(50) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE readings (
id integer NOT NULL ,
user_id integer NOT NULL,
article_id integer NOT NULL,
rating integer NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE groups (
id integer NOT NULL ,
name varchar(50) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE memberships (
user_id integer NOT NULL,
group_id integer NOT NULL,
PRIMARY KEY (user_id,group_id)
);
CREATE TABLE membership_statuses (
id integer NOT NULL ,
user_id integer NOT NULL,
group_id integer NOT NULL,
status varchar(50) NOT NULL,
PRIMARY KEY (id)
);
create table kitchen_sinks (
id_1 integer not null,
id_2 integer not null,
a_date date,
a_string varchar(100),
primary key (id_1, id_2)
);
create table restaurants (
franchise_id integer not null,
store_id integer not null,
name varchar(100),
primary key (franchise_id, store_id)
);
create table restaurants_suburbs (
franchise_id integer not null,
store_id integer not null,
city_id integer not null,
suburb_id integer not null
);

View file

@ -0,0 +1,16 @@
drop table MEMBERSHIPS;
drop table REFERENCE_CODES;
drop table TARIFFS;
drop table ARTICLES;
drop table GROUPS;
drop table MEMBERSHIP_STATUSES;
drop table READINGS;
drop table REFERENCE_TYPES;
drop table STREETS;
drop table PRODUCTS;
drop table USERS;
drop table SUBURBS;
drop table PRODUCT_TARIFFS;
drop table KITCHEN_SINK;
drop table RESTAURANTS;
drop table RESTAURANTS_SUBURBS;

View file

@ -0,0 +1,174 @@
create table reference_types (
reference_type_id int(11) not null auto_increment,
type_label varchar(50) default null,
abbreviation varchar(50) default null,
description varchar(50) default null,
primary key (reference_type_id)
) type=InnoDB;
create table reference_codes (
reference_type_id int(11),
reference_code int(11) not null,
code_label varchar(50) default null,
abbreviation varchar(50) default null,
description varchar(50) default null,
primary key (reference_type_id, reference_code)
) type=InnoDB;
create table products (
id int(11) not null auto_increment,
name varchar(50) default null,
primary key (id)
) type=InnoDB;
create table tariffs (
tariff_id int(11) not null,
start_date date not null,
amount integer(11) default null,
primary key (tariff_id, start_date)
) type=InnoDB;
create table product_tariffs (
product_id int(11) not null,
tariff_id int(11) not null,
tariff_start_date date not null,
primary key (product_id, tariff_id, tariff_start_date)
) type=InnoDB;
create table suburbs (
city_id int(11) not null,
suburb_id int(11) not null,
name varchar(50) not null,
primary key (city_id, suburb_id)
) type=InnoDB;
create table streets (
id int(11) not null auto_increment,
city_id int(11) not null,
suburb_id int(11) not null,
name varchar(50) not null,
primary key (id)
) type=InnoDB;
create table users (
id int(11) not null auto_increment,
name varchar(50) not null,
primary key (id)
) type=InnoDB;
create table articles (
id int(11) not null auto_increment,
name varchar(50) not null,
primary key (id)
) type=InnoDB;
create table readings (
id int(11) not null auto_increment,
user_id int(11) not null,
article_id int(11) not null,
rating int(11) not null,
primary key (id)
) type=InnoDB;
create table groups (
id int(11) not null auto_increment,
name varchar(50) not null,
primary key (id)
) type=InnoDB;
create table memberships (
user_id int(11) not null,
group_id int(11) not null,
primary key (user_id,group_id)
) type=InnoDB;
create table membership_statuses (
id int(11) not null auto_increment,
user_id int(11) not null,
group_id int(11) not null,
status varchar(50) not null,
primary key (id)
) type=InnoDB;
create table departments (
department_id int(11) not null,
location_id int(11) not null,
primary key (department_id, location_id)
) type=InnoDB;
create table employees (
id int(11) not null auto_increment,
department_id int(11) default null,
location_id int(11) default null,
primary key (id)
) type=InnoDB;
create table comments (
id int(11) not null auto_increment,
person_id varchar(100) default null,
person_type varchar(100) default null,
hack_id varchar(100) default null,
primary key (id)
) type=InnoDB;
create table hacks (
name varchar(50) not null,
primary key (name)
) type=InnoDB;
create table kitchen_sinks (
id_1 int(11) not null,
id_2 int(11) not null,
a_date date,
a_string varchar(100),
primary key (id_1, id_2)
) type=InnoDB;
create table restaurants (
franchise_id int(11) not null,
store_id int(11) not null,
name varchar(100),
primary key (franchise_id, store_id)
) type=InnoDB;
create table restaurants_suburbs (
franchise_id int(11) not null,
store_id int(11) not null,
city_id int(11) not null,
suburb_id int(11) not null
) type=InnoDB;
create table dorms (
id int(11) not null auto_increment,
primary key(id)
) type=InnoDB;
create table rooms (
dorm_id int(11) not null,
room_id int(11) not null,
primary key (dorm_id, room_id)
) type=InnoDB;
create table room_attributes (
id int(11) not null auto_increment,
name varchar(50),
primary key(id)
) type=InnoDB;
create table room_attribute_assignments (
dorm_id int(11) not null,
room_id int(11) not null,
room_attribute_id int(11) not null
) type=InnoDB;
create table students (
id int(11) not null auto_increment,
primary key(id)
) type=InnoDB;
create table room_assignments (
student_id int(11) not null,
dorm_id int(11) not null,
room_id int(11) not null
) type=InnoDB;

View file

@ -0,0 +1,39 @@
drop table reference_types;
drop sequence reference_types_seq;
drop table reference_codes;
drop table products;
drop sequence products_seq;
drop table tariffs;
drop table product_tariffs;
drop table suburbs;
drop table streets;
drop sequence streets_seq;
drop table users;
drop sequence users_seq;
drop table articles;
drop sequence articles_seq;
drop table readings;
drop sequence readings_seq;
drop table groups;
drop sequence groups_seq;
drop table memberships;
drop table membership_statuses;
drop sequence membership_statuses_seq;
drop table departments;
drop table employees;
drop sequence employees_seq;
drop table comments;
drop sequence comments_seq;
drop table hacks;
drop table kitchen_sinks;
drop table restaurants;
drop table restaurants_suburbs;
drop table dorms;
drop sequence dorms_seq;
drop table rooms;
drop table room_attributes;
drop sequence room_attributes_seq;
drop table room_attribute_assignments;
drop table room_assignments;
drop table students;
drop sequence students_seq;

View file

@ -0,0 +1,188 @@
create sequence reference_types_seq start with 1000;
create table reference_types (
reference_type_id number(11) primary key,
type_label varchar2(50) default null,
abbreviation varchar2(50) default null,
description varchar2(50) default null
);
create table reference_codes (
reference_type_id number(11),
reference_code number(11),
code_label varchar2(50) default null,
abbreviation varchar2(50) default null,
description varchar2(50) default null
);
create sequence products_seq start with 1000;
create table products (
id number(11) primary key,
name varchar2(50) default null
);
create table tariffs (
tariff_id number(11),
start_date date,
amount number(11) default null,
constraint tariffs_pk primary key (tariff_id, start_date)
);
create table product_tariffs (
product_id number(11),
tariff_id number(11),
tariff_start_date date,
constraint product_tariffs_pk primary key (product_id, tariff_id, tariff_start_date)
);
create table suburbs (
city_id number(11),
suburb_id number(11),
name varchar2(50) not null,
constraint suburbs_pk primary key (city_id, suburb_id)
);
create sequence streets_seq start with 1000;
create table streets (
id number(11) primary key,
city_id number(11) not null,
suburb_id number(11) not null,
name varchar2(50) not null
);
create sequence users_seq start with 1000;
create table users (
id number(11) primary key,
name varchar2(50) not null
);
create sequence articles_seq start with 1000;
create table articles (
id number(11) primary key,
name varchar2(50) not null
);
create sequence readings_seq start with 1000;
create table readings (
id number(11) primary key,
user_id number(11) not null,
article_id number(11) not null,
rating number(11) not null
);
create sequence groups_seq start with 1000;
create table groups (
id number(11) primary key,
name varchar2(50) not null
);
create table memberships (
user_id number(11) not null,
group_id number(11) not null,
constraint memberships_pk primary key (user_id, group_id)
);
create sequence membership_statuses_seq start with 1000;
create table membership_statuses (
id number(11) primary key,
user_id number(11) not null,
group_id number(11) not null,
status varchar2(50) not null
);
create table departments (
department_id number(11) not null,
location_id number(11) not null,
constraint departments_pk primary key (department_id, location_id)
);
create sequence employees_seq start with 1000;
create table employees (
id number(11) not null primary key,
department_id number(11) default null,
location_id number(11) default null
);
create sequence comments_seq start with 1000;
create table comments (
id number(11) not null primary key,
person_id varchar(100) default null,
person_type varchar(100) default null,
hack_id varchar(100) default null
);
create table hacks (
name varchar(50) not null primary key
);
create table kitchen_sinks (
id_1 number(11) not null,
id_2 number(11) not null,
a_date date,
a_string varchar(100),
constraint kitchen_sinks_pk primary key (id_1, id_2)
);
create table restaurants (
franchise_id number(11) not null,
store_id number(11) not null,
name varchar(100),
constraint restaurants_pk primary key (franchise_id, store_id)
);
create table restaurants_suburbs (
franchise_id number(11) not null,
store_id number(11) not null,
city_id number(11) not null,
suburb_id number(11) not null
);
create sequence dorms_seq start with 1000;
create table dorms (
id number(11) not null,
constraint dorms_pk primary key (id)
);
create table rooms (
dorm_id number(11) not null,
room_id number(11) not null,
constraint rooms_pk primary key (dorm_id, room_id)
);
create sequence room_attributes_seq start with 1000;
create table room_attributes (
id number(11) not null,
name varchar(50),
constraint room_attributes_pk primary key (id)
);
create table room_attribute_assignments (
dorm_id number(11) not null,
room_id number(11) not null,
room_attribute_id number(11) not null
);
create sequence students_seq start with 1000;
create table students (
id number(11) not null,
constraint students_pk primary key (id)
);
create table room_assignments (
student_id number(11) not null,
dorm_id number(11) not null,
room_id number(11) not null
);

View file

@ -0,0 +1,199 @@
create sequence public.reference_types_seq start 1000;
create table reference_types (
reference_type_id int default nextval('public.reference_types_seq'),
type_label varchar(50) default null,
abbreviation varchar(50) default null,
description varchar(50) default null,
primary key (reference_type_id)
);
create table reference_codes (
reference_type_id int,
reference_code int not null,
code_label varchar(50) default null,
abbreviation varchar(50) default null,
description varchar(50) default null
);
create sequence public.products_seq start 1000;
create table products (
id int not null default nextval('public.products_seq'),
name varchar(50) default null,
primary key (id)
);
create table tariffs (
tariff_id int not null,
start_date date not null,
amount int default null,
primary key (tariff_id, start_date)
);
create table product_tariffs (
product_id int not null,
tariff_id int not null,
tariff_start_date date not null,
primary key (product_id, tariff_id, tariff_start_date)
);
create table suburbs (
city_id int not null,
suburb_id int not null,
name varchar(50) not null,
primary key (city_id, suburb_id)
);
create sequence public.streets_seq start 1000;
create table streets (
id int not null default nextval('public.streets_seq'),
city_id int not null,
suburb_id int not null,
name varchar(50) not null,
primary key (id)
);
create sequence public.users_seq start 1000;
create table users (
id int not null default nextval('public.users_seq'),
name varchar(50) not null,
primary key (id)
);
create sequence public.articles_seq start 1000;
create table articles (
id int not null default nextval('public.articles_seq'),
name varchar(50) not null,
primary key (id)
);
create sequence public.readings_seq start 1000;
create table readings (
id int not null default nextval('public.readings_seq'),
user_id int not null,
article_id int not null,
rating int not null,
primary key (id)
);
create sequence public.groups_seq start 1000;
create table groups (
id int not null default nextval('public.groups_seq'),
name varchar(50) not null,
primary key (id)
);
create table memberships (
user_id int not null,
group_id int not null,
primary key (user_id, group_id)
);
create sequence public.membership_statuses_seq start 1000;
create table membership_statuses (
id int not null default nextval('public.membership_statuses_seq'),
user_id int not null,
group_id int not null,
status varchar(50) not null,
primary key (id)
);
create table departments (
department_id int not null,
location_id int not null,
primary key (department_id, location_id)
);
create sequence public.employees_seq start 1000;
create table employees (
id int not null default nextval('public.employees_seq'),
department_id int default null,
location_id int default null,
primary key (id)
);
create sequence public.comments_seq start 1000;
create table comments (
id int not null default nextval('public.comments_seq'),
person_id varchar(100) default null,
person_type varchar(100) default null,
hack_id varchar(100) default null,
primary key (id)
);
create table hacks (
name varchar(50) not null,
primary key (name)
);
create table kitchen_sinks (
id_1 int not null,
id_2 int not null,
a_date date,
a_string varchar(100),
primary key (id_1, id_2)
);
create table restaurants (
franchise_id int not null,
store_id int not null,
name varchar(100),
primary key (franchise_id, store_id)
);
create table restaurants_suburbs (
franchise_id int not null,
store_id int not null,
city_id int not null,
suburb_id int not null
);
create sequence public.dorms_seq start 1000;
create table dorms (
id int not null default nextval('public.dorms_seq'),
primary key (id)
);
create table rooms (
dorm_id int not null,
room_id int not null,
primary key (dorm_id, room_id)
);
create sequence public.room_attributes_seq start 1000;
create table room_attributes (
id int not null default nextval('public.room_attributes_seq'),
name varchar(50),
primary key (id)
);
create table room_attribute_assignments (
dorm_id int not null,
room_id int not null,
room_attribute_id int not null
);
create sequence public.students_seq start 1000;
create table students (
id int not null default nextval('public.students_seq'),
primary key (id)
);
create table room_assignments (
student_id int not null,
dorm_id int not null,
room_id int not null
);

View file

@ -0,0 +1,160 @@
create table reference_types (
reference_type_id integer primary key,
type_label varchar(50) default null,
abbreviation varchar(50) default null,
description varchar(50) default null
);
create table reference_codes (
reference_type_id int(11),
reference_code int(11) not null,
code_label varchar(50) default null,
abbreviation varchar(50) default null,
description varchar(50) default null,
primary key (reference_type_id, reference_code)
);
create table products (
id int(11) not null primary key,
name varchar(50) default null
);
create table tariffs (
tariff_id int(11) not null,
start_date date not null,
amount integer(11) default null,
primary key (tariff_id, start_date)
);
create table product_tariffs (
product_id int(11) not null,
tariff_id int(11) not null,
tariff_start_date date not null,
primary key (product_id, tariff_id, tariff_start_date)
);
create table suburbs (
city_id int(11) not null,
suburb_id int(11) not null,
name varchar(50) not null,
primary key (city_id, suburb_id)
);
create table streets (
id integer not null primary key autoincrement,
city_id int(11) not null,
suburb_id int(11) not null,
name varchar(50) not null
);
create table users (
id integer not null primary key autoincrement,
name varchar(50) not null
);
create table articles (
id integer not null primary key autoincrement,
name varchar(50) not null
);
create table readings (
id integer not null primary key autoincrement,
user_id int(11) not null,
article_id int(11) not null,
rating int(11) not null
);
create table groups (
id integer not null primary key autoincrement,
name varchar(50) not null
);
create table memberships (
user_id int not null,
group_id int not null,
primary key (user_id, group_id)
);
create table membership_statuses (
id integer not null primary key autoincrement,
user_id int not null,
group_id int not null,
status varchar(50) not null
);
create table departments (
department_id integer not null,
location_id integer not null,
primary key (department_id, location_id)
);
create table employees (
id integer not null primary key autoincrement,
department_id integer null,
location_id integer null
);
create table comments (
id integer not null primary key autoincrement,
person_id varchar(100) null,
person_type varchar(100) null,
hack_id varchar(100) null
);
create table hacks (
name varchar(50) not null primary key
);
create table kitchen_sinks (
id_1 integer not null,
id_2 integer not null,
a_date date,
a_string varchar(100),
primary key (id_1, id_2)
);
create table restaurants (
franchise_id integer not null,
store_id integer not null,
name varchar(100),
primary key (franchise_id, store_id)
);
create table restaurants_suburbs (
franchise_id integer not null,
store_id integer not null,
city_id integer not null,
suburb_id integer not null
);
create table dorms (
id integer not null primary key autoincrement
);
create table rooms (
dorm_id integer not null,
room_id integer not null,
primary key (dorm_id, room_id)
);
create table room_attributes (
id integer not null primary key autoincrement,
name varchar(50)
);
create table room_attribute_assignments (
dorm_id integer not null,
room_id integer not null,
room_attribute_id integer not null
);
create table students (
id integer not null primary key autoincrement
);
create table room_assignments (
student_id integer not null,
dorm_id integer not null,
room_id integer not null
);

View file

@ -0,0 +1,5 @@
class Department < ActiveRecord::Base
# set_primary_keys *keys - turns on composite key functionality
set_primary_keys :department_id, :location_id
has_many :employees, :foreign_key => [:department_id, :location_id]
end

View file

@ -0,0 +1,3 @@
department1-cpk:
department_id: 1
location_id: 1

View file

@ -0,0 +1,4 @@
class Employee < ActiveRecord::Base
belongs_to :department, :foreign_key => [:department_id, :location_id]
has_many :comments, :as => :person
end

View file

@ -0,0 +1,9 @@
employee1:
id: 1
department_id: 1
location_id: 1
employee2:
id: 2
department_id: 1
location_id: 1

View file

@ -0,0 +1,3 @@
class Group < ActiveRecord::Base
has_many :memberships
end

View file

@ -0,0 +1,3 @@
cpk:
id: 1
name: Composite Primary Keys

View file

@ -0,0 +1,6 @@
class Hack < ActiveRecord::Base
set_primary_keys :name
has_many :comments, :as => :person
has_one :first_comment, :as => :person, :class_name => "Comment"
end

View file

@ -0,0 +1,2 @@
andrew:
name: andrew

View file

@ -0,0 +1,7 @@
class Membership < ActiveRecord::Base
# set_primary_keys *keys - turns on composite key functionality
set_primary_keys :user_id, :group_id
belongs_to :user
belongs_to :group
has_many :statuses, :class_name => 'MembershipStatus', :foreign_key => [:user_id, :group_id]
end

View file

@ -0,0 +1,3 @@
class MembershipStatus < ActiveRecord::Base
belongs_to :membership, :foreign_key => [:user_id, :group_id]
end

View file

@ -0,0 +1,10 @@
santiago-cpk:
id: 1
user_id: 1
group_id: 1
status: Active
drnic-cpk:
id: 2
user_id: 2
group_id: 1
status: Owner

View file

@ -0,0 +1,6 @@
santiago-cpk:
user_id: 1
group_id: 1
drnic-cpk:
user_id: 2
group_id: 1

View file

@ -0,0 +1,7 @@
class Product < ActiveRecord::Base
set_primary_keys :id # redundant
has_many :product_tariffs, :foreign_key => :product_id
has_one :product_tariff, :foreign_key => :product_id
has_many :tariffs, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
end

View file

@ -0,0 +1,5 @@
class ProductTariff < ActiveRecord::Base
set_primary_keys :product_id, :tariff_id, :tariff_start_date
belongs_to :product, :foreign_key => :product_id
belongs_to :tariff, :foreign_key => [:tariff_id, :tariff_start_date]
end

View file

@ -0,0 +1,12 @@
first_flat:
product_id: 1
tariff_id: 1
tariff_start_date: <%= Date.today.to_s(:db) %>
first_free:
product_id: 1
tariff_id: 2
tariff_start_date: <%= Date.today.to_s(:db) %>
second_free:
product_id: 2
tariff_id: 2
tariff_start_date: <%= Date.today.to_s(:db) %>

View file

@ -0,0 +1,6 @@
first_product:
id: 1
name: Product One
second_product:
id: 2
name: Product Two

View file

@ -0,0 +1,4 @@
class Reading < ActiveRecord::Base
belongs_to :article
belongs_to :user
end

View file

@ -0,0 +1,10 @@
santiago_first:
id: 1
user_id: 1
article_id: 1
rating: 4
santiago_second:
id: 2
user_id: 1
article_id: 2
rating: 5

View file

@ -0,0 +1,7 @@
class ReferenceCode < ActiveRecord::Base
set_primary_keys :reference_type_id, :reference_code
belongs_to :reference_type, :foreign_key => "reference_type_id"
validates_presence_of :reference_code, :code_label, :abbreviation
end

View file

@ -0,0 +1,28 @@
name_prefix_mr:
reference_type_id: 1
reference_code: 1
code_label: MR
abbreviation: Mr
name_prefix_mrs:
reference_type_id: 1
reference_code: 2
code_label: MRS
abbreviation: Mrs
name_prefix_ms:
reference_type_id: 1
reference_code: 3
code_label: MS
abbreviation: Ms
gender_male:
reference_type_id: 2
reference_code: 1
code_label: MALE
abbreviation: Male
gender_female:
reference_type_id: 2
reference_code: 2
code_label: FEMALE
abbreviation: Female

View file

@ -0,0 +1,7 @@
class ReferenceType < ActiveRecord::Base
set_primary_key :reference_type_id
has_many :reference_codes, :foreign_key => "reference_type_id"
validates_presence_of :type_label, :abbreviation
validates_uniqueness_of :type_label
end

View file

@ -0,0 +1,9 @@
name_prefix:
reference_type_id: 1
type_label: NAME_PREFIX
abbreviation: Name Prefix
gender:
reference_type_id: 2
type_label: GENDER
abbreviation: Gender

View file

@ -0,0 +1,3 @@
class Street < ActiveRecord::Base
belongs_to :suburb, :foreign_key => [:city_id, :suburb_id]
end

View file

@ -0,0 +1,15 @@
first:
id: 1
city_id: 1
suburb_id: 1
name: First Street
second1:
id: 2
city_id: 2
suburb_id: 1
name: First Street
second2:
id: 3
city_id: 2
suburb_id: 1
name: Second Street

View file

@ -0,0 +1,6 @@
class Suburb < ActiveRecord::Base
set_primary_keys :city_id, :suburb_id
has_many :streets, :foreign_key => [:city_id, :suburb_id]
has_many :first_streets, :foreign_key => [:city_id, :suburb_id],
:class_name => 'Street', :conditions => "streets.name = 'First Street'"
end

View file

@ -0,0 +1,9 @@
first:
city_id: 1
suburb_id: 1
name: First Suburb
second:
city_id: 2
suburb_id: 1
name: Second Suburb

View file

@ -0,0 +1,6 @@
class Tariff < ActiveRecord::Base
set_primary_keys [:tariff_id, :start_date]
has_many :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
has_one :product_tariff, :foreign_key => [:tariff_id, :tariff_start_date]
has_many :products, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
end

View file

@ -0,0 +1,13 @@
flat:
tariff_id: 1
start_date: <%= Date.today.to_s(:db) %>
amount: 50
free:
tariff_id: 2
start_date: <%= Date.today.to_s(:db) %>
amount: 0
flat_future:
tariff_id: 1
start_date: <%= Date.today.next.to_s(:db) %>
amount: 100

View file

@ -0,0 +1,10 @@
class User < ActiveRecord::Base
has_many :readings
has_many :articles, :through => :readings
has_many :comments, :as => :person
has_many :hacks, :through => :comments, :source => :hack
def find_custom_articles
articles.find(:all, :conditions => ["name = ?", "Article One"])
end
end

View file

@ -0,0 +1,6 @@
santiago:
id: 1
name: Santiago
drnic:
id: 2
name: Dr Nic

View file

@ -0,0 +1,34 @@
# From:
# http://www.bigbold.com/snippets/posts/show/2178
# http://blog.caboo.se/articles/2006/06/11/stupid-hash-tricks
#
# An example utilisation of these methods in a controller is:
# def some_action
# # some script kiddie also passed in :bee, which we don't want tampered with _here_.
# @model = Model.create(params.pass(:foo, :bar))
# end
class Hash
# lets through the keys in the argument
# >> {:one => 1, :two => 2, :three => 3}.pass(:one)
# => {:one=>1}
def pass(*keys)
keys = keys.first if keys.first.is_a?(Array)
tmp = self.clone
tmp.delete_if {|k,v| ! keys.include?(k.to_sym) }
tmp.delete_if {|k,v| ! keys.include?(k.to_s) }
tmp
end
# blocks the keys in the arguments
# >> {:one => 1, :two => 2, :three => 3}.block(:one)
# => {:two=>2, :three=>3}
def block(*keys)
keys = keys.first if keys.first.is_a?(Array)
tmp = self.clone
tmp.delete_if {|k,v| keys.include?(k.to_sym) }
tmp.delete_if {|k,v| keys.include?(k.to_s) }
tmp
end
end

View file

@ -0,0 +1,405 @@
module ActionController
# === Action Pack pagination for Active Record collections
#
# The Pagination module aids in the process of paging large collections of
# Active Record objects. It offers macro-style automatic fetching of your
# model for multiple views, or explicit fetching for single actions. And if
# the magic isn't flexible enough for your needs, you can create your own
# paginators with a minimal amount of code.
#
# The Pagination module can handle as much or as little as you wish. In the
# controller, have it automatically query your model for pagination; or,
# if you prefer, create Paginator objects yourself.
#
# Pagination is included automatically for all controllers.
#
# For help rendering pagination links, see
# ActionView::Helpers::PaginationHelper.
#
# ==== Automatic pagination for every action in a controller
#
# class PersonController < ApplicationController
# model :person
#
# paginate :people, :order => 'last_name, first_name',
# :per_page => 20
#
# # ...
# end
#
# Each action in this controller now has access to a <tt>@people</tt>
# instance variable, which is an ordered collection of model objects for the
# current page (at most 20, sorted by last name and first name), and a
# <tt>@person_pages</tt> Paginator instance. The current page is determined
# by the <tt>params[:page]</tt> variable.
#
# ==== Pagination for a single action
#
# def list
# @person_pages, @people =
# paginate :people, :order => 'last_name, first_name'
# end
#
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
# and <tt>@people</tt> for a single action, and uses the default of 10 items
# per page.
#
# ==== Custom/"classic" pagination
#
# def list
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
# @people = Person.find :all, :order => 'last_name, first_name',
# :limit => @person_pages.items_per_page,
# :offset => @person_pages.current.offset
# end
#
# Explicitly creates the paginator from the previous example and uses
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
#
module Pagination
unless const_defined?(:OPTIONS)
# A hash holding options for controllers using macro-style pagination
OPTIONS = Hash.new
# The default options for pagination
DEFAULT_OPTIONS = {
:class_name => nil,
:singular_name => nil,
:per_page => 10,
:conditions => nil,
:order_by => nil,
:order => nil,
:join => nil,
:joins => nil,
:count => nil,
:include => nil,
:select => nil,
:group => nil,
:parameter => 'page'
}
else
DEFAULT_OPTIONS[:group] = nil
end
def self.included(base) #:nodoc:
super
base.extend(ClassMethods)
end
def self.validate_options!(collection_id, options, in_action) #:nodoc:
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
valid_options = DEFAULT_OPTIONS.keys
valid_options << :actions unless in_action
unknown_option_keys = options.keys - valid_options
raise ActionController::ActionControllerError,
"Unknown options: #{unknown_option_keys.join(', ')}" unless
unknown_option_keys.empty?
options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
end
# Returns a paginator and a collection of Active Record model instances
# for the paginator's current page. This is designed to be used in a
# single action; to automatically paginate multiple actions, consider
# ClassMethods#paginate.
#
# +options+ are:
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
# camelizing the singular name
# <tt>:per_page</tt>:: the maximum number of items to include in a
# single page. Defaults to 10
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
# Model.count
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
# and Model.count
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
#
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
#
# <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
#
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, true)
paginator_and_collection_for(collection_id, options)
end
# These methods become class methods on any controller
module ClassMethods
# Creates a +before_filter+ which automatically paginates an Active
# Record model for all actions in a controller (or certain actions if
# specified with the <tt>:actions</tt> option).
#
# +options+ are the same as PaginationHelper#paginate, with the addition
# of:
# <tt>:actions</tt>:: an array of actions for which the pagination is
# active. Defaults to +nil+ (i.e., every action)
def paginate(collection_id, options={})
Pagination.validate_options!(collection_id, options, false)
module_eval do
before_filter :create_paginators_and_retrieve_collections
OPTIONS[self] ||= Hash.new
OPTIONS[self][collection_id] = options
end
end
end
def create_paginators_and_retrieve_collections #:nodoc:
Pagination::OPTIONS[self.class].each do |collection_id, options|
next unless options[:actions].include? action_name if
options[:actions]
paginator, collection =
paginator_and_collection_for(collection_id, options)
paginator_name = "@#{options[:singular_name]}_pages"
self.instance_variable_set(paginator_name, paginator)
collection_name = "@#{collection_id.to_s}"
self.instance_variable_set(collection_name, collection)
end
end
# Returns the total number of items in the collection to be paginated for
# the +model+ and given +conditions+. Override this method to implement a
# custom counter.
def count_collection_for_pagination(model, options)
model.count(:conditions => options[:conditions],
:joins => options[:join] || options[:joins],
:include => options[:include],
:select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
end
# Returns a collection of items for the given +model+ and +options[conditions]+,
# ordered by +options[order]+, for the current page in the given +paginator+.
# Override this method to implement a custom finder.
def find_collection_for_pagination(model, options, paginator)
model.find(:all, :conditions => options[:conditions],
:order => options[:order_by] || options[:order],
:joins => options[:join] || options[:joins], :include => options[:include],
:select => options[:select], :limit => options[:per_page],
:group => options[:group], :offset => paginator.current.offset)
end
protected :create_paginators_and_retrieve_collections,
:count_collection_for_pagination,
:find_collection_for_pagination
def paginator_and_collection_for(collection_id, options) #:nodoc:
klass = options[:class_name].constantize
page = params[options[:parameter]]
count = count_collection_for_pagination(klass, options)
paginator = Paginator.new(self, count, options[:per_page], page)
collection = find_collection_for_pagination(klass, options, paginator)
return paginator, collection
end
private :paginator_and_collection_for
# A class representing a paginator for an Active Record collection.
class Paginator
include Enumerable
# Creates a new Paginator on the given +controller+ for a set of items
# of size +item_count+ and having +items_per_page+ items per page.
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
# than or equal to zero). The page CGI parameter for links defaults to
# "page" and can be overridden with +page_parameter+.
def initialize(controller, item_count, items_per_page, current_page=1)
raise ArgumentError, 'must have at least one item per page' if
items_per_page <= 0
@controller = controller
@item_count = item_count || 0
@items_per_page = items_per_page
@pages = {}
self.current_page = current_page
end
attr_reader :controller, :item_count, :items_per_page
# Sets the current page number of this paginator. If +page+ is a Page
# object, its +number+ attribute is used as the value; if the page does
# not belong to this Paginator, an ArgumentError is raised.
def current_page=(page)
if page.is_a? Page
raise ArgumentError, 'Page/Paginator mismatch' unless
page.paginator == self
end
page = page.to_i
@current_page_number = has_page_number?(page) ? page : 1
end
# Returns a Page object representing this paginator's current page.
def current_page
@current_page ||= self[@current_page_number]
end
alias current :current_page
# Returns a new Page representing the first page in this paginator.
def first_page
@first_page ||= self[1]
end
alias first :first_page
# Returns a new Page representing the last page in this paginator.
def last_page
@last_page ||= self[page_count]
end
alias last :last_page
# Returns the number of pages in this paginator.
def page_count
@page_count ||= @item_count.zero? ? 1 :
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
end
alias length :page_count
# Returns true if this paginator contains the page of index +number+.
def has_page_number?(number)
number >= 1 and number <= page_count
end
# Returns a new Page representing the page with the given index
# +number+.
def [](number)
@pages[number] ||= Page.new(self, number)
end
# Successively yields all the paginator's pages to the given block.
def each(&block)
page_count.times do |n|
yield self[n+1]
end
end
# A class representing a single page in a paginator.
class Page
include Comparable
# Creates a new Page for the given +paginator+ with the index
# +number+. If +number+ is not in the range of valid page numbers or
# is not a number at all, it defaults to 1.
def initialize(paginator, number)
@paginator = paginator
@number = number.to_i
@number = 1 unless @paginator.has_page_number? @number
end
attr_reader :paginator, :number
alias to_i :number
# Compares two Page objects and returns true when they represent the
# same page (i.e., their paginators are the same and they have the
# same page number).
def ==(page)
return false if page.nil?
@paginator == page.paginator and
@number == page.number
end
# Compares two Page objects and returns -1 if the left-hand page comes
# before the right-hand page, 0 if the pages are equal, and 1 if the
# left-hand page comes after the right-hand page. Raises ArgumentError
# if the pages do not belong to the same Paginator object.
def <=>(page)
raise ArgumentError unless @paginator == page.paginator
@number <=> page.number
end
# Returns the item offset for the first item in this page.
def offset
@paginator.items_per_page * (@number - 1)
end
# Returns the number of the first item displayed.
def first_item
offset + 1
end
# Returns the number of the last item displayed.
def last_item
[@paginator.items_per_page * @number, @paginator.item_count].min
end
# Returns true if this page is the first page in the paginator.
def first?
self == @paginator.first
end
# Returns true if this page is the last page in the paginator.
def last?
self == @paginator.last
end
# Returns a new Page object representing the page just before this
# page, or nil if this is the first page.
def previous
if first? then nil else @paginator[@number - 1] end
end
# Returns a new Page object representing the page just after this
# page, or nil if this is the last page.
def next
if last? then nil else @paginator[@number + 1] end
end
# Returns a new Window object for this page with the specified
# +padding+.
def window(padding=2)
Window.new(self, padding)
end
# Returns the limit/offset array for this page.
def to_sql
[@paginator.items_per_page, offset]
end
def to_param #:nodoc:
@number.to_s
end
end
# A class for representing ranges around a given page.
class Window
# Creates a new Window object for the given +page+ with the specified
# +padding+.
def initialize(page, padding=2)
@paginator = page.paginator
@page = page
self.padding = padding
end
attr_reader :paginator, :page
# Sets the window's padding (the number of pages on either side of the
# window page).
def padding=(padding)
@padding = padding < 0 ? 0 : padding
# Find the beginning and end pages of the window
@first = @paginator.has_page_number?(@page.number - @padding) ?
@paginator[@page.number - @padding] : @paginator.first
@last = @paginator.has_page_number?(@page.number + @padding) ?
@paginator[@page.number + @padding] : @paginator.last
end
attr_reader :padding, :first, :last
# Returns an array of Page objects in the current window.
def pages
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
end
alias to_a :pages
end
end
end
end

View file

@ -0,0 +1,135 @@
module ActionView
module Helpers
# Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally
# also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
#
# <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
# <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
module PaginationHelper
unless const_defined?(:DEFAULT_OPTIONS)
DEFAULT_OPTIONS = {
:name => :page,
:window_size => 2,
:always_show_anchors => true,
:link_to_current_page => false,
:params => {}
}
end
# Creates a basic HTML link bar for the given +paginator+. Links will be created
# for the next and/or previous page and for a number of other pages around the current
# pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
#
# ==== Options
# <tt>:name</tt>:: the routing name for this paginator
# (defaults to +page+)
# <tt>:prefix</tt>:: prefix for pagination links
# (i.e. Older Pages: 1 2 3 4)
# <tt>:suffix</tt>:: suffix for pagination links
# (i.e. 1 2 3 4 <- Older Pages)
# <tt>:window_size</tt>:: the number of pages to show around
# the current page (defaults to <tt>2</tt>)
# <tt>:always_show_anchors</tt>:: whether or not the first and last
# pages should always be shown
# (defaults to +true+)
# <tt>:link_to_current_page</tt>:: whether or not the current page
# should be linked to (defaults to
# +false+)
# <tt>:params</tt>:: any additional routing parameters
# for page URLs
#
# ==== Examples
# # We'll assume we have a paginator setup in @person_pages...
#
# pagination_links(@person_pages)
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :link_to_current_page => true)
# # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :always_show_anchors => false)
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>
#
# pagination_links(@person_pages, :window_size => 1)
# # => 1 <a href="/?page=2/">2</a> ... <a href="/?page=10/">10</a>
#
# pagination_links(@person_pages, :params => { :viewer => "flash" })
# # => 1 <a href="/?page=2&amp;viewer=flash/">2</a> <a href="/?page=3&amp;viewer=flash/">3</a> ...
# # <a href="/?page=10&amp;viewer=flash/">10</a>
def pagination_links(paginator, options={}, html_options={})
name = options[:name] || DEFAULT_OPTIONS[:name]
params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
prefix = options[:prefix] || ''
suffix = options[:suffix] || ''
pagination_links_each(paginator, options, prefix, suffix) do |n|
params[name] = n
link_to(n.to_s, params, html_options)
end
end
# Iterate through the pages of a given +paginator+, invoking a
# block for each page number that needs to be rendered as a link.
#
# ==== Options
# <tt>:window_size</tt>:: the number of pages to show around
# the current page (defaults to +2+)
# <tt>:always_show_anchors</tt>:: whether or not the first and last
# pages should always be shown
# (defaults to +true+)
# <tt>:link_to_current_page</tt>:: whether or not the current page
# should be linked to (defaults to
# +false+)
#
# ==== Example
# # Turn paginated links into an Ajax call
# pagination_links_each(paginator, page_options) do |link|
# options = { :url => {:action => 'list'}, :update => 'results' }
# html_options = { :href => url_for(:action => 'list') }
#
# link_to_remote(link.to_s, options, html_options)
# end
def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
options = DEFAULT_OPTIONS.merge(options)
link_to_current_page = options[:link_to_current_page]
always_show_anchors = options[:always_show_anchors]
current_page = paginator.current_page
window_pages = current_page.window(options[:window_size]).pages
return if window_pages.length <= 1 unless link_to_current_page
first, last = paginator.first, paginator.last
html = ''
html << prefix if prefix
if always_show_anchors and not (wp_first = window_pages[0]).first?
html << yield(first.number)
html << ' ... ' if wp_first.number - first.number > 1
html << ' '
end
window_pages.each do |page|
if current_page == page && !link_to_current_page
html << page.number.to_s
else
html << yield(page.number)
end
html << ' '
end
if always_show_anchors and not (wp_last = window_pages[-1]).last?
html << ' ... ' if last.number - wp_last.number > 1
html << yield(last.number)
end
html << suffix if suffix
html
end
end # PaginationHelper
end # Helpers
end # ActionView

View file

@ -0,0 +1,160 @@
require 'abstract_unit'
require 'fixtures/article'
require 'fixtures/product'
require 'fixtures/tariff'
require 'fixtures/product_tariff'
require 'fixtures/suburb'
require 'fixtures/street'
require 'fixtures/restaurant'
require 'fixtures/dorm'
require 'fixtures/room'
require 'fixtures/room_attribute'
require 'fixtures/room_attribute_assignment'
require 'fixtures/student'
require 'fixtures/room_assignment'
require 'fixtures/user'
require 'fixtures/reading'
class TestAssociations < Test::Unit::TestCase
fixtures :articles, :products, :tariffs, :product_tariffs, :suburbs, :streets, :restaurants, :restaurants_suburbs,
:dorms, :rooms, :room_attributes, :room_attribute_assignments, :students, :room_assignments, :users, :readings
def test_has_many_through_with_conditions_when_through_association_is_not_composite
user = User.find(:first)
assert_equal 1, user.articles.find(:all, :conditions => ["articles.name = ?", "Article One"]).size
end
def test_has_many_through_with_conditions_when_through_association_is_composite
room = Room.find(:first)
assert_equal 0, room.room_attributes.find(:all, :conditions => ["room_attributes.name != ?", "keg"]).size
end
def test_has_many_through_on_custom_finder_when_through_association_is_composite_finder_when_through_association_is_not_composite
user = User.find(:first)
assert_equal 1, user.find_custom_articles.size
end
def test_has_many_through_on_custom_finder_when_through_association_is_composite
room = Room.find(:first)
assert_equal 0, room.find_custom_room_attributes.size
end
def test_count
assert_equal 2, Product.count(:include => :product_tariffs)
assert_equal 3, Tariff.count(:include => :product_tariffs)
assert_equal 2, Tariff.count(:group => :start_date).size
end
def test_products
assert_not_nil products(:first_product).product_tariffs
assert_equal 2, products(:first_product).product_tariffs.length
assert_not_nil products(:first_product).tariffs
assert_equal 2, products(:first_product).tariffs.length
assert_not_nil products(:first_product).product_tariff
end
def test_product_tariffs
assert_not_nil product_tariffs(:first_flat).product
assert_not_nil product_tariffs(:first_flat).tariff
assert_equal Product, product_tariffs(:first_flat).product.class
assert_equal Tariff, product_tariffs(:first_flat).tariff.class
end
def test_tariffs
assert_not_nil tariffs(:flat).product_tariffs
assert_equal 1, tariffs(:flat).product_tariffs.length
assert_not_nil tariffs(:flat).products
assert_equal 1, tariffs(:flat).products.length
assert_not_nil tariffs(:flat).product_tariff
end
# Its not generating the instances of associated classes from the rows
def test_find_includes_products
assert @products = Product.find(:all, :include => :product_tariffs)
assert_equal 2, @products.length
assert_not_nil @products.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array'
assert_equal 3, @products.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length},
"Incorrect number of product_tariffs returned"
end
def test_find_includes_tariffs
assert @tariffs = Tariff.find(:all, :include => :product_tariffs)
assert_equal 3, @tariffs.length
assert_not_nil @tariffs.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array'
assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length},
"Incorrect number of product_tariffs returnedturned"
end
def test_find_includes_product
assert @product_tariffs = ProductTariff.find(:all, :include => :product)
assert_equal 3, @product_tariffs.length
assert_not_nil @product_tariffs.first.instance_variable_get('@product'), '@product not set'
end
def test_find_includes_comp_belongs_to_tariff
assert @product_tariffs = ProductTariff.find(:all, :include => :tariff)
assert_equal 3, @product_tariffs.length
assert_not_nil @product_tariffs.first.instance_variable_get('@tariff'), '@tariff not set'
end
def test_find_includes_extended
assert @products = Product.find(:all, :include => {:product_tariffs => :tariff})
assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@product_tariffs').length},
"Incorrect number of product_tariffs returned"
assert @tariffs = Tariff.find(:all, :include => {:product_tariffs => :product})
assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length},
"Incorrect number of product_tariffs returned"
end
def test_join_where_clause
@product = Product.find(:first, :include => :product_tariffs)
where_clause = @product.product_tariffs.composite_where_clause(
['foo','bar'], [1,2]
)
assert_equal('(foo=1 AND bar=2)', where_clause)
end
def test_has_many_through
@products = Product.find(:all, :include => :tariffs)
assert_equal 3, @products.inject(0) {|sum, product| sum + product.instance_variable_get('@tariffs').length},
"Incorrect number of tariffs returned"
end
def test_has_many_through_when_not_pre_loaded
student = Student.find(:first)
rooms = student.rooms
assert_equal 1, rooms.size
assert_equal 1, rooms.first.dorm_id
assert_equal 1, rooms.first.room_id
end
def test_has_many_through_when_through_association_is_composite
dorm = Dorm.find(:first)
assert_equal 1, dorm.rooms.length
assert_equal 1, dorm.rooms.first.room_attributes.length
assert_equal 'keg', dorm.rooms.first.room_attributes.first.name
end
def test_associations_with_conditions
@suburb = Suburb.find([2, 1])
assert_equal 2, @suburb.streets.size
@suburb = Suburb.find([2, 1])
assert_equal 1, @suburb.first_streets.size
@suburb = Suburb.find([2, 1], :include => :streets)
assert_equal 2, @suburb.streets.size
@suburb = Suburb.find([2, 1], :include => :first_streets)
assert_equal 1, @suburb.first_streets.size
end
def test_has_and_belongs_to_many
@restaurant = Restaurant.find([1,1])
assert_equal 2, @restaurant.suburbs.size
@restaurant = Restaurant.find([1,1], :include => :suburbs)
assert_equal 2, @restaurant.suburbs.size
end
end

View file

@ -0,0 +1,22 @@
require 'abstract_unit'
require 'fixtures/kitchen_sink'
require 'fixtures/reference_type'
class TestAttributeMethods < Test::Unit::TestCase
fixtures :kitchen_sinks, :reference_types
def test_read_attribute_with_single_key
rt = ReferenceType.find(1)
assert_equal(1, rt.reference_type_id)
assert_equal('NAME_PREFIX', rt.type_label)
assert_equal('Name Prefix', rt.abbreviation)
end
def test_read_attribute_with_composite_keys
sink = KitchenSink.find(1,2)
assert_equal(1, sink.id_1)
assert_equal(2, sink.id_2)
assert_equal(Date.today, sink.a_date.to_date)
assert_equal('string', sink.a_string)
end
end

View file

@ -0,0 +1,84 @@
require 'abstract_unit'
require 'fixtures/reference_type'
require 'fixtures/reference_code'
require 'fixtures/product'
require 'fixtures/tariff'
require 'fixtures/product_tariff'
class TestAttributes < Test::Unit::TestCase
fixtures :reference_types, :reference_codes, :products, :tariffs, :product_tariffs
CLASSES = {
:single => {
:class => ReferenceType,
:primary_keys => :reference_type_id,
},
:dual => {
:class => ReferenceCode,
:primary_keys => [:reference_type_id, :reference_code],
},
}
def setup
self.class.classes = CLASSES
end
def test_brackets
testing_with do
@first.attributes.each_pair do |attr_name, value|
assert_equal value, @first[attr_name]
end
end
end
def test_brackets_primary_key
testing_with do
assert_equal @first.id, @first[@primary_keys], "[] failing for #{@klass}"
assert_equal @first.id, @first[@first.class.primary_key]
end
end
def test_brackets_assignment
testing_with do
@first.attributes.each_pair do |attr_name, value|
@first[attr_name]= !value.nil? ? value * 2 : '1'
assert_equal !value.nil? ? value * 2 : '1', @first[attr_name]
end
end
end
def test_brackets_foreign_key_assignment
@flat = Tariff.find(1, Date.today.to_s(:db))
@second_free = ProductTariff.find(2,2,Date.today.to_s(:db))
@second_free_fk = [:tariff_id, :tariff_start_date]
@second_free[key = @second_free_fk] = @flat.id
compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
assert_equal @flat.id, @second_free[key]
@second_free[key = @second_free_fk.to_composite_ids] = @flat.id
assert_equal @flat.id, @second_free[key]
compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
@second_free[key = @second_free_fk.to_composite_ids] = @flat.id.to_s
assert_equal @flat.id, @second_free[key]
compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
@second_free[key = @second_free_fk.to_composite_ids] = @flat.id.to_s
assert_equal @flat.id, @second_free[key]
compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
@second_free[key = @second_free_fk.to_composite_ids.to_s] = @flat.id
assert_equal @flat.id, @second_free[key]
compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
@second_free[key = @second_free_fk.to_composite_ids.to_s] = @flat.id.to_s
assert_equal @flat.id, @second_free[key]
compare_indexes('@flat', @flat.class.primary_key, '@second_free', @second_free_fk)
end
private
def compare_indexes(obj_name1, indexes1, obj_name2, indexes2)
obj1, obj2 = eval "[#{obj_name1}, #{obj_name2}]"
indexes1.length.times do |key_index|
assert_equal obj1[indexes1[key_index].to_s],
obj2[indexes2[key_index].to_s],
"#{obj_name1}[#{indexes1[key_index]}]=#{obj1[indexes1[key_index].to_s].inspect} != " +
"#{obj_name2}[#{indexes2[key_index]}]=#{obj2[indexes2[key_index].to_s].inspect}; " +
"#{obj_name2} = #{obj2.inspect}"
end
end
end

View file

@ -0,0 +1,34 @@
require 'abstract_unit'
require 'fixtures/reference_type'
require 'fixtures/reference_code'
class TestClone < Test::Unit::TestCase
fixtures :reference_types, :reference_codes
CLASSES = {
:single => {
:class => ReferenceType,
:primary_keys => :reference_type_id,
},
:dual => {
:class => ReferenceCode,
:primary_keys => [:reference_type_id, :reference_code],
},
}
def setup
self.class.classes = CLASSES
end
def test_truth
testing_with do
clone = @first.clone
assert_equal @first.attributes.block(@klass.primary_key), clone.attributes
if composite?
@klass.primary_key.each {|key| assert_nil clone[key], "Primary key '#{key}' should be nil"}
else
assert_nil clone[@klass.primary_key], "Sole primary key should be nil"
end
end
end
end

View file

@ -0,0 +1,51 @@
require 'abstract_unit'
require 'fixtures/reference_type'
require 'fixtures/reference_code'
class CompositeArraysTest < Test::Unit::TestCase
def test_new_primary_keys
keys = CompositePrimaryKeys::CompositeKeys.new
assert_not_nil keys
assert_equal '', keys.to_s
assert_equal '', "#{keys}"
end
def test_initialize_primary_keys
keys = CompositePrimaryKeys::CompositeKeys.new([1,2,3])
assert_not_nil keys
assert_equal '1,2,3', keys.to_s
assert_equal '1,2,3', "#{keys}"
end
def test_to_composite_keys
keys = [1,2,3].to_composite_keys
assert_equal CompositePrimaryKeys::CompositeKeys, keys.class
assert_equal '1,2,3', keys.to_s
end
def test_new_ids
keys = CompositePrimaryKeys::CompositeIds.new
assert_not_nil keys
assert_equal '', keys.to_s
assert_equal '', "#{keys}"
end
def test_initialize_ids
keys = CompositePrimaryKeys::CompositeIds.new([1,2,3])
assert_not_nil keys
assert_equal '1,2,3', keys.to_s
assert_equal '1,2,3', "#{keys}"
end
def test_to_composite_ids
keys = [1,2,3].to_composite_ids
assert_equal CompositePrimaryKeys::CompositeIds, keys.class
assert_equal '1,2,3', keys.to_s
end
def test_flatten
keys = [CompositePrimaryKeys::CompositeIds.new([1,2,3]), CompositePrimaryKeys::CompositeIds.new([4,5,6])]
assert_equal 6, keys.flatten.size
end
end

Some files were not shown because too many files have changed in this diff Show more