Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial

git-svn-id: svn+ssh://rubyforge.org/var/svn/compositekeys/trunk@1 c9d88e0a-f118-0410-93d6-eb14a717986d
  • Loading branch information...
commit 227dfc9014de3541f426d6f1b747aa13523a933a 0 parents
nicwilliams authored
17 .project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>composite_primary_keys</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.rubypeople.rdt.core.rubybuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.rubypeople.rdt.core.rubynature</nature>
+ </natures>
+</projectDescription>
8 CHANGELOG
@@ -0,0 +1,8 @@
+* 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'
0  README
No changes.
176 Rakefile
@@ -0,0 +1,176 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+require File.join(File.dirname(__FILE__), 'lib', 'composite_primary_keys', 'version')
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'composite_primary_keys'
+PKG_VERSION = CompositePrimayKeys::VERSION::STRING + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+RELEASE_NAME = "REL #{PKG_VERSION}"
+
+RUBY_FORGE_PROJECT = "compositekeys"
+RUBY_FORGE_USER = "nicwilliams"
+
+PKG_FILES = FileList[
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile"
+].exclude(/\bCVS\b|~$/)
+
+
+desc "Default Task"
+task :default => [ :test_mysql ] # UNTESTED =, :test_sqlite, :test_postgresql ]
+
+# Run the unit tests
+
+for adapter in %w( mysql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
+ Rake::TestTask.new("test_#{adapter}") { |t|
+ t.libs << "test" << "test/connections/native_#{adapter}"
+ t.pattern = "test/*_test{,_#{adapter}}.rb"
+ t.verbose = true
+ }
+end
+
+SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions))
+
+desc 'Build the MySQL test databases'
+task :build_mysql_databases do
+ %x( mysqladmin create activerecord_unittest )
+ %x( mysqladmin create activerecord_unittest2 )
+ %x( mysql activerecord_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
+ %x( mysql activerecord_unittest < #{File.join(SCHEMA_PATH, 'mysql2.sql')} )
+end
+
+desc 'Drop the MySQL test databases'
+task :drop_mysql_databases do
+ %x( mysqladmin -f drop activerecord_unittest )
+ %x( mysqladmin -f drop activerecord_unittest2 )
+end
+
+desc 'Rebuild the MySQL test databases'
+task :rebuild_mysql_databases => [:drop_mysql_databases, :build_mysql_databases]
+
+desc 'Build the PostgreSQL test databases'
+task :build_postgresql_databases do
+ %x( createdb activerecord_unittest )
+ %x( createdb activerecord_unittest2 )
+ %x( psql activerecord_unittest -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} )
+ %x( psql activerecord_unittest2 -f #{File.join(SCHEMA_PATH, 'postgresql2.sql')} )
+end
+
+desc 'Drop the PostgreSQL test databases'
+task :drop_postgresql_databases do
+ %x( dropdb activerecord_unittest )
+ %x( dropdb activerecord_unittest2 )
+end
+
+desc 'Rebuild the PostgreSQL test databases'
+task :rebuild_postgresql_databases => [:drop_postgresql_databases, :build_postgresql_databases]
+
+# Generate the RDoc documentation
+
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "Composite Primary Keys -- Composite keys for Active Records/Rails"
+ rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
+ rdoc.rdoc_files.include('README', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('dev-utils/*.rb')
+}
+
+# Enhance rdoc task to copy referenced images also
+task :rdoc do
+ FileUtils.mkdir_p "doc/files/examples/"
+end
+
+
+# Create compressed packages
+
+dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Support for composite primary keys in ActiveRecords"
+ s.description = %q{ActiveRecords only support a single primary key, preventing their use on legacy databases where tables have primary keys over 2+ columns. This solution allows an ActiveRecord to be extended to support multiple keys using the class method set_primary_keys.}
+
+ s.files = [ "Rakefile", "install.rb", "README", "CHANGELOG" ]
+ dist_dirs.each do |dir|
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
+ end
+
+ s.add_dependency('activerecord', '= 1.14.3' + PKG_BUILD)
+
+ s.require_path = 'lib'
+ s.autorequire = 'composite_primary_keys'
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = %w( README )
+ s.rdoc_options.concat ['--main', 'README']
+
+ s.author = "Dr Nic Williams"
+ s.email = "drnicwilliams@gmail.com"
+ s.homepage = "http://composite_primary_keys.rubyforge.org"
+ s.rubyforge_project = "composite_primary_keys"
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = false
+ p.need_zip = false
+end
+
+task :lines do
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
+
+ for file_name in FileList["lib/composite_primary_keys/**/*.rb"]
+ next if file_name =~ /vendor/
+ f = File.open(file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
+
+ total_lines += lines
+ total_codelines += codelines
+
+ lines, codelines = 0, 0
+ end
+
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+end
+
+
+# Publishing ------------------------------------------------------
+
+desc "Publish the beta gem"
+task :pgem => [:package] do
+ Rake::SshFilePublisher.new("drnicwilliams@gmail.com", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh drnicwilliams@gmail.com './gemupdate.sh'`
+end
+
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::SshDirPublisher.new("drnicwilliams@gmail.com", "public_html/ar", "doc").upload
+end
+
+desc "Publish the release files to RubyForge."
+task :release => [ :package ] do
+ `rubyforge login`
+
+ for ext in %w( gem tgz zip )
+ release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
+ puts release_command
+ system(release_command)
+ end
+end
10 debug.log
@@ -0,0 +1,10 @@
+# Logfile created on Tue Jul 18 21:59:37 W. Europe Daylight Time 2006 by logger.rb/1.5.2.7
+ SQL (0.015000) SET NAMES 'utf8'
+ SQL (0.000000) BEGIN
+ SQL (0.000000) ROLLBACK
+ SQL (0.125000) SET NAMES 'utf8'
+ SQL (0.000000) BEGIN
+ SQL (0.000000) ROLLBACK
+ SQL (0.000000) SET NAMES 'utf8'
+ SQL (0.000000) BEGIN
+ SQL (0.000000) ROLLBACK
30 install.rb
@@ -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
+}
41 lib/composite_primary_keys.rb
@@ -0,0 +1,41 @@
+#--
+# 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
+ $:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
+ require 'active_record'
+ rescue LoadError
+ require 'rubygems'
+ require_gem 'activerecord'
+ end
+end
+
+require 'composite_primary_keys/base'
+
+ActiveRecord::Base.class_eval do
+ include CompositePrimayKeys::ActiveRecord::Base
+end
183 lib/composite_primary_keys/base.rb
@@ -0,0 +1,183 @@
+module CompositePrimayKeys
+ module ActiveRecord #:nodoc:
+ module Base #:nodoc:
+ def self.append_features(base)
+ super
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def set_primary_keys(*keys)
+ @@primary_keys = []
+ cattr_accessor :primary_keys
+ self.primary_keys = keys
+
+ class_eval <<-EOV
+ include CompositePrimayKeys::ActiveRecord::Base::InstanceMethods
+ extend CompositePrimayKeys::ActiveRecord::Base::CompositeClassMethods
+ EOV
+ end
+ end
+
+ module InstanceMethods
+
+ # 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
+ attr_names.map do |attr_name|
+ column = column_for_attribute(attr_name)
+ define_read_method(:id, attr_name, column) if self.class.generate_read_methods
+ read_attribute(attr_name)
+ end
+ end
+ alias_method :ids, :id
+
+ # Enables Active Record objects to be used as URL parameters in Action Pack automatically.
+ def to_param
+ id_to_s(ids)
+ end
+
+ def id_before_type_cast #:nodoc:
+ # TODO
+ read_attribute_before_type_cast(self.class.primary_key)
+ end
+
+ def quoted_id #:nodoc:
+ # TODO
+ quote(id, column_for_attribute(self.class.primary_key))
+ end
+
+ # Sets the primary ID.
+ def id=(value)
+ ids = id.split(value) if value.is_a?(String)
+ 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
+ ids.each {|id| write_attribute(self.class.primary_key , id)}
+ end
+ end
+
+ module CompositeClassMethods
+
+ INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
+ ID_SEP = ','
+
+ def primary_keys_to_s(sep = ID_SEP)
+ primary_keys.map(&:to_s).join(sep)
+ 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(ids, id_sep = ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
+ "#{left_bracket}#{ids.map{|id| sanitize(id)}.join('#{id_sep}')}#{right_bracket}"
+ end
+
+ #id_to_s([1,2]) -> "1,2"
+ #id_to_s([1,2], '-') -> "1-2"
+ def id_to_s(ids, id_sep = ID_SEP)
+ "#{ids.map{|id| sanitize(id)}.join('#{id_sep}')}"
+ 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)
+ delete_all([ "(#{primary_keys_to_s}) IN (#{ids_to_s(ids)})" ])
+ 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)
+ ids.first.is_a?(Array) ? ids.each { |id_set| destroy(id_set) } : find(ids).destroy
+ end
+
+ # Alias for the composite primary_keys accessor method
+ def primary_key
+ raise INVALID_FOR_COMPOSITE_KEYS
+ # primary_keys
+ # Initially invalidate the method to find places where its used
+ 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 ##
+
+ # 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 INVALID_FOR_COMPOSITE_KEYS
+ end
+
+ def reset_sequence_name #:nodoc:
+ raise INVALID_FOR_COMPOSITE_KEYS
+ end
+
+ def set_primary_key(value = nil, &block)
+ raise INVALID_FOR_COMPOSITE_KEYS
+ end
+
+ private
+ def find_one(id, options)
+ raise INVALID_FOR_COMPOSITE_KEYS
+ end
+
+ def find_some(ids, options)
+ raise INVALID_FOR_COMPOSITE_KEYS
+ end
+
+ def find_from_ids(ids, options)
+ 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' -> find_from_ids ['2,1']
+ ids = ids[0].split(';').map {|id_set| id_set.split ','} if ids.first.is_a? String
+ ids = [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 "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 =>
+ # "(#{table_name}.a, #{table_name}.b) IN ((10, 50), (11, 51))"
+
+ keys_sql = primary_keys.map {|key| "#{table_name}.#{key.to_s}"}.join(',')
+ ids_sql = ids.map {|id_set| id_set.map {|id| sanitize(id)}.join(',')}.join('),(')
+ options.update :conditions => "(#{keys_sql}) IN ((#{ids_sql}))"
+
+ result = find_every(options)
+
+ if result.size == ids.size
+ result
+ else
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
+ end
+ end
+
+ end
+ end
+ end
+end
9 lib/composite_primary_keys/version.rb
@@ -0,0 +1,9 @@
+module CompositePrimayKeys
+ module VERSION #:nodoc:
+ MAJOR = 0
+ MINOR = 1
+ TINY = 1
+
+ STRING = [MAJOR, MINOR, TINY].join('.')
+ end
+end
BIN  pkg/composite_primary_keys-0.0.1.gem
Binary file not shown
BIN  pkg/composite_primary_keys-0.1.1.gem
Binary file not shown
67 test/abstract_unit.rb
@@ -0,0 +1,67 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+$:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib')
+
+require 'test/unit'
+require 'active_record'
+require 'active_record/fixtures'
+require 'active_support/binding_of_caller'
+require 'active_support/breakpoint'
+require 'connection'
+
+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 = (ENV['AR_NO_TX_FIXTURES'] != "yes")
+
+ def create_fixtures(*table_names, &block)
+ Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures/", table_names, {}, &block)
+ end
+
+ 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
+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
13 test/connections/native_mysql/connection.rb
@@ -0,0 +1,13 @@
+print "Using native MySQL\n"
+require 'logger'
+
+ActiveRecord::Base.logger = Logger.new("debug.log")
+
+db1 = 'composite_primary_keys_unittest'
+
+ActiveRecord::Base.establish_connection(
+ :adapter => "mysql",
+ :username => "root",
+ :encoding => "utf8",
+ :database => db1
+)
8 test/dummy_test.rb
@@ -0,0 +1,8 @@
+require 'abstract_unit'
+
+class DummyTest < Test::Unit::TestCase
+
+ def test_truth
+ assert true
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.