Submitting a patch to the Open Source project: composite_primary_keys

Posted by Dmytro Shteflyuk on under Ruby & Rails

Not so far ago I have found a weird bug in the Open Source Ruby gem called composite_primary_keys, occurred when you specify :primary_key option for has_one or has_many association. There are two ways to get it fixed: submit an issue and wait till someone will work out this problem or fix it by yourself and then pull request to get the patch merged into the core. This is a great library and I use it in almost all my project, so I decided to help the author and fix this bug by myself. Here I will show you how to do that.

I have discovered, that composite_primary_keys breaks my SQL queries when :primary_key option specified both for has_many and has_one associations.

Step 0. Reproducing the bug

First of all we need to reproduce a bug. Please note: if you know where the problem is, you can skip this step. Let’s start from a simple example:

1
rails cpk_bug && cd cpk_bug

Now we will add dependencies to the config/environment.rb:

1
config.gem 'composite_primary_keys'

and to the config/environments/test.rb:

1
2
3
config.gem 'rspec', :lib => 'spec'
config.gem 'rspec-rails', :lib => false
config.gem 'factory_girl'

Okay. Now we are ready to start. Let’s generate some migrations:

1
2
3
4
5
script/generate rspec_model document upload_request_id:integer title:string description:text
script/generate rspec_model upload_request filename:string state:integer
script/generate rspec_model copyright_request upload_request_id:integer explanation:text
rm -rf spec/fixtures
rake db:migrate && rake db:test:clone

Let’s create our factories:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Factory.define :copyright_request do |cr|
  cr.explanation "This document is copyrighted by O'Reilly"
end

Factory.define :document do |d|
  d.sequence(:title) { |n| "Document #{n}" }
  d.description { |a| "The perfect description for the document '#{a.title}'" }
  d.association :upload_request
end

Factory.define :upload_request do |ur|
  ur.sequence(:filename) { |n| "file#{'%03d' % n}.pdf" }
  ur.state 0
end

And our spec which should fail:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'spec/spec_helper'

describe Document do
  context 'when has copyright requests' do
    before :each do
      # Fake upload request used to desynchronise document and
      # upload request IDs
      Factory(:upload_request)
    end
   
    it 'should not have any copyright requests when just created' do
      @document = Factory(:document)
      @document.copyright_requests.should == []
    end
   
    it 'should return a list of copyright requests from #copyright_requests' do
      @document = Factory(:document)
      @request1 = Factory(:copyright_request, :upload_request => @document.upload_request)
     
      @document.copyright_requests.should == [@request1]
    end
  end
end

Ok, it fails because we haven’t defined our associations on models. Let’s do it:

1
2
3
4
5
6
7
8
9
10
11
12
class Document < ActiveRecord::Base
  belongs_to :upload_request
 
  has_many :copyright_requests, :primary_key => :upload_request_id, :foreign_key => :upload_request_id
end

class UploadRequest < ActiveRecord::Base
end

class CopyrightRequest < ActiveRecord::Base
  belongs_to :upload_request
end

Woohoo! Specs are failing with an error we’re trying to reproduce:

1
2
3
4
5
6
7
8
9
10
11
12
~/cpk_bug$ spec spec
..F.

1)
'Document when has copyright requests should return a list of copyright requests from #copyright_requests' FAILED
expected: [#<CopyrightRequest id: 1, upload_request_id: 2, explanation: "This document is copyrighted by O'Reilly", created_at: "2009-12-07 11:19:11", updated_at: "2009-12-07 11:19:11">],
     got: [] (using ==)
/Users/kpumuk/cpk_bug/spec/models/document_spec.rb:20:

Finished in 0.155708 seconds

4 examples, 1 failure

Step 1. Setting up an environment for composite_primary_keys gem

In general to submit a patch to the Open Source project hosted by GitHub you have to perform the following steps: fork the repository on GitHub, write tests which fail, write a patch, ensure it works, push your changes to your fork repository, and submit a pull request. Let’s do just that!

To fork the repository I will use a perfect github gem by Dr Nic (BTW, he is the author of composite_primary_keys!)

1
2
3
4
sudo gem install github
gh clone drnic/composite_primary_keys
cd composite_primary_keys
gh fork

Now let’s configure our test environment and run tests:

1
2
3
rake local:setup
rake mysql:build_databases
rake test_mysql

You should see something like this:

1
2
3
4
5
6
Using native MySQL
Started
...................................................................................
Finished in 0.487679 seconds.

83 tests, 262 assertions, 0 failures, 0 errors

If you have any problems, check the test/README_tests.txt file for help.

Step 2. Reproducing failing tests inside composite_primary_keys test suite

We are doing TDD, right? So before any fixes we have to write a failing test first. Gem we’re hacking has a powerful test suite with many database tables created, so all we need is just to add associations to one of models, which will cover our issue.

First, add this to the test/fixtures/membership.rb model:

1
2
  has_many :readings, :primary_key => :user_id, :foreign_key => :user_id
  has_one :reading, :primary_key => :user_id, :foreign_key => :user_id, :order => 'id DESC'

And this tests set goes to the test/test_associations.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  def test_has_many_with_primary_key
    @membership = Membership.find([1, 1])
   
    assert_equal 2, @membership.readings.size
  end

  def test_has_one_with_primary_key
    @membership = Membership.find([1, 1])
   
    assert_equal 2, @membership.reading.id
  end

  def test_joins_has_many_with_primary_key
    @membership = Membership.find(:first, :joins => :readings, :conditions => { :readings => { :id => 1 } })
   
    assert_equal [1, 1], @membership.id
  end

  def test_joins_has_one_with_primary_key
    @membership = Membership.find(:first, :joins => :reading, :conditions => { :readings => { :id => 2 } })
   
    assert_equal [1, 1], @membership.id
  end

Now rake test_mysql produces following error (there are 4 of them, I will show only the first one):

1
2
3
4
  1) Error:
test_has_many_with_primary_key(TestAssociations):
ActiveRecord::StatementInvalid: Mysql::Error: Operand should contain 1 column(s): SELECT * FROM `readings` WHERE (`readings`.`user_id` = 1,1)
...

Well, that are the errors we are working on. Time to fix them!

Step 3. Fixing the bug

I will not explain how I fixed that, you can check my commit for details. Here is the diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
diff --git a/lib/composite_primary_keys/associations.rb b/lib/composite_primary_keys/associations.rb
index 6b63664..9a9e173 100644
--- a/lib/composite_primary_keys/associations.rb
+++ b/lib/composite_primary_keys/associations.rb
@@ -180,11 +180,12 @@ module ActiveRecord::Associations::ClassMethods
                 raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
               else
                 foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+                primary_key = options[:primary_key] || parent.primary_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)),
+                    full_keys(parent.aliased_table_name, primary_key)),
                 ]
             end
           when :belongs_to
@@ -338,7 +339,7 @@ module ActiveRecord::Associations
           @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 = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, owner_quoted_id)
           @finder_sql << " AND (#{conditions})" if conditions
       end
 
@@ -386,7 +387,7 @@ module ActiveRecord::Associations
             "#{@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)
+          @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, owner_quoted_id)
       end
 
       @finder_sql << " AND (#{conditions})" if conditions

Run tests to get the following output:

1
2
3
4
5
6
7
~/cpk_bug/composite_primary_keys (master)$ rake test_mysql
Using native MySQL
Started
.......................................................................................
Finished in 0.511129 seconds.

87 tests, 266 assertions, 0 failures, 0 errors

We are done for now!

Step 4. Committing changes and pulling request

It’s time to commit our changes now:

1
2
3
4
5
6
7
8
9
10
11
12
13
~/cpk_bug/composite_primary_keys (master)$ git add .
~/cpk_bug/composite_primary_keys (master)$ git commit -m 'Fixed several bugs in has_one and has_many associations when :primary_key specified'
[master 3e29891] Fixed several bugs in has_one and has_many associations when :primary_key specified
 3 files changed, 31 insertions(+), 3 deletions(-)
~/cpk_bug/composite_primary_keys (master)$ git push
Counting objects: 16, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 2.14 KiB, done.
Total 9 (delta 7), reused 0 (delta 0)
To git@github.com:kpumuk/composite_primary_keys.git
   050d832..3e29891  HEAD -> master
~/cpk_bug/composite_primary_keys (master)$ gh home

Okay, the next step is a pull request. Last command opened a browser window with your fork. Navigate to the latest commit and press the “Pull Request” button (at the time of writing this article gh pull-request didn’t worked, and you can try to fix by yourself to understand the workflow):

Patching the composite_primary_keys gem

That’s all, we have contributed to the community! Today Darrin Holst merged my commit into the core, and you can find it here. Not all things went smooth (I forgot to add a fixture, so tests were failing on first run), but he helped me a lot to get it working. That’s how Open Source works: we help each other to develop high quality software.

Credits

First of all, thanks to Dr Nic for the great plugin, one of the best piece of functionality I can’t imagine life without. Thanks to Darrin Holst for his patience and great help in debugging tests problem, and also for merging my commit into the composite_primary_keys core. Thanks to GitHub for the great Open Source code hosting solution, which makes working on Open Source projects so exciting.

Do you have comments or suggestions? You are welcome! Also, I will be happy if you follow me in Twitter.

No Responses to this entry

Subscribe to comments with RSS or TrackBack to 'Submitting a patch to the Open Source project: composite_primary_keys'.

Post a comment

You can use simple HTML-formatting tags (like <a>, <strong>, <em>, <ul>, <blockquote>, and other). To format your code sample use [cc lang="php"]$a = "hello";[/cc] (allowed languages are ruby, php, yaml, html, csharp, javascript). Also you can use [cc][/cc] block and its syntax would not be highlighted.

Submit Comment