composite_primary_keys 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ * 0.0.1 * Initial version
2
+ - set_primary_keys(*keys) is the activation class method to transform an ActiveRecord into
3
+ a composite primary key AR
4
+ - find(*ids) supports the passing of
5
+ - id sets: Foo.find(2,1),
6
+ - lists of id sets: Foo.find([2,1], [7,3], [8,12]),
7
+ - and even stringified versions of the above:
8
+ - Foo.find '2,1' or Foo.find '2,1;7,3'
data/README ADDED
File without changes
@@ -0,0 +1,177 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/contrib/rubyforgepublisher'
8
+ require File.join(File.dirname(__FILE__), 'lib', 'composite_primary_keys', 'version')
9
+
10
+ PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
11
+ PKG_NAME = 'composite_primary_keys'
12
+ PKG_VERSION = CompositePrimayKeys::VERSION::STRING + PKG_BUILD
13
+ PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
14
+
15
+ RELEASE_NAME = "REL #{PKG_VERSION}"
16
+
17
+ RUBY_FORGE_PROJECT = "compositekeys"
18
+ RUBY_FORGE_USER = "nicwilliams"
19
+
20
+ PKG_FILES = FileList[
21
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile"
22
+ ].exclude(/\bCVS\b|~$/)
23
+
24
+
25
+ desc "Default Task"
26
+ task :default => [ :test_mysql ] # UNTESTED =, :test_sqlite, :test_postgresql ]
27
+
28
+ # Run the unit tests
29
+
30
+ for adapter in %w( mysql ) # UNTESTED - postgresql sqlite sqlite3 firebird sqlserver sqlserver_odbc db2 oracle sybase openbase )
31
+ Rake::TestTask.new("test_#{adapter}") { |t|
32
+ t.libs << "test" << "test/connections/native_#{adapter}"
33
+ t.pattern = "test/*_test{,_#{adapter}}.rb"
34
+ t.verbose = true
35
+ }
36
+ end
37
+
38
+ SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions))
39
+
40
+ desc 'Build the MySQL test databases'
41
+ task :build_mysql_databases => [:drop_mysql_databases] do
42
+ puts File.join(SCHEMA_PATH, 'mysql.sql')
43
+ %x( mysqladmin -u root create activerecord_unittest )
44
+ #%x( mysqladmin -u root create activerecord_unittest2 )
45
+ %x( mysql -u root activerecord_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
46
+ #%x( mysql -u root activerecord_unittest < #{File.join(SCHEMA_PATH, 'mysql2.sql')} )
47
+ end
48
+
49
+ desc 'Drop the MySQL test databases'
50
+ task :drop_mysql_databases do
51
+ %x( mysqladmin -u root -f drop activerecord_unittest )
52
+ #%x( mysqladmin -u root -f drop activerecord_unittest2 )
53
+ end
54
+
55
+ desc 'Rebuild the MySQL test databases'
56
+ task :rebuild_mysql_databases => [:drop_mysql_databases, :build_mysql_databases]
57
+
58
+ desc 'Build the PostgreSQL test databases'
59
+ task :build_postgresql_databases do
60
+ %x( createdb activerecord_unittest )
61
+ %x( createdb activerecord_unittest2 )
62
+ %x( psql activerecord_unittest -f #{File.join(SCHEMA_PATH, 'postgresql.sql')} )
63
+ %x( psql activerecord_unittest2 -f #{File.join(SCHEMA_PATH, 'postgresql2.sql')} )
64
+ end
65
+
66
+ desc 'Drop the PostgreSQL test databases'
67
+ task :drop_postgresql_databases do
68
+ %x( dropdb activerecord_unittest )
69
+ %x( dropdb activerecord_unittest2 )
70
+ end
71
+
72
+ desc 'Rebuild the PostgreSQL test databases'
73
+ task :rebuild_postgresql_databases => [:drop_postgresql_databases, :build_postgresql_databases]
74
+
75
+ # Generate the RDoc documentation
76
+
77
+ Rake::RDocTask.new { |rdoc|
78
+ rdoc.rdoc_dir = 'doc'
79
+ rdoc.title = "Composite Primary Keys -- Composite keys for Active Records/Rails"
80
+ rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
81
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
82
+ rdoc.rdoc_files.include('README', 'CHANGELOG')
83
+ rdoc.rdoc_files.include('lib/**/*.rb')
84
+ rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
85
+ rdoc.rdoc_files.include('dev-utils/*.rb')
86
+ }
87
+
88
+ # Enhance rdoc task to copy referenced images also
89
+ task :rdoc do
90
+ FileUtils.mkdir_p "doc/files/examples/"
91
+ end
92
+
93
+
94
+ # Create compressed packages
95
+
96
+ dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
97
+
98
+ spec = Gem::Specification.new do |s|
99
+ s.name = PKG_NAME
100
+ s.version = PKG_VERSION
101
+ s.summary = "Support for composite primary keys in ActiveRecords"
102
+ 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.}
103
+
104
+ s.files = [ "Rakefile", "install.rb", "README", "CHANGELOG" ]
105
+ dist_dirs.each do |dir|
106
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
107
+ end
108
+
109
+ s.add_dependency('activerecord', '= 1.14.3' + PKG_BUILD)
110
+
111
+ s.require_path = 'lib'
112
+ s.autorequire = 'composite_primary_keys'
113
+
114
+ s.has_rdoc = true
115
+ s.extra_rdoc_files = %w( README )
116
+ s.rdoc_options.concat ['--main', 'README']
117
+
118
+ s.author = "Dr Nic Williams"
119
+ s.email = "drnicwilliams@gmail.com"
120
+ s.homepage = "http://composite_primary_keys.rubyforge.org"
121
+ s.rubyforge_project = "composite_primary_keys"
122
+ end
123
+
124
+ Rake::GemPackageTask.new(spec) do |p|
125
+ p.gem_spec = spec
126
+ p.need_tar = false
127
+ p.need_zip = false
128
+ end
129
+
130
+ task :lines do
131
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
132
+
133
+ for file_name in FileList["lib/composite_primary_keys/**/*.rb"]
134
+ next if file_name =~ /vendor/
135
+ f = File.open(file_name)
136
+
137
+ while line = f.gets
138
+ lines += 1
139
+ next if line =~ /^\s*$/
140
+ next if line =~ /^\s*#/
141
+ codelines += 1
142
+ end
143
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
144
+
145
+ total_lines += lines
146
+ total_codelines += codelines
147
+
148
+ lines, codelines = 0, 0
149
+ end
150
+
151
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
152
+ end
153
+
154
+
155
+ # Publishing ------------------------------------------------------
156
+
157
+ desc "Publish the beta gem"
158
+ task :pgem => [:package] do
159
+ Rake::SshFilePublisher.new("drnicwilliams@gmail.com", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
160
+ `ssh drnicwilliams@gmail.com './gemupdate.sh'`
161
+ end
162
+
163
+ desc "Publish the API documentation"
164
+ task :pdoc => [:rdoc] do
165
+ Rake::SshDirPublisher.new("drnicwilliams@gmail.com", "public_html/ar", "doc").upload
166
+ end
167
+
168
+ desc "Publish the release files to RubyForge."
169
+ task :release => [ :package ] do
170
+ `ruby scripts/rubyforge login`
171
+
172
+ for ext in %w( gem tgz zip )
173
+ release_command = "ruby scripts/rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
174
+ puts release_command
175
+ system(release_command)
176
+ end
177
+ end
@@ -0,0 +1,30 @@
1
+ require 'rbconfig'
2
+ require 'find'
3
+ require 'ftools'
4
+
5
+ include Config
6
+
7
+ # this was adapted from rdoc's install.rb by ways of Log4r
8
+
9
+ $sitedir = CONFIG["sitelibdir"]
10
+ unless $sitedir
11
+ version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
12
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
13
+ $sitedir = $:.find {|x| x =~ /site_ruby/ }
14
+ if !$sitedir
15
+ $sitedir = File.join($libdir, "site_ruby")
16
+ elsif $sitedir !~ Regexp.quote(version)
17
+ $sitedir = File.join($sitedir, version)
18
+ end
19
+ end
20
+
21
+ # the acual gruntwork
22
+ Dir.chdir("lib")
23
+
24
+ Find.find("composite_primary_keys", "composite_primary_keys.rb") { |f|
25
+ if f[-3..-1] == ".rb"
26
+ File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
27
+ else
28
+ File::makedirs(File.join($sitedir, *f.split(/\//)))
29
+ end
30
+ }
@@ -0,0 +1,41 @@
1
+ #--
2
+ # Copyright (c) 2006 Nic Williams
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__)) unless
25
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
+
27
+ unless defined?(ActiveRecord)
28
+ begin
29
+ $:.unshift(File.dirname(__FILE__) + "/../../activerecord/lib")
30
+ require 'active_record'
31
+ rescue LoadError
32
+ require 'rubygems'
33
+ require_gem 'activerecord'
34
+ end
35
+ end
36
+
37
+ require 'composite_primary_keys/base'
38
+
39
+ ActiveRecord::Base.class_eval do
40
+ include CompositePrimayKeys::ActiveRecord::Base
41
+ end
@@ -0,0 +1,219 @@
1
+ module CompositePrimayKeys
2
+ module ActiveRecord #:nodoc:
3
+ module Base #:nodoc:
4
+
5
+ INVALID_FOR_COMPOSITE_KEYS = 'Not appropriate for composite primary keys'
6
+ ID_SEP = ','
7
+
8
+ def self.append_features(base)
9
+ super
10
+ base.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ def set_primary_keys(*keys)
15
+ @@primary_keys = []
16
+ cattr_accessor :primary_keys
17
+ self.primary_keys = keys
18
+
19
+ class_eval <<-EOV
20
+ include CompositePrimayKeys::ActiveRecord::Base::InstanceMethods
21
+ extend CompositePrimayKeys::ActiveRecord::Base::CompositeClassMethods
22
+ EOV
23
+ end
24
+ end
25
+
26
+ module InstanceMethods
27
+
28
+ # A model instance's primary keys is always available as model.ids
29
+ # whether you name it the default 'id' or set it to something else.
30
+ def id
31
+ attr_names = self.class.primary_keys
32
+ attr_names.map {|attr_name| read_attribute(attr_name)}
33
+ end
34
+ alias_method :ids, :id
35
+
36
+ #id_to_s([1,2]) -> "1,2"
37
+ #id_to_s([1,2], '-') -> "1-2"
38
+ def id_to_s(ids, id_sep = CompositePrimayKeys::ActiveRecord::Base::ID_SEP)
39
+ ids.map{|id| self.class.sanitize(id)}.join("#{id_sep}")
40
+ end
41
+
42
+ # Enables Active Record objects to be used as URL parameters in Action Pack automatically.
43
+ def to_param
44
+ id_to_s(ids)
45
+ end
46
+
47
+ def id_before_type_cast #:nodoc:
48
+ # TODO
49
+ read_attribute_before_type_cast(self.class.primary_key)
50
+ end
51
+
52
+ def quoted_id #:nodoc:
53
+ # TODO
54
+ quote(id, column_for_attribute(self.class.primary_key))
55
+ end
56
+
57
+ # Sets the primary ID.
58
+ def id=(value)
59
+ ids = id.split(value) if value.is_a?(String)
60
+ unless ids.is_a?(Array) and ids.length == self.class.primary_keys.length
61
+ raise "#{self.class}.id= requires #{self.class.primary_keys.length} ids"
62
+ end
63
+ ids.each {|id| write_attribute(self.class.primary_key , id)}
64
+ end
65
+
66
+ # Define an attribute reader method. Cope with nil column.
67
+ def define_read_method(symbol, attr_name, column)
68
+ cast_code = column.type_cast_code('v') if column
69
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
70
+
71
+ unless self.class.primary_keys.include? attr_name.to_sym
72
+ access_code = access_code.insert(0, "raise NoMethodError, 'missing attribute: #{attr_name}', caller unless @attributes.has_key?('#{attr_name}'); ")
73
+ self.class.read_methods << attr_name
74
+ end
75
+
76
+ evaluate_read_method attr_name, "def #{symbol}; #{access_code}; end"
77
+ end
78
+
79
+ def method_missing(method_id, *args, &block)
80
+ method_name = method_id.to_s
81
+ if @attributes.include?(method_name) or
82
+ (md = /\?$/.match(method_name) and
83
+ @attributes.include?(method_name = md.pre_match))
84
+ define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods
85
+ md ? query_attribute(method_name) : read_attribute(method_name)
86
+ elsif self.class.primary_keys.include? method_name.to_sym
87
+ get_attr(method_name.to_sym)
88
+ elsif md = /(=|_before_type_cast)$/.match(method_name)
89
+ attribute_name, method_type = md.pre_match, md.to_s
90
+ if @attributes.include?(attribute_name)
91
+ case method_type
92
+ when '='
93
+ write_attribute(attribute_name, args.first)
94
+ when '_before_type_cast'
95
+ read_attribute_before_type_cast(attribute_name)
96
+ end
97
+ else
98
+ super
99
+ end
100
+ else
101
+ super
102
+ end
103
+ end
104
+ end
105
+
106
+ module CompositeClassMethods
107
+
108
+ def primary_keys_to_s(sep = CompositePrimayKeys::ActiveRecord::Base::ID_SEP)
109
+ primary_keys.map(&:to_s).join(sep)
110
+ end
111
+
112
+ #ids_to_s([[1,2],[7,3]]) -> "(1,2),(7,3)"
113
+ #ids_to_s([[1,2],[7,3]], ',', ';', '', '') -> "1,2;7,3"
114
+ def ids_to_s(ids, id_sep = CompositePrimayKeys::ActiveRecord::Base::ID_SEP, list_sep = ',', left_bracket = '(', right_bracket = ')')
115
+ "#{left_bracket}#{ids.map{|id| sanitize(id)}.join('#{id_sep}')}#{right_bracket}"
116
+ end
117
+
118
+ # Returns true if the given +ids+ represents the primary keys of a record in the database, false otherwise.
119
+ # Example:
120
+ # Person.exists?(5,7)
121
+ def exists?(ids)
122
+ obj = find(ids) rescue false
123
+ !obj.nil? and obj.is_a?(self)
124
+ end
125
+
126
+ # Deletes the record with the given +ids+ without instantiating an object first, e.g. delete(1,2)
127
+ # If an array of ids is provided (e.g. delete([1,2], [3,4]), all of them
128
+ # are deleted.
129
+ def delete(*ids)
130
+ delete_all([ "(#{primary_keys_to_s}) IN (#{ids_to_s(ids)})" ])
131
+ end
132
+
133
+ # Destroys the record with the given +ids+ by instantiating the object and calling #destroy (all the callbacks are the triggered).
134
+ # If an array of ids is provided, all of them are destroyed.
135
+ def destroy(*ids)
136
+ ids.first.is_a?(Array) ? ids.each { |id_set| destroy(id_set) } : find(ids).destroy
137
+ end
138
+
139
+ # Alias for the composite primary_keys accessor method
140
+ def primary_key
141
+ raise CompositePrimayKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
142
+ # primary_keys
143
+ # Initially invalidate the method to find places where its used
144
+ end
145
+
146
+ # Returns an array of column objects for the table associated with this class.
147
+ # Each column that matches to one of the primary keys has its
148
+ # primary attribute set to true
149
+ def columns
150
+ unless @columns
151
+ @columns = connection.columns(table_name, "#{name} Columns")
152
+ @columns.each {|column| column.primary = primary_keys.include?(column.name.to_sym)}
153
+ end
154
+ @columns
155
+ end
156
+
157
+ ## DEACTIVATED METHODS ##
158
+ public
159
+ # Lazy-set the sequence name to the connection's default. This method
160
+ # is only ever called once since set_sequence_name overrides it.
161
+ def sequence_name #:nodoc:
162
+ raise CompositePrimayKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
163
+ end
164
+
165
+ def reset_sequence_name #:nodoc:
166
+ raise CompositePrimayKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
167
+ end
168
+
169
+ def set_primary_key(value = nil, &block)
170
+ raise CompositePrimayKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
171
+ end
172
+
173
+ private
174
+ def find_one(id, options)
175
+ raise CompositePrimayKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
176
+ end
177
+
178
+ def find_some(ids, options)
179
+ raise CompositePrimayKeys::ActiveRecord::Base::INVALID_FOR_COMPOSITE_KEYS
180
+ end
181
+
182
+ def find_from_ids(ids, options)
183
+ conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
184
+ # if ids is just a flat list, then its size must = primary_key.length (one id per primary key, in order)
185
+ # if ids is list of lists, then each inner list must follow rule above
186
+ #if ids.first.is_a?(String) - find '2,1' -> find_from_ids ['2,1']
187
+ ids = ids[0].split(';').map {|id_set| id_set.split ','} if ids.first.is_a? String
188
+ ids = [ids] if not ids.first.kind_of?(Array)
189
+
190
+ ids.each do |id_set|
191
+ unless id_set.is_a?(Array)
192
+ raise "Ids must be in an Array, instead received: #{id_set.inspect}"
193
+ end
194
+ unless id_set.length == primary_keys.length
195
+ raise "Incorrect number of primary keys for #{class_name}: #{primary_keys.inspect}"
196
+ end
197
+ end
198
+
199
+ # Let keys = [:a, :b]
200
+ # If ids = [[10, 50], [11, 51]], then :conditions =>
201
+ # "(#{table_name}.a, #{table_name}.b) IN ((10, 50), (11, 51))"
202
+
203
+ keys_sql = primary_keys.map {|key| "#{table_name}.#{key.to_s}"}.join(',')
204
+ ids_sql = ids.map {|id_set| id_set.map {|id| sanitize(id)}.join(',')}.join('),(')
205
+ options.update :conditions => "(#{keys_sql}) IN ((#{ids_sql}))"
206
+
207
+ result = find_every(options)
208
+
209
+ if result.size == ids.size
210
+ ids.size == 1 ? result[0] : result
211
+ else
212
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions}"
213
+ end
214
+ end
215
+
216
+ end
217
+ end
218
+ end
219
+ end