├── VERSION ├── .document ├── lib ├── secondbase │ ├── railtie.rb │ ├── model.rb │ ├── active_record │ │ ├── patches.rb │ │ ├── base.rb │ │ ├── test_fixtures.rb │ │ ├── associations │ │ │ └── has_and_belongs_to_many_association.rb │ │ └── fixtures.rb │ ├── rake_method_chain.rb │ └── tasks.rb ├── generators │ └── secondbase │ │ ├── templates │ │ └── migration.rb │ │ ├── USAGE │ │ └── migration_generator.rb └── secondbase.rb ├── test ├── test_secondbase.rb └── helper.rb ├── Gemfile ├── CHANGELOG.md ├── rails_generators └── secondbase │ ├── secondbase_migration_generator.rb │ ├── templates │ └── migration.rb │ └── USAGE ├── Gemfile.lock ├── .gitignore ├── LICENSE.txt ├── Rakefile ├── secondbase.gemspec └── README.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.0 -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /lib/secondbase/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'secondbase' 2 | require 'rails' 3 | module SecondBase 4 | class Railtie < Rails::Railtie 5 | 6 | rake_tasks do 7 | load "secondbase/tasks.rb" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/test_secondbase.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestSecondbase < Test::Unit::TestCase 4 | should "probably rename this file and start testing for real" do 5 | flunk "hey buddy, you should probably rename this file and start testing for real" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/secondbase/model.rb: -------------------------------------------------------------------------------- 1 | # Secondbase model definition 2 | # 3 | # NOTE: By extending this model, you assume that the underlying table will be located in your Second (Data)base 4 | module SecondBase 5 | 6 | class Base < ActiveRecord::Base 7 | establish_connection ActiveRecord::Base.configurations[SecondBase::CONNECTION_PREFIX][Rails.env] 8 | 9 | self.abstract_class = true 10 | end 11 | end -------------------------------------------------------------------------------- /lib/generators/secondbase/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name.underscore.camelize %> < ActiveRecord::Migration 2 | ############################################################ 3 | # Database migration targeting the SecondBase! 4 | # Generated using: rails generator secondbase:migration [MigrationName] 5 | 6 | def self.up 7 | 8 | end 9 | 10 | def self.down 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | # Add dependencies required to use your gem here. 3 | # Example: 4 | # gem "activesupport", ">= 2.3.5" 5 | 6 | # Add dependencies to develop your gem here. 7 | # Include everything needed to run rake, tests, features, etc. 8 | group :development do 9 | gem "shoulda", ">= 0" 10 | gem "bundler", ">= 1.0.0" 11 | gem "jeweler", "~> 1.8.2" 12 | gem "activerecord", "~> 3.0.0" 13 | end 14 | -------------------------------------------------------------------------------- /lib/secondbase/active_record/patches.rb: -------------------------------------------------------------------------------- 1 | #################### 2 | ## ActiveRecord patches for all versions of rails 3 | require 'secondbase/active_record/base' 4 | 5 | 6 | #################### 7 | ## ActiveRecord patches for specific versions of rails 8 | if Rails.env.test? 9 | require 'secondbase/active_record/fixtures' 10 | require 'secondbase/active_record/test_fixtures' 11 | end 12 | 13 | require 'secondbase/active_record/associations/has_and_belongs_to_many_association' -------------------------------------------------------------------------------- /lib/secondbase/active_record/base.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | class Base 3 | 4 | # Arel is concerned with "engines". Normally the engine defaults to the primary 5 | # connection (ActiveRecord::Base). This will let us easily override the engine 6 | # when dealing with Seoncdbase models (deep in ActiveRecord code). 7 | # Since SecondBase::Base inherits from ActiveRecord::Base, this will pass the 8 | # right engine around. 9 | def self.engine 10 | self 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'test/unit' 11 | require 'shoulda' 12 | 13 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 14 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 15 | require 'secondbase' 16 | 17 | class Test::Unit::TestCase 18 | end 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.6 4 | 5 | * This version of the gem ONLY SUPPORTS Rails 3.x. For 2.x support, check out the branch 'rails_2_3' (or version .5 of the gem) 6 | * patched has_and_belongs_to_many associations, for secondbase models, so that ActiveRecord understands that the join table is in the secondbase. 7 | * patched ActiveRecord::TestFixtures so that transactional fixture support is respected for the SecondBase. 8 | * reorganized monkey patches to make it easier to work in fixes for different versions of rails. -------------------------------------------------------------------------------- /rails_generators/secondbase/secondbase_migration_generator.rb: -------------------------------------------------------------------------------- 1 | class SecondbaseMigrationGenerator < Rails::Generator::NamedBase 2 | def manifest 3 | record do |m| 4 | m.migration_template 'migration.rb', "db/migrate/#{SecondBase::CONNECTION_PREFIX}", :assigns => get_local_assigns 5 | end 6 | end 7 | 8 | 9 | private 10 | def get_local_assigns 11 | returning(assigns = {}) do 12 | if class_name.underscore =~ /^(add|remove)_.*_(?:to|from)_(.*)/ 13 | assigns[:migration_action] = $1 14 | assigns[:table_name] = $2.pluralize 15 | else 16 | assigns[:attributes] = [] 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/secondbase/rake_method_chain.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | # We want to provide a way to alias tasks so we can hook our custom logic 4 | # into the existing rails framework. For more information and usage, see: 5 | # http://www.metaskills.net/2010/5/26/the-alias_method_chain-of-rake-override-rake-task 6 | Rake::TaskManager.class_eval do 7 | def alias_task(fq_name) 8 | new_name = "#{fq_name}:original" 9 | @tasks[new_name] = @tasks.delete(fq_name) 10 | end 11 | end 12 | 13 | def alias_task(fq_name) 14 | Rake.application.alias_task(fq_name) 15 | end 16 | 17 | def override_task(*args, &block) 18 | name, params, deps = Rake.application.resolve_args(args.dup) 19 | fq_name = Rake.application.instance_variable_get(:@scope).dup.push(name).join(':') 20 | alias_task(fq_name) 21 | Rake::Task.define_task(*args, &block) 22 | end -------------------------------------------------------------------------------- /rails_generators/secondbase/templates/migration.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name.underscore.camelize %> < ActiveRecord::Migration 2 | ############################################################ 3 | # Database migration targeting the Secondbase! 4 | # Generated using: ./script/generator secondbase_migration [ModelName] 5 | 6 | def self.up<% attributes.each do |attribute| %> 7 | <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><% end -%> 8 | <%- end %> 9 | end 10 | 11 | def self.down<% attributes.reverse.each do |attribute| %> 12 | <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><% end -%> 13 | <%- end %> 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activemodel (3.0.0) 5 | activesupport (= 3.0.0) 6 | builder (~> 2.1.2) 7 | i18n (~> 0.4.1) 8 | activerecord (3.0.0) 9 | activemodel (= 3.0.0) 10 | activesupport (= 3.0.0) 11 | arel (~> 1.0.0) 12 | tzinfo (~> 0.3.23) 13 | activesupport (3.0.0) 14 | arel (1.0.1) 15 | activesupport (~> 3.0.0) 16 | builder (2.1.2) 17 | git (1.2.5) 18 | i18n (0.4.1) 19 | jeweler (1.8.4) 20 | bundler (~> 1.0) 21 | git (>= 1.2.5) 22 | rake 23 | rdoc 24 | json (1.7.5) 25 | rake (0.9.2.2) 26 | rdoc (3.12) 27 | json (~> 1.4) 28 | shoulda (2.11.1) 29 | tzinfo (0.3.23) 30 | 31 | PLATFORMS 32 | ruby 33 | 34 | DEPENDENCIES 35 | activerecord (~> 3.0.0) 36 | bundler (>= 1.0.0) 37 | jeweler (~> 1.8.2) 38 | shoulda 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | 4 | # rdoc generated 5 | rdoc 6 | 7 | # yard generated 8 | doc 9 | .yardoc 10 | 11 | # bundler 12 | .bundle 13 | 14 | # jeweler generated 15 | pkg 16 | 17 | # rvm 18 | .rvmrc 19 | 20 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 21 | # 22 | # * Create a file at ~/.gitignore 23 | # * Include files you want ignored 24 | # * Run: git config --global core.excludesfile ~/.gitignore 25 | # 26 | # After doing this, these files will be ignored in all your git projects, 27 | # saving you from having to 'pollute' every project you touch with them 28 | # 29 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 30 | # 31 | # For MacOS: 32 | # 33 | #.DS_Store 34 | # 35 | # For TextMate 36 | #*.tmproj 37 | #tmtags 38 | # 39 | # For emacs: 40 | #*~ 41 | #\#* 42 | #.\#* 43 | # 44 | # For vim: 45 | #*.swp 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 karledurante 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/generators/secondbase/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Stubs out a new (second)database migration. Pass the migration name, either 3 | CamelCased or under_scored, and an optional list of attribute pairs as arguments. 4 | 5 | For your organizational convenience, a migration class is generated in 6 | db/migrate/secondbase prefixed by a timestamp of the current date and time. 7 | 8 | You can name your migration in either of these formats to generate add/remove 9 | column lines from supplied attributes: AddColumnsToTable or RemoveColumnsFromTable 10 | 11 | Example: 12 | `rails generate secondbase:migration AddSslFlag` 13 | 14 | If the current date is May 14, 2008 and the current time 09:09:12, this creates the AddSslFlag migration 15 | db/migrate/secondbase/20080514090912_add_ssl_flag.rb 16 | 17 | You can continue to use `rake db:migrate` to migrate your first and second databases, or you can 18 | target secondbase by using `rake db:migrate:secondbase`. Remember, your migrations are timestamped, 19 | so regardless of the database they target, they will be unique and you will not have collision issues. -------------------------------------------------------------------------------- /lib/secondbase/active_record/test_fixtures.rb: -------------------------------------------------------------------------------- 1 | ## ActiveRecord::TestFixtures 2 | ## Monkey patch active record's test_fixtures module to manage 3 | ## transactions for the SecondBase 4 | module ActiveRecord 5 | module TestFixtures 6 | alias_method :original_setup_fixtures, :setup_fixtures 7 | alias_method :original_teardown_fixtures, :teardown_fixtures 8 | 9 | def setup_fixtures 10 | original_setup_fixtures 11 | # start tx for secondbase, if required 12 | # Load fixtures once and begin transaction. 13 | if run_in_transaction? 14 | SecondBase::Base.connection.increment_open_transactions 15 | SecondBase::Base.connection.transaction_joinable = false 16 | SecondBase::Base.connection.begin_db_transaction 17 | end 18 | end 19 | 20 | def teardown_fixtures 21 | original_teardown_fixtures 22 | 23 | # Rollback secondbase changes if a transaction is active. 24 | if run_in_transaction? && SecondBase::Base.connection.open_transactions != 0 25 | SecondBase::Base.connection.rollback_db_transaction 26 | SecondBase::Base.connection.decrement_open_transactions 27 | end 28 | 29 | SecondBase::Base.clear_active_connections! 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/generators/secondbase/migration_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | require 'rails/generators/migration' 3 | require 'active_record' 4 | 5 | module Secondbase 6 | class MigrationGenerator < Rails::Generators::NamedBase 7 | include Rails::Generators::Migration 8 | 9 | def self.source_root 10 | File.join(File.dirname(__FILE__), 'templates') 11 | end 12 | 13 | # Implement the required interface for Rails::Generators::Migration. 14 | # taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb 15 | def self.next_migration_number(dirname) #:nodoc: 16 | if ActiveRecord::Base.timestamped_migrations 17 | Time.now.utc.strftime("%Y%m%d%H%M%S") 18 | else 19 | "%.3d" % (current_migration_number(dirname) + 1) 20 | end 21 | end 22 | 23 | def create_migration_file 24 | migration_template 'migration.rb', 25 | "db/migrate/#{SecondBase::CONNECTION_PREFIX}/#{class_name.underscore}.rb", 26 | :assigns => get_local_assigns 27 | end 28 | 29 | private 30 | # TODO: We need to add support for name/value pairs like title:string dob:date etc.. 31 | def get_local_assigns 32 | { :class_name => class_name } 33 | end 34 | 35 | end 36 | end -------------------------------------------------------------------------------- /rails_generators/secondbase/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Stubs out a new database migration. Pass the migration name, either 3 | CamelCased or under_scored, and an optional list of attribute pairs as arguments. 4 | 5 | A migration class is generated in db/migrate_mysql prefixed by a timestamp of the current date and time. 6 | 7 | You can name your migration in either of these formats to generate add/remove 8 | column lines from supplied attributes: AddColumnsToTable or RemoveColumnsFromTable 9 | 10 | Example: 11 | `./script/generate secondbase_migration AddSslFlag` 12 | 13 | If the current date is May 14, 2008 and the current time 09:09:12, this creates the AddSslFlag migration 14 | db/migrate_mysql/20080514090912_add_ssl_flag.rb 15 | 16 | `./script/generate secondbase_migration AddTitleBodyToPost title:string body:text published:boolean` 17 | 18 | This will create the AddTitleBodyToPost in db/migrate_mysql/20080514090912_add_title_body_to_post.rb with 19 | this in the Up migration: 20 | 21 | add_column :posts, :title, :string 22 | add_column :posts, :body, :text 23 | add_column :posts, :published, :boolean 24 | 25 | And this in the Down migration: 26 | 27 | remove_column :posts, :published 28 | remove_column :posts, :body 29 | remove_column :posts, :title 30 | -------------------------------------------------------------------------------- /lib/secondbase.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'secondbase/active_record/patches' 3 | 4 | module SecondBase 5 | CONNECTION_PREFIX = 'secondbase' 6 | 7 | require 'secondbase/railtie' 8 | require 'secondbase/rake_method_chain' 9 | 10 | def self.do 11 | "You have just gotten to SecondBase, my friend." 12 | end 13 | 14 | def self.has_runner(env) 15 | ActiveRecord::Base.establish_connection(SecondBase::config(env)) 16 | reset_visitor_cache 17 | end 18 | 19 | def self.config(env) 20 | ActiveRecord::Base.configurations[SecondBase::CONNECTION_PREFIX][env] 21 | end 22 | 23 | # TODO: We should really look at faking out the connection used by ActiveRecord 24 | # during migrations, this would prevent us from digging around Arel internals. 25 | # Arel caches the SQL translator based on the engine (ActiveRecord::Base). This 26 | # means that if we swap out the base connection we risk the SQL translator being wrong. 27 | # This is an ugly hack that resets the adapter. See Line 27 of Arel's visitors.rb class. 28 | def self.reset_visitor_cache 29 | if Rails.version.to_i >= 3 30 | engine = ActiveRecord::Base 31 | adapter = engine.connection_pool.spec.config[:adapter] 32 | Arel::Visitors::ENGINE_VISITORS[engine] = (Arel::Visitors::VISITORS[adapter] || Arel::Visitors::ToSql).new(engine) 33 | end 34 | end 35 | end 36 | 37 | module FirstBase 38 | def self.config(env) 39 | ActiveRecord::Base.configurations[env] 40 | end 41 | 42 | def self.has_runner(env) 43 | ActiveRecord::Base.establish_connection(FirstBase::config(env)) 44 | SecondBase.reset_visitor_cache 45 | end 46 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'jeweler' 13 | Jeweler::Tasks.new do |gem| 14 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 15 | gem.name = "secondbase" 16 | gem.homepage = "http://github.com/karledurante/secondbase" 17 | gem.license = "MIT" 18 | gem.summary = %Q{Allow Rails manage second database in your projects} 19 | gem.description = %Q{Secondbase provides support to Rails to create a homogeneous environment for a dual database project. Using the rake tasks already familiar to you, this gem enables Rails to work with two primary databases, instead of just one.} 20 | gem.email = "kdurante@customink.com" 21 | gem.authors = ["karledurante"] 22 | gem.add_development_dependency 'activerecord', '~> 3.0.0' 23 | # Include your dependencies below. Runtime dependencies are required when using your gem, 24 | # and development dependencies are only needed for development (ie running rake tasks, tests, etc) 25 | # gem.add_runtime_dependency 'jabber4r', '> 0.1' 26 | # gem.add_development_dependency 'rspec', '> 1.2.3' 27 | end 28 | Jeweler::RubygemsDotOrgTasks.new 29 | 30 | require 'rake/testtask' 31 | Rake::TestTask.new(:test) do |test| 32 | test.libs << 'lib' << 'test' 33 | test.pattern = 'test/**/test_*.rb' 34 | test.verbose = true 35 | end 36 | 37 | task :default => :test 38 | 39 | require 'rake/rdoctask' 40 | Rake::RDocTask.new do |rdoc| 41 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 42 | 43 | rdoc.rdoc_dir = 'rdoc' 44 | rdoc.title = "secondbase #{version}" 45 | rdoc.rdoc_files.include('README*') 46 | rdoc.rdoc_files.include('lib/**/*.rb') 47 | end 48 | -------------------------------------------------------------------------------- /lib/secondbase/active_record/associations/has_and_belongs_to_many_association.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | # = Active Record Has And Belongs To Many Association 3 | module Associations 4 | class HasAndBelongsToManyAssociation < AssociationCollection 5 | 6 | # determines the appropriate engine for the join table's parent klass 7 | def arel_engine 8 | Arel::Sql::Engine.new(@reflection.klass.engine) 9 | end 10 | 11 | # This method is entirely replicated, except for line 25. We simply 12 | # need to pass in the appropriate engine to Arel. 13 | def insert_record(record, force = true, validate = true) 14 | if record.new_record? 15 | if force 16 | record.save! 17 | else 18 | return false unless record.save(:validate => validate) 19 | end 20 | end 21 | 22 | if @reflection.options[:insert_sql] 23 | @owner.connection.insert(interpolate_and_sanitize_sql(@reflection.options[:insert_sql], record)) 24 | else 25 | relation = Arel::Table.new(@reflection.options[:join_table], arel_engine) 26 | timestamps = record_timestamp_columns(record) 27 | timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? 28 | 29 | attributes = Hash[columns.map do |column| 30 | name = column.name 31 | value = case name.to_s 32 | when @reflection.primary_key_name.to_s 33 | @owner.id 34 | when @reflection.association_foreign_key.to_s 35 | record.id 36 | when *timestamps 37 | timezone 38 | else 39 | @owner.send(:quote_value, record[name], column) if record.has_attribute?(name) 40 | end 41 | [relation[name], value] unless value.nil? 42 | end] 43 | 44 | relation.insert(attributes) 45 | end 46 | 47 | return true 48 | end 49 | 50 | # This method is entirely replicated, except for line 57. We simply 51 | # need to pass in the appropriate engine to Arel. 52 | def delete_records(records) 53 | if sql = @reflection.options[:delete_sql] 54 | records.each { |record| @owner.connection.delete(interpolate_and_sanitize_sql(sql, record)) } 55 | else 56 | 57 | relation = Arel::Table.new(@reflection.options[:join_table], arel_engine) 58 | 59 | relation.where(relation[@reflection.primary_key_name].eq(@owner.id). 60 | and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) 61 | ).delete 62 | end 63 | end 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /secondbase.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{secondbase} 8 | s.version = "0.6.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["karledurante"] 12 | s.date = %q{2012-08-31} 13 | s.description = %q{Secondbase provides support to Rails to create a homogeneous environment for a dual database project. Using the rake tasks already familiar to you, this gem enables Rails to work with two primary databases, instead of just one.} 14 | s.email = %q{kdurante@customink.com} 15 | s.extra_rdoc_files = [ 16 | "LICENSE.txt", 17 | "README.rdoc" 18 | ] 19 | s.files = [ 20 | ".document", 21 | "CHANGELOG.md", 22 | "Gemfile", 23 | "Gemfile.lock", 24 | "LICENSE.txt", 25 | "README.rdoc", 26 | "Rakefile", 27 | "VERSION", 28 | "lib/generators/secondbase/USAGE", 29 | "lib/generators/secondbase/migration_generator.rb", 30 | "lib/generators/secondbase/templates/migration.rb", 31 | "lib/secondbase.rb", 32 | "lib/secondbase/active_record/associations/has_and_belongs_to_many_association.rb", 33 | "lib/secondbase/active_record/base.rb", 34 | "lib/secondbase/active_record/fixtures.rb", 35 | "lib/secondbase/active_record/patches.rb", 36 | "lib/secondbase/active_record/test_fixtures.rb", 37 | "lib/secondbase/model.rb", 38 | "lib/secondbase/railtie.rb", 39 | "lib/secondbase/rake_method_chain.rb", 40 | "lib/secondbase/tasks.rb", 41 | "rails_generators/secondbase/USAGE", 42 | "rails_generators/secondbase/secondbase_migration_generator.rb", 43 | "rails_generators/secondbase/templates/migration.rb", 44 | "secondbase.gemspec", 45 | "test/helper.rb", 46 | "test/test_secondbase.rb" 47 | ] 48 | s.homepage = %q{http://github.com/karledurante/secondbase} 49 | s.licenses = ["MIT"] 50 | s.require_paths = ["lib"] 51 | s.rubygems_version = %q{1.4.2} 52 | s.summary = %q{Allow Rails manage second database in your projects} 53 | 54 | if s.respond_to? :specification_version then 55 | s.specification_version = 3 56 | 57 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 58 | s.add_development_dependency(%q, [">= 0"]) 59 | s.add_development_dependency(%q, [">= 1.0.0"]) 60 | s.add_development_dependency(%q, ["~> 1.8.2"]) 61 | s.add_development_dependency(%q, ["~> 3.0.0"]) 62 | s.add_development_dependency(%q, ["~> 3.0.0"]) 63 | else 64 | s.add_dependency(%q, [">= 0"]) 65 | s.add_dependency(%q, [">= 1.0.0"]) 66 | s.add_dependency(%q, ["~> 1.8.2"]) 67 | s.add_dependency(%q, ["~> 3.0.0"]) 68 | s.add_dependency(%q, ["~> 3.0.0"]) 69 | end 70 | else 71 | s.add_dependency(%q, [">= 0"]) 72 | s.add_dependency(%q, [">= 1.0.0"]) 73 | s.add_dependency(%q, ["~> 1.8.2"]) 74 | s.add_dependency(%q, ["~> 3.0.0"]) 75 | s.add_dependency(%q, ["~> 3.0.0"]) 76 | end 77 | end 78 | 79 | -------------------------------------------------------------------------------- /lib/secondbase/active_record/fixtures.rb: -------------------------------------------------------------------------------- 1 | ########################### 2 | # Monkey patch Fixtures 3 | # Fixtures needs to load fixtures into the database defined by the parent class! 4 | # 5 | # I feel like the concepts here could be incorporated directly into Fixtures. 6 | # I mean, they shouldn't be so presumptions to think that every model lives in the 7 | # same database.... 8 | class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) 9 | def self.create_fixtures(fixtures_directory, table_names, class_names = {}) 10 | table_names = [table_names].flatten.map { |n| n.to_s } 11 | table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') } 12 | connection = block_given? ? yield : ActiveRecord::Base.connection 13 | 14 | # make sure we only load secondbase tables that have fixtures defined... 15 | sb_table_names = SecondBase::Base.send(:descendants).map(&:table_name) 16 | sb_table_names = sb_table_names & table_names 17 | sb_connection = SecondBase::Base.connection 18 | 19 | # filter out the secondbase tables from firstbase, otherwise we'll get SQL errors... 20 | table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) || sb_table_names.include?(table_name) } 21 | fixtures = process_fixture_table_names(table_names_to_fetch, class_names, connection, fixtures_directory) 22 | fixtures = [fixtures] if !fixtures.instance_of?(Array) 23 | 24 | sb_table_names_to_fetch = sb_table_names.reject { |table_name| fixture_is_cached?(sb_connection, table_name)} 25 | sb_fixtures = process_fixture_table_names(sb_table_names_to_fetch, class_names, sb_connection, fixtures_directory) 26 | sb_fixtures = [sb_fixtures] if !sb_fixtures.instance_of?(Array) 27 | 28 | (fixtures + sb_fixtures).compact 29 | end 30 | 31 | def self.process_fixture_table_names(table_names_to_fetch, class_names, connection, fixtures_directory) 32 | fixtures_map = {} 33 | unless table_names_to_fetch.empty? 34 | ActiveRecord::Base.silence do 35 | connection.disable_referential_integrity do 36 | # fixtures_map = {} 37 | 38 | fixtures = table_names_to_fetch.map do |table_name| 39 | obj = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) 40 | fixtures_map[table_name] = obj 41 | end 42 | 43 | all_loaded_fixtures.update(fixtures_map) 44 | 45 | connection.transaction(:requires_new => true) do 46 | fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } 47 | fixtures.each { |fixture| fixture.insert_fixtures } 48 | 49 | # Cap primary key sequences to max(pk). 50 | if connection.respond_to?(:reset_pk_sequence!) 51 | table_names_to_fetch.each do |table_name| 52 | connection.reset_pk_sequence!(table_name.tr('/', '_')) 53 | end 54 | end 55 | end 56 | 57 | cache_fixtures(connection, fixtures_map) 58 | end 59 | end 60 | end 61 | 62 | table_names_to_fetch = nil if table_names_to_fetch.blank? 63 | cached_fixtures(connection, table_names_to_fetch) 64 | end 65 | 66 | end -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = secondbase 2 | 3 | SecondBase adds a second database to your application. While Rails enables you to establish connections to as many external databases as you like, Rails can only manage a single database with it's migration and testing tasks. 4 | 5 | SecondBase enables Rails to work with, and manage, a second database (almost) transparently. As a developer, you should not even realize a second database is in play. Core rake tasks such as rake db:create, rake db:migrate, and rake test will continue to work seamlessly with both databases without you, the developer, having to run any extra rake tasks. 6 | 7 | SecondBase works with Rails 2.3.x and 3.0.x. I've not tried to use SecondBase with Rails 3.1 yet, although someone has submitted a pull request fixing a 3.1 issue, so I assume it's working OK? 8 | 9 | == Contributing to SecondBase 10 | 11 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 12 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 13 | * Fork the project 14 | * Start a feature/bugfix branch 15 | * Commit and push until you are happy with your contribution 16 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 17 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 18 | 19 | == System Requirements 20 | SecondBase now supports Rails 2.x and Rails 3.x. 21 | 22 | == Installation 23 | Modify your Gemfile to include SecondBase: 24 | 25 | gem 'secondbase', '0.5.0' 26 | 27 | Run `bundle install`. You thought it would be harder? If you're using Rails 2.x, then yes, a little bit harder. You must also add this to your Rakefile: 28 | 29 | require 'secondbase/tasks' if defined?(SecondBase) 30 | 31 | PLEASE NOTE that if you are using bundler with Rails 2.x, then you simply need to add this to your Rakefile: 32 | 33 | require 'secondbase/tasks' 34 | 35 | == Usage 36 | === Database 37 | Configure your database.yml to define your SecondBase: 38 | 39 | # Your normal rails definitions... 40 | development: 41 | adapter: mysql #postgres, oracle, etc 42 | encoding: utf8 43 | database: development 44 | 45 | test: 46 | adapter: mysql #postgres, oracle, etc 47 | encoding: utf8 48 | database: test 49 | 50 | # Your secondbase database configurations... 51 | secondbase: 52 | development: 53 | adapter: mysql 54 | encoding: utf8 55 | database: secondbase_development 56 | 57 | test: 58 | adapter: mysql 59 | encoding: utf8 60 | database: secondbase_test 61 | 62 | 63 | === Migrations 64 | SecondBase comes with a generator to assist in managing your migrations 65 | 66 | Rails 3.x: 67 | rails generator secondbase:migration CreateWidgetsTable 68 | 69 | Rails 2.x: 70 | script/generate secondbase_migration CreateWidgetsTable 71 | 72 | The generator will organize your second (data)base migrations alongside of your primary database. The above command will generate the file: 73 | 74 | db/migrate/secondbase/20101203211338_create_widgets_table.rb 75 | 76 | To run your migrations, simply run: 77 | 78 | rake db:migrate 79 | 80 | This will migrate your first and second (data)bases. If, for some reason, you only want to migrate your second (data)base, run: 81 | 82 | rake db:migrate:secondbase 83 | 84 | Please note that migrating up and migrating down must be done specifically on your first or second (data)base. As usual, to migrate your first (data)base up or down to version 20101203211338, you could run: 85 | 86 | rake db:migrate:up VERSION=20101005311335 87 | rake db:migrate:down VERSION=20101005311335 88 | 89 | To migrate your second (data)base up or down to version 20101203211338, you would run: 90 | 91 | rake db:migrate:up:secondbase VERSION=20101203211338 92 | rake db:migrate:down:secondbase VERSION=20101203211338 93 | 94 | 95 | === Models 96 | Every model in your project that extends ActiveRecord::Base will point to the database defined by Rails.env. This is the default Rails behavior and should be of no surprise to you. So how do we point our models to the second (data)base? 97 | 98 | SecondBase offers a base model that you can simply extend: 99 | 100 | require 'secondbase/model' 101 | 102 | class Widget < SecondBase::Base 103 | # you're Widget model is now pointing to your second (data)base table 'widgets' 104 | end 105 | 106 | ActiveRecord associations will still work between your Firstbase and SecondBase models! 107 | 108 | # Notice how normal this all looks... 109 | class User < ActiveRecord::Base 110 | has_many :widgets 111 | end 112 | 113 | === Rake Tasks & Custom Classes 114 | If you need to write rake tasks, or some other code that does not extend ActiveRecord, you can simply establish a connection to your second (data)base: 115 | 116 | SecondBase::has_runner(Rails.env) 117 | 118 | Please note that this is equivalent to using ActiveRecord::Base.establish_connection(config) and will reset the base connection of your ENTIRE application. No worries, to move the runner back to first you can use: 119 | 120 | FirstBase::has_runner(Rails.env) 121 | 122 | === Testing 123 | Tests can still be run using `rake test` or `rake test:units`, etc. However, if you are using fixtures, you will need to update your TestHelper class to include: 124 | 125 | require 'secondbase/fixtures' 126 | 127 | This is patch to fixtures that will identify the fixtures which belong to models that extend SecondBase::Base. The patch will then ensure that the table descendants of SecondBase::Base get loaded into your second test (data)base. 128 | 129 | At this time, I can verify that SecondBase works with Fixtures, Machinist and FactoryGirl. Conceivably, other test factories should work, but there is currently no support for this. If you have the time to update this gem to be test object compatible, by all means... 130 | 131 | == TODO 132 | - Migration generator in Rails 3.x needs support for attribute generation (similar to rails generate migration). For example: 133 | `rails generate secondbase:migration AddTitleBodyToPost title:string body:text published:boolean` 134 | 135 | - rake db:fixtures:load is currently broken. Like many other things I have fixed, it assumes you only one a single database and attempts to load all fixtures into it. I don't believe we can get away with alias chaining this one, I think (like the Fixtures class), we'll have to freedom patch it. 136 | 137 | - TESTS!! Not 100% sure how to test the rake tasks, but I can definitely write tests for the classes and generators. I did this in my spare time, sorry... 138 | 139 | == Copyright 140 | 141 | Copyright (c) 2010 karledurante. See LICENSE.txt for 142 | further details. 143 | 144 | -------------------------------------------------------------------------------- /lib/secondbase/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'secondbase' 2 | 3 | #################################### 4 | # 5 | # SecondBase database managment tasks 6 | # 7 | # We are overriding a handful of rake tasks here: 8 | # db:create 9 | # db:migrate 10 | # db:test:prepare 11 | # 12 | # We ARE NOT redefining the implementation of these tasks, we are simply 13 | # appending custom functionality to them. We just want to be sure that in 14 | # addition to creating, migrating, and preparing your default (Rails.env) 15 | # database, that we can also work with with the second (data)base. 16 | 17 | namespace :db do 18 | override_task :create do 19 | # First, we execute the original/default create task 20 | Rake::Task["db:create:original"].invoke 21 | 22 | # now, we create our secondary databases 23 | Rake::Task['environment'].invoke 24 | ActiveRecord::Base.configurations[SecondBase::CONNECTION_PREFIX].each_value do |config| 25 | next unless config['database'] 26 | 27 | # Only connect to local databases 28 | local_database?(config) { create_database(config) } 29 | end 30 | end 31 | 32 | override_task :migrate do 33 | Rake::Task['environment'].invoke 34 | 35 | # Migrate secondbase... 36 | Rake::Task["db:migrate:secondbase"].invoke 37 | 38 | # Execute the original/default prepare task 39 | Rake::Task["db:migrate:original"].invoke 40 | end 41 | 42 | override_task :abort_if_pending_migrations do 43 | # Execute the original/default prepare task 44 | Rake::Task["db:abort_if_pending_migrations"].invoke 45 | 46 | Rake::Task["db:abort_if_pending_migrations:secondbase"].invoke 47 | end 48 | 49 | namespace :test do 50 | override_task :prepare do 51 | Rake::Task['environment'].invoke 52 | 53 | # Clone the secondary database structure 54 | Rake::Task["db:test:prepare:secondbase"].invoke 55 | 56 | # Execute the original/default prepare task 57 | Rake::Task["db:test:prepare:original"].invoke 58 | end 59 | end 60 | 61 | ################################## 62 | # SecondBase specific database tasks 63 | namespace :abort_if_pending_migrations do 64 | desc "determines if your secondbase has pending migrations" 65 | task :secondbase => :environment do 66 | # reset connection to secondbase... 67 | SecondBase::has_runner(Rails.env) 68 | 69 | pending_migrations = ActiveRecord::Migrator.new(:up, "db/migrate/#{SecondBase::CONNECTION_PREFIX}").pending_migrations 70 | 71 | if pending_migrations.any? 72 | puts "You have #{pending_migrations.size} pending migrations:" 73 | pending_migrations.each do |pending_migration| 74 | puts ' %4d %s' % [pending_migration.version, pending_migration.name] 75 | end 76 | abort %{Run "rake db:migrate" to update your database then try again.} 77 | end 78 | 79 | # reset connection back to firstbase... 80 | FirstBase::has_runner(Rails.env) 81 | end 82 | end 83 | 84 | namespace :migrate do 85 | desc "migrates the second database" 86 | task :secondbase => :load_config do 87 | Rake::Task['environment'].invoke 88 | # NOTE: We are not generating a db schema on purpose. Since we're running 89 | # in a dual db mode, it could be confusing to have two schemas. 90 | 91 | # reset connection to secondbase... 92 | SecondBase::has_runner(Rails.env) 93 | 94 | # run secondbase migrations... 95 | ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true 96 | ActiveRecord::Migrator.migrate("db/migrate/#{SecondBase::CONNECTION_PREFIX}/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) 97 | 98 | # reset connection back to firstbase... 99 | FirstBase::has_runner(Rails.env) 100 | end 101 | 102 | namespace :up do 103 | desc 'Runs the "up" for a given SecondBase migration VERSION.' 104 | task :secondbase => :environment do 105 | version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil 106 | raise "VERSION is required" unless version 107 | 108 | # reset connection to secondbase... 109 | SecondBase::has_runner(Rails.env) 110 | 111 | ActiveRecord::Migrator.run(:up, "db/migrate/#{SecondBase::CONNECTION_PREFIX}/", version) 112 | 113 | # reset connection back to firstbase... 114 | FirstBase::has_runner(Rails.env) 115 | end 116 | end 117 | 118 | namespace :down do 119 | desc 'Runs the "down" for a given SecondBase migration VERSION.' 120 | task :secondbase => :environment do 121 | version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil 122 | raise "VERSION is required" unless version 123 | 124 | # reset connection to secondbase... 125 | SecondBase::has_runner(Rails.env) 126 | 127 | ActiveRecord::Migrator.run(:down, "db/migrate/#{SecondBase::CONNECTION_PREFIX}/", version) 128 | 129 | # reset connection back to firstbase... 130 | FirstBase::has_runner(Rails.env) 131 | end 132 | end 133 | end 134 | 135 | namespace :create do 136 | desc 'Create the database defined in config/database.yml for the current RAILS_ENV' 137 | task :secondbase => :load_config do 138 | 139 | # We can still use the #create_database method defined in activerecord's databases.rake 140 | # we call it passing the secondbase config instead of the default (Rails.env) config... 141 | create_database(secondbase_config(Rails.env)) 142 | end 143 | end 144 | 145 | namespace :structure do 146 | namespace :dump do 147 | desc "dumps structure for both (first and second) databases." 148 | task :secondbase do 149 | Rake::Task['environment'].invoke 150 | 151 | SecondBase::has_runner(Rails.env) 152 | 153 | # dump the current env's db, be sure to add the schema information!!! 154 | dump_file = "#{RAILS_ROOT}/db/#{SecondBase::CONNECTION_PREFIX}_#{RAILS_ENV}_structure.sql" 155 | 156 | File.open(dump_file, "w+") do |f| 157 | f << ActiveRecord::Base.connection.structure_dump 158 | end 159 | 160 | if ActiveRecord::Base.connection.supports_migrations? 161 | File.open(dump_file, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } 162 | end 163 | 164 | FirstBase::has_runner(Rails.env) 165 | end 166 | end 167 | end 168 | 169 | namespace :test do 170 | namespace :prepare do 171 | desc 'Prepares the test instance of secondbase' 172 | task :secondbase => 'db:abort_if_pending_migrations:secondbase' do 173 | Rake::Task["db:test:clone_structure:secondbase"].invoke 174 | end 175 | end 176 | 177 | namespace :purge do 178 | task :secondbase do 179 | Rake::Task['environment'].invoke 180 | 181 | SecondBase::has_runner('test') 182 | 183 | ActiveRecord::Base.connection.recreate_database(secondbase_config('test')["database"], secondbase_config('test')) 184 | 185 | FirstBase::has_runner(Rails.env) 186 | end 187 | end 188 | 189 | namespace :clone_structure do 190 | task :secondbase do 191 | Rake::Task['environment'].invoke 192 | 193 | # dump secondbase structure and purge the test secondbase 194 | `rake db:structure:dump:secondbase` 195 | `rake db:test:purge:secondbase` 196 | 197 | # now lets clone the structure for secondbase 198 | SecondBase::has_runner('test') 199 | 200 | ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') if secondbase_config(RAILS_ENV)['adapter'][/mysql/] 201 | 202 | IO.readlines("#{RAILS_ROOT}/db/#{SecondBase::CONNECTION_PREFIX}_#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table| 203 | ActiveRecord::Base.connection.execute(table) 204 | end 205 | 206 | FirstBase::has_runner(Rails.env) 207 | end 208 | end 209 | end 210 | 211 | end 212 | 213 | 214 | #################################### 215 | # 216 | # Some helper methods to run back and forth between first and second base. 217 | def secondbase_config(env) 218 | ActiveRecord::Base.configurations[SecondBase::CONNECTION_PREFIX][env] 219 | end --------------------------------------------------------------------------------