I recently hit upon the Enhanced Migrations plugin by Revolution on Rails. Works great when you develop on branches. The #dump_schema_information method of ActiveRecord::ConnectionAdapters::SchemaStatements only dumps the most recently migration file. Since each migration is now a separate entry in the migrations_info table, we can’t report only the latest one.

To this end, I generated the following diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ svn diff vendor
Index: vendor/plugins/enhanced_migrations/lib/enhanced_migrations.rb
===================================================================
--- vendor/plugins/enhanced_migrations/lib/enhanced_migrations.rb       (revision 7767)
+++ vendor/plugins/enhanced_migrations/lib/enhanced_migrations.rb       (working copy)
@@ -58,8 +58,8 @@
 
 ActiveRecord::ConnectionAdapters::SchemaStatements.send(:define_method, :dump_schema_information) do
   begin
-    if (current_schema = ActiveRecord::Migrator.current_version) > 0
-      return "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} VALUES (#{current_schema}, NOW())" 
+    select_all("SELECT * FROM #{ActiveRecord::Migrator.schema_info_table_name} ORDER BY created_at, id").map do |migration|
+      "INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} VALUES(#{migration["id"]}, '#{migration["created_at"]}');\n"
     end
   rescue ActiveRecord::StatementInvalid 
     # No Schema Info

Hope this is useful for other people.

Well, in relation to my Useful #with_scope technique post, here’s a plugin that implements that idea. This code is used on a production system. It works perfectly for my needs at the moment.

AutoScope

Automatically create scoped access methods on your ActiveRecord models.

Examples

Declare your scopes within your ActiveRecord::Base subclasses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Contact < ActiveRecord::Base
  auto_scope \
      :old => {:find => {:conditions => ["born_on < ?", 30.years.ago]}},
      :young => {:find => {:conditions => ["born_on > ?", 1.year.ago]}}
end

class Testimonial < ActiveRecord::Base
  auto_scope \
      :approved => {
          :find => {:conditions => ["approved_at < ?", proc {Time.now}]},
          :create => {:approved_at => proc {Time.now}}},
      :unapproved => {
          :find => {:conditions => "approved_at IS NULL"},
          :create => {:approved_at => nil}}
end
These declarations give you access to the following scoped methods:
1
2
3
4
Testimonial.approved.count
Testimonial.unapproved.create!(params[:testimonial])
@young_contacts = Contact.young
@contacts = Contact.old.find(:all, :conditions => ["name LIKE ?", params[:name]])

The plugin’s home page is: http://xlsuite.org/plugins/auto_scope The plugin’s Subversion repository is: http://svn.xlsuite.org/plugins/auto_scope

Useful #with_scope technique

March 28th, 2007

I just stumbled upon something very interesting. In my application, we accept anonymous testimonials from the web, but they must not be shown until they have been reviewed and approved by an administrator.

The first API I designed was this:
1
2
3
4
def test_can_get_approved_only
  testimonials = Testimonial.find(:completed)
  assert_equal [@approved_testimonial], testimonials
end

That worked well enough, even after I started adding code to take care of :all and :first, and modifying #count to also use the same scoping rules.

But then, I hit a snag with scoped #has_many accesses. The following failed:
1
2
3
4
def test_can_count_approved_testimonials_for_a_single_contact
  testimonials = @contact.testimonials.count(:completed)
  assert_equal 1, testimonials
end
This generated an invalid statement:
1
2
test_can_count_approved_testimonials_for_a_single_contact(TestimonialTest::AbilityToCountTest):
ActiveRecord::StatementInvalid: Mysql::Error: Unknown column 'approved' in 'where clause': SELECT count(*) AS count_all FROM testimonials WHERE (testimonials.contact_id = 1 AND (approved))
I did a bit of debugging and found that #has_many had already modified the #count parameters even before my #count_with_extensions method was called. So I decided to change my API. I decided I wanted something like this:
1
2
3
4
def test_can_get_approved_only
  testimonials = Testimonial.completed.find
  assert_equal [@approved_testimonial], testimonials
end
This new API first defines the scope, then calls a normal ActiveRecord::Base method. My first implementation was:
1
2
3
4
5
def self.completed
  with_scope(:find => {:conditions => ["approved_at IS NOT NULL"]}) do
    yield self
  end
end
When I ran my tests, my error was immediately apparent:
1
2
test_can_get_approved_only(TestimonialTest::AbilityToCountTest):
LocalJumpError: no block given
Ooops… So I thought a bit more. What I wanted to return from #completed was an object that could stand in for my class (Testimonial in this case), but for which the scope had been defined. I ended up with this implementation:
1
2
3
4
5
6
7
8
9
10
11
12
def completed
  returning Object.new do |proxy|
    class << proxy
      def method_missing(method, *args)
        Testimonial.with_scope(:find => {:conditions => ["approved_at IS NOT NULL"]},
            :create => {:approved_at => Time.now}) do
          Testimonial.send(method, *args)
        end
      end
    end
  end
end

What this implementation does is:

  1. It starts by creating a proxy object;
  2. It redefines #method_missing on that object;
  3. The new #method_missing method creates the scope we originally wanted and calls the method we were originally targetting;
  4. Then the proxy object is returned.
With that implementation, I get something nice:
1
2
3
4
5
6
7
8
$ rake test:recent
(in /home/francois/src)
/usr/local/bin/ruby -Ilib:test "/usr/local/lib/ruby/gems/1.8/gems/rake-0.7.2/lib/rake/rake_test_loader.rb" "test/unit/testimonial_test.rb"
Started
...............
Finished in 3.041738 seconds.

15 tests, 31 assertions, 0 failures, 0 errors

 

Search

A picture of me

I am François Beausoleil, a Ruby on Rails coder. During the day, I work on XLsuite. At night, I am interested many things. Read my biography

Tags

(3) (1) (0) (2) (1) (1) (2) (2) (1) (2) (1) (2) (1) (2) (1) (1) (1) (1) (2) (14) (1) (1) (1) (1) (2) (1) (1) (2) (0) (1) (2) (1) (3) (1) (1) (1) (1) (1) (1) (0) (3) (2) (1) (2) (2) (1) (3) (2) (8) (8) (9) (12) (1) (1) (3) (1) (1) (1) (1) (1) (1) (2) (2) (2) (1) (1) (3) (1) (3) (1) (0) (21) (1) (1) (0) (1) (1) (1) (21) (23) (1) (1) (13) (1) (1) (2) (3) (1) (1) (4) (1) (2) (3) (0) (1) (7) (3) (1) (5) (5) (2) (2) (2) (4) (6) (7) (1) (0) (1) (1) (2) (2) (1) (4) (12) (2) (1) (2) (4) (1) (1) (1) (2) (8) (2) (3) (2) (2) (1) (3) (1) (1)

Links

Projects I work on

Categories

Archives