Permalink
Browse files

Add Sequel adapter.

  • Loading branch information...
1 parent d6fa4f6 commit 5640f5eaf9021be2ce329b602c1119e4be3da0bd John Firebaugh committed Jul 13, 2011
View
@@ -32,6 +32,7 @@ GEM
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
+ sequel (3.25.0)
tzinfo (0.3.28)
PLATFORMS
@@ -45,3 +46,4 @@ DEPENDENCIES
rcov
rdoc
rspec
+ sequel
View
@@ -0,0 +1,16 @@
+require 'sequel'
+require 'machinist'
+require 'machinist/sequel/blueprint'
+require 'machinist/sequel/lathe'
+
+module Sequel #:nodoc:
+ class Model #:nodoc:
+ extend Machinist::Machinable
+
+ def self.blueprint_class
+ Machinist::Sequel::Blueprint
+ end
+
+ attr_accessor :machinist_deferred_associations
+ end
+end
@@ -0,0 +1,22 @@
+module Machinist::Sequel
+ class Blueprint < Machinist::Blueprint
+ # Make and save an object.
+ def make!(attributes = {})
+ object = make(attributes)
+ object.save(:raise_on_failure => true)
+
+ deferred_associations = object.machinist_deferred_associations
+ if deferred_associations
+ deferred_associations.each do |association, array|
+ array.each {|e| object.send(association.add_method, e) }
+ end
+ end
+
+ object
+ end
+
+ def lathe_class #:nodoc:
+ Machinist::Sequel::Lathe
+ end
+ end
+end
@@ -0,0 +1,35 @@
+module Machinist::Sequel
+ class Lathe < Machinist::Lathe
+ def make_one_value(attribute, args) #:nodoc:
+ if block_given?
+ raise_argument_error(attribute) unless args.empty?
+ yield
+ else
+ make_association(attribute, args)
+ end
+ end
+
+ def make_association(attribute, args) #:nodoc:
+ association = @klass.association_reflection(attribute)
+ if association
+ association.associated_class.make(*args)
+ else
+ raise_argument_error(attribute)
+ end
+ end
+
+ def assign_attribute(attribute, value)
+ association = @klass.association_reflection(attribute)
+ if association
+ if association.returns_array?
+ (object.machinist_deferred_associations ||= []) << [association, value]
+ elsif value.new?
+ value.save(:raise_on_failure => true)
+ super
+ end
+ else
+ super
+ end
+ end
+ end
+end
View
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
s.require_paths = ["lib"]
s.add_development_dependency "activerecord"
+ s.add_development_dependency "sequel"
s.add_development_dependency "mysql"
s.add_development_dependency "rake"
s.add_development_dependency "rcov"
@@ -85,7 +85,7 @@
username { "user_#{sn}" }
end
Post.blueprint do
- author { User.make(:username => "post_author_#{sn}") }
+ author { ActiveRecordEnvironment::User.make(:username => "post_author_#{sn}") }
end
post = Post.make!
post.should be_a(Post)
View
@@ -0,0 +1,108 @@
+require File.dirname(__FILE__) + '/spec_helper'
+require 'support/sequel_environment'
+
+describe Machinist::Sequel do
+ include SequelEnvironment
+
+ before(:each) do
+ empty_database!
+ end
+
+ context "make" do
+ it "returns an unsaved object" do
+ Post.blueprint { }
+ post = Post.make
+ post.should be_a(Post)
+ post.should be_new
+ end
+ end
+
+ context "make!" do
+ it "makes and saves objects" do
+ Post.blueprint { }
+ post = Post.make!
+ post.should be_a(Post)
+ post.should_not be_new
+ end
+
+ it "raises an exception for an invalid object" do
+ User.blueprint { }
+ lambda {
+ User.make!(:username => "")
+ }.should raise_error(Sequel::ValidationFailed)
+ end
+ end
+
+ context "associations support" do
+ it "handles many_to_one associations" do
+ User.blueprint do
+ username { "user_#{sn}" }
+ end
+ Post.blueprint do
+ author
+ end
+ post = Post.make!
+ post.should be_a(Post)
+ post.should_not be_new
+ post.author.should be_a(User)
+ post.author.should_not be_new
+ end
+
+ it "handles one_to_many associations" do
+ Post.blueprint do
+ comments(3)
+ end
+ Comment.blueprint { }
+ post = Post.make!
+ post.should be_a(Post)
+ post.should_not be_new
+ post.should have(3).comments
+ post.comments.each do |comment|
+ comment.should be_a(Comment)
+ comment.should_not be_new
+ end
+ end
+
+ it "handles many_to_many associations" do
+ Post.blueprint do
+ tags(3)
+ end
+ Tag.blueprint do
+ name { "tag_#{sn}" }
+ end
+ post = Post.make!
+ post.should be_a(Post)
+ post.should_not be_new
+ post.should have(3).tags
+ post.tags.each do |tag|
+ tag.should be_a(Tag)
+ tag.should_not be_new
+ end
+ end
+
+ it "handles overriding associations" do
+ User.blueprint do
+ username { "user_#{sn}" }
+ end
+ Post.blueprint do
+ author { SequelEnvironment::User.make(:username => "post_author_#{sn}") }
+ end
+ post = Post.make!
+ post.should be_a(Post)
+ post.should_not be_new
+ post.author.should be_a(User)
+ post.author.should_not be_new
+ post.author.username.should =~ /^post_author_\d+$/
+ end
+ end
+
+ context "error handling" do
+ it "raises an exception for an attribute with no value" do
+ User.blueprint { username }
+ lambda {
+ User.make
+ }.should raise_error(ArgumentError)
+ end
+ end
+
+end
@@ -34,26 +34,27 @@
end
end
-class User < ActiveRecord::Base
- validates_presence_of :username
- validates_uniqueness_of :username
-end
-class Post < ActiveRecord::Base
- has_many :comments
- belongs_to :author, :class_name => "User"
- has_and_belongs_to_many :tags
-end
+module ActiveRecordEnvironment
-class Comment < ActiveRecord::Base
- belongs_to :post
-end
+ class User < ActiveRecord::Base
+ validates_presence_of :username
+ validates_uniqueness_of :username
+ end
-class Tag < ActiveRecord::Base
- has_and_belongs_to_many :posts
-end
+ class Post < ActiveRecord::Base
+ has_many :comments
+ belongs_to :author, :class_name => "User"
+ has_and_belongs_to_many :tags
+ end
-module ActiveRecordEnvironment
+ class Comment < ActiveRecord::Base
+ belongs_to :post
+ end
+
+ class Tag < ActiveRecord::Base
+ has_and_belongs_to_many :posts
+ end
def empty_database!
[User, Post, Comment].each do |klass|
@@ -0,0 +1,73 @@
+require 'sequel'
+require 'sequel/extensions/migration'
+require 'machinist/sequel'
+
+DB = Sequel.mysql(:database => 'machinist')
+
+Sequel.migration do
+ change do
+ create_table! :users do
+ primary_key :id
+ String :username
+ end
+
+ create_table! :posts do
+ primary_key :id
+ String :title
+ Integer :author_id
+ String :body, :text => true
+ end
+
+ create_table! :comments do
+ primary_key :id
+ Integer :post_id
+ String :body, :text => true
+ end
+
+ create_table! :tags do
+ primary_key :id
+ String :name
+ end
+
+ create_table! :posts_tags do
+ Integer :post_id
+ Integer :tag_id
+ end
+ end
+end
+
+
+module SequelEnvironment
+
+ class User < Sequel::Model
+ plugin :validation_helpers
+
+ def validate
+ super
+ validates_presence :username
+ validates_unique :username
+ end
+ end
+
+ class Post < Sequel::Model
+ one_to_many :comments
+ many_to_one :author, :class_name => "SequelEnvironment::User"
+ many_to_many :tags
+ end
+
+ class Comment < Sequel::Model
+ many_to_one :post
+ end
+
+ class Tag < Sequel::Model
+ many_to_many :posts
+ end
+
+ def empty_database!
+ [User, Post, Comment].each do |klass|
+ klass.delete
+ klass.clear_blueprints!
+ end
+ end
+
+end

1 comment on commit 5640f5e

Thanks John for this Sequel adapter. I found one bug in it, that was causing many-to-one associations to render in the database as many-to-many. This was fixed with the following 2 added lines:

   def assign_attribute(attribute, value)
      association = @klass.association_reflection(attribute)
      if association
        if association.returns_array?
          (object.machinist_deferred_associations ||= []) << [association, value]
        elsif value.new?
          value.save(:raise_on_failure => true)
          super
        # NOTE: next 2 lines were necessary to fix a bug
        #  in the sequel adapter.  Without it, many-to-one
        #  relationships are incorrectly rendered in the
        #  database as a many-to-many.  -Dean
        else
          super
        end
      else
        super
      end
    end
 

Here is a spec that recreates the problem:

      RssFeed.blueprint do
        name { Faker::Lorem.word }
      end

      feed = RssFeed.make!
      20.times { Video.make!( :managed_feed => feed  ) }

Without the above fix, 20 RssFeed rows and 20 video rows are created. Total of 40 rows.

With the above fix, 1 RssFeed is created, and 20 videos, all linked back to the common RssFeed. Total of 21 rows.

Dean

Please sign in to comment.