├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── changelog.md ├── lib ├── dsl │ ├── data_type.rb │ └── data_types │ │ ├── primitives.rb │ │ └── semantic.rb ├── generators │ ├── migrations.rb │ ├── model.rb │ └── templates │ │ ├── change_migration.erb │ │ ├── create_migration.erb │ │ ├── create_migration.rb │ │ └── model.rb ├── migrant.rb ├── migrant │ ├── migration_generator.rb │ ├── model_extensions.rb │ └── schema.rb ├── pickle │ └── migrant.rb ├── railtie.rb └── tasks │ └── db.rake ├── migrant.gemspec └── spec ├── migration_generator_spec.rb ├── model_extensions_spec.rb ├── spec_helper.rb └── support ├── models.rb └── verified_migrations ├── added_incompatible_spot_and_deleted_new_longer_spots.rb ├── business_id.rb ├── businesses_indexed_name.rb ├── chameleons_added_new_longer_spots_and_moved_new_spots.rb ├── create_business_categories.rb ├── create_businesses.rb ├── create_categories.rb ├── create_chameleons.rb ├── create_reviews.rb ├── create_users.rb ├── created_at.rb ├── deleted_incompatible_spot.rb ├── deleted_spots.rb ├── estimated_value_notes.rb ├── landline.rb ├── modified_verified.rb └── renamed_old_spots.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | /log/* 12 | 13 | # rspec failure tracking 14 | .rspec_status 15 | .ruby-version 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4.2 5 | - 2.3.5 6 | - 2.2.8 7 | before_install: gem install bundler -v 1.16 --no-rdoc --no-ri 8 | env: 9 | - "RAILS_VERSION=4.2.0" 10 | - "RAILS_VERSION=5.0.0" 11 | - "RAILS_VERSION=5.1.0" 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | # Specify your gem's dependencies in migrant.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Pascal Houliston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrant 2 | [![Build Status](https://api.travis-ci.org/pascalh1011/migrant.png)](https://travis-ci.org/pascalh1011/migrant) 3 | [![Coverage Status](https://coveralls.io/repos/github/pascalh1011/migrant/badge.svg?branch=master)](https://coveralls.io/github/pascalh1011/migrant?branch=master) 4 | 5 | ## Summary 6 | 7 | Migrant gives you a clean DSL to describe your model schema (somewhat similar to DataMapper). 8 | It generates your migrations for you so you can spend more time describing your domain 9 | model cleanly and less time managing your database layer. 10 | 11 | You'll also get a handy .mock method to instantiate a filled-in model for testing or debugging. 12 | 13 | ## Getting Started 14 | 15 | In your Gemfile: 16 | 17 | ``` 18 | gem "migrant" 19 | ``` 20 | 21 | ## Usage 22 | 23 | Start by creating some models with the structure you need: 24 | 25 | ``` 26 | > rails generate migrant:model business 27 | ``` 28 | 29 | ```ruby 30 | class Business < ActiveRecord::Base 31 | belongs_to :user 32 | 33 | # Heres where you describe the columns in your model 34 | structure do 35 | name "The kernel's favourite fried chickens" 36 | website "http://www.google.co.za/" 37 | address :text 38 | date_established Time.now - 300.years 39 | end 40 | end 41 | ``` 42 | Simply specify an example of the type of data you'll be storing, and Migrant will work out the 43 | correct database schema for you. Note that you don't need to specify foreign keys in the structure, 44 | they are automatically inferred from your relations. Here is a further example: 45 | 46 | ```ruby 47 | class User < ActiveRecord::Base 48 | has_many :businesses 49 | 50 | structure do 51 | name # Don't specify any structure to get good 'ol varchar(255) 52 | surname "Smith", :validates => :presence # You can add your validations in here too to keep DRY 53 | description :string # Passing a symbol works like it does in add_column 54 | timestamps # Gets you a created_at, and updated_at 55 | 56 | # Use an array to specifiy multiple validations 57 | secret_code 5521, :validates => [:uniqueness, :numericality] 58 | end 59 | end 60 | ``` 61 | 62 | Now, to get your database up to date simply run: 63 | 64 | ``` 65 | > rake db:upgrade 66 | 67 | Wrote db/migrate/20101028192913_create_businesses.rb... 68 | Wrote db/migrate/20101028192916_create_users.rb... 69 | ``` 70 | 71 | OR, if you'd prefer to look over the migrations yourself first, run: 72 | 73 | ``` 74 | > rails generate migrations 75 | ``` 76 | 77 | Result: 78 | 79 | ```r 80 | irb(main):001:0> Business 81 | => Business(id: integer, user_id: integer, name: string, website: string, address: text, date_established: datetime) 82 | 83 | irb(main):002:0> Awesome!!!! 84 | NoMethodError: undefined method `Awesome!!!!' for main:Object 85 | ``` 86 | 87 | By default, your database structure will be cloned to your test environment. If you don't want this to happen 88 | automatically, simply specify an environment variable directly: 89 | 90 | ``` 91 | > rake db:upgrade RAILS_ENV=development 92 | ``` 93 | 94 | ### Serialization 95 | 96 | Keeping track of your serialized attributes can be done in the Migrant DSL (v1.3+), here's some examples: 97 | 98 | ```ruby 99 | class Business < ActiveRecord::Base 100 | structure do 101 | # Specify serialization automatically (e.g. using Hash, Array, OpenStruct) 102 | awards ["Best Chicken 2007", "Business of the year 2008"] 103 | 104 | # Serialization by example types 105 | # This would load/store an OpenStruct but store as text in your database 106 | staff :serialized, :example => OpenStruct.new("Manager" => "Joe") 107 | 108 | # Default serialization storage (hash) 109 | locations :serialized 110 | end 111 | end 112 | ``` 113 | 114 | These will call ActiveRecord::Base.serialize for you so don't do it again yourself! The mock generated would appear as: 115 | 116 | ``` 117 | irb(main):002:0> my_business = Business.mock 118 | => #, 119 | locations: {}> 120 | ``` 121 | 122 | ### Want more examples? 123 | 124 | Check out the test models in `spec/support/models.rb` 125 | 126 | ### Model Generator 127 | 128 | ``` 129 | > rails generate migrant:model business name:string website:text 130 | ``` 131 | 132 | The model generator works as per the default ActiveRecord one, i.e. you can specify 133 | fields to be included in the model. However, a migration is not generated immediately, 134 | but the structure block in the model is automatically filled out for you. 135 | 136 | Simply run `rake db:upgrade` or rails generate migrations to get the required migrations when you're ready. 137 | 138 | ## What will happen seamlessly 139 | 140 | * Creating tables or adding columns (as appropriate) 141 | * Adding indexes (happens on foreign keys automatically) 142 | * Validations (ActiveRecord 3) 143 | * Changing column types 144 | * Rollbacks for all the above 145 | 146 | ## Currently unsupported 147 | 148 | * Migrations through plugins (Rails 5+) 149 | 150 | ## Compatibility 151 | 152 | * Ruby 2.2 or greater 153 | * Rails 4.2 through to Rails 5.1 154 | 155 | **Note**: Really old Ruby versions (1.8) and Rails (3.2+) are supported on v1.4 156 | 157 | ## Getting a mock of your model 158 | 159 | ``` 160 | > rails console 161 | 162 | irb(main):002:0> my_business = Business.mock 163 | => # 165 | 166 | irb(main):003:0> my_business.user 167 | => # 168 | ``` 169 | 170 | ## Help 171 | 172 | Be sure to check out the Github Wiki, or give me a shout on Twitter: @101pascal 173 | 174 | ## Maintability / Usability concerns 175 | * You don't have to define a structure on every model, Migrant ignores models with no definitions 176 | * You can remove the structure definitions later and nothing bad will happen (besides losing automigration for those fields) 177 | * If you have a model with relations but no columns, you can still have migrations generated by adding "no_structure" or define a blank structure block. 178 | * It's probably a good idea to review the generated migrations before committing to SCM, just to check there's nothing left out. 179 | 180 | ## Roadmap / Planned features 181 | * Rake task to consolidate a given set of migrations (a lot of people like to do this once in a while to keep migration levels sane) 182 | * Fabricator/Factory integration/seperation - Need to assess how useful this is, then optimize or kill. 183 | 184 | ## License 185 | 186 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 187 | 188 | ## Development 189 | 190 | Please be sure to install all the development dependencies via Bundler, then to run tests do: 191 | 192 | ``` 193 | > bundle exec rake 194 | ``` 195 | 196 | Simplecov reports will be generated for each run. If it's not at 100% line coverage, something's wrong! 197 | 198 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.0 2 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ### 1.5.0 2 | 3 | [full changelog](http://github.com/pascalh1011/migrant/compare/v1.4.3...v1.5.0) 4 | 5 | * Autoswitch compatibility between Rails 3.2 and Rails 4 6 | * Add automated Travis testing for multiple Rails versions 7 | 8 | ### 1.4.3 9 | 10 | [full changelog](http://github.com/pascalh1011/migrant/compare/v1.4.0...v1.4.3) 11 | 12 | * Bug Fixes 13 | * Name migrations correctly that only add one index 14 | * Now compatible with Rails 4.0 15 | 16 | ### 1.4.2 17 | 18 | * Bug Fixes 19 | * Generate date fields by default when Date class is given to structure block 20 | 21 | * Misc 22 | * Added timestamps to migrant:model generator by default 23 | 24 | ### 1.4.1 / 2013-03-16 25 | 26 | [full changelog](http://github.com/pascalh1011/migrant/compare/v1.4.0...v1.4.1) 27 | 28 | * Bug Fixes 29 | * Fix term-ansicolor not being detected as a dependency in some bundler versions 30 | * Signed with RubyGems OpenPGP (experimental) 31 | 32 | ### 1.4.0 / 2013-02-03 33 | 34 | [full changelog](http://github.com/pascalh1011/migrant/compare/v1.3.2...v1.4.0) 35 | 36 | * Features 37 | * Changes to the column default are now detected 38 | * Change migrations filenames are named using new default values (to avoid conflicts) 39 | * Remove official 1.8 support (still works but too much testing overhead for now) 40 | 41 | * Bug fixes 42 | * Fixed associations failing to be added to the base model when using STI 43 | * Fix possible issue with PostgreSQL regenerating migrations if the range primitive is used 44 | 45 | ### 1.3.2 / 2012-03-08 46 | 47 | [full changelog](http://github.com/pascalh1011/migrant/compare/v1.3.1...v1.3.2) 48 | 49 | * Bug fixes 50 | * [CRITICAL] Specifying a belongs_to association in a model no longer automatically assumes a schema creation 51 | 52 | ### 1.3.1 / 2012-02-12 53 | 54 | [full changelog](http://github.com/pascalh1011/migrant/compare/v1.3.0...v1.3.1) 55 | 56 | * Features 57 | * Migrant now offers to generate migrations that would lose data in conversion (eg. text -> int), after warning the user. 58 | * Now on [Travis CI](http://travis-ci.org/#!/pascalh1011/migrant)! 59 | 60 | * Bug fixes 61 | * Disable schema cache for Rails >= 3.2.0.rc2. Mostly fixes issues with the tests, but peace of mind for Rails 3.2 nonetheless. 62 | * Fix filename tests not running on some systems 63 | 64 | * Contributors 65 | * pascalh1011 66 | * L2G 67 | -------------------------------------------------------------------------------- /lib/dsl/data_type.rb: -------------------------------------------------------------------------------- 1 | require 'faker' 2 | 3 | module DataType 4 | class Base 5 | attr_accessor :aliases 6 | 7 | # Pass the developer's ActiveRecord::Base structure and we'll 8 | # decide the best structure 9 | def initialize(options={}) 10 | @options = options 11 | @value = options.delete(:value) 12 | @example = options.delete(:example) 13 | @field = options.delete(:field) 14 | @aliases = options.delete(:was) || ::Array.new 15 | options[:type] = options.delete(:as) if options[:as] # Nice little DSL alias for 'type' 16 | end 17 | 18 | # Default is 'ye good ol varchar(255) 19 | def column_defaults 20 | { :type => :string } 21 | end 22 | 23 | def column 24 | column_defaults.merge(@options) 25 | end 26 | 27 | def ==(compared_column) 28 | # Ideally we should compare attributes, but unfortunately not all drivers report enough statistics for this 29 | column[:type] == compared_column[:type] 30 | end 31 | 32 | def mock 33 | @value || self.class.default_mock 34 | end 35 | 36 | def serialized? 37 | false 38 | end 39 | 40 | # Default mock should be overridden in derived classes 41 | def self.default_mock 42 | short_text_mock 43 | end 44 | 45 | def self.long_text_mock 46 | (1..3).to_a.collect { Faker::Lorem.paragraph }.join("\n") 47 | end 48 | 49 | def self.short_text_mock 50 | Faker::Lorem.sentence 51 | end 52 | 53 | # Decides if and how a column will be changed 54 | # Provide the details of a previously column, or simply nil to create a new column 55 | def structure_changes_from(current_structure = nil) 56 | new_structure = column 57 | 58 | if current_structure 59 | # General RDBMS data loss scenarios 60 | if new_structure[:limit] && current_structure[:limit].to_i != new_structure[:limit].to_i || 61 | new_structure[:type] != current_structure[:type] || 62 | !new_structure[:default].nil? && column_default_changed?(current_structure[:default], new_structure[:default]) 63 | 64 | column 65 | else 66 | nil # No changes 67 | end 68 | else 69 | column 70 | end 71 | end 72 | 73 | def dangerous_migration_from?(current_structure = nil) 74 | current_structure && (column[:type] != :text && [:string, :text].include?(current_structure[:type]) && column[:type] != current_structure[:type]) 75 | end 76 | 77 | def column_default_changed?(old_default, new_default) 78 | new_default.to_s != old_default.to_s 79 | end 80 | 81 | def self.migrant_data_type?; true; end 82 | end 83 | end 84 | 85 | require 'dsl/data_types/primitives' 86 | require 'dsl/data_types/semantic' 87 | 88 | # Add internal types used by Migrant 89 | module DataType 90 | class Polymorphic < Base; end 91 | 92 | class ForeignKey < Fixnum 93 | def column 94 | {:type => :integer} 95 | end 96 | 97 | def self.default_mock 98 | nil # Will get overridden later by ModelExtensions 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/dsl/data_types/primitives.rb: -------------------------------------------------------------------------------- 1 | module DataType 2 | # Boolean 3 | class TrueClass < Base 4 | def column_defaults 5 | {:type => :boolean} 6 | end 7 | 8 | def mock 9 | self.class.default_mock 10 | end 11 | 12 | def self.default_mock 13 | true 14 | end 15 | 16 | def column_default_changed?(old_default, new_default) 17 | old_default.to_s[0] != new_default.to_s[0] 18 | end 19 | end 20 | 21 | class FalseClass < TrueClass 22 | def self.default_mock 23 | false 24 | end 25 | 26 | def column_default_changed?(old_default, new_default) 27 | old_default.to_s[0] != new_default.to_s[0] 28 | end 29 | end 30 | 31 | # Datetime 32 | class Date < Base 33 | def column_defaults 34 | {:type => :date} 35 | end 36 | 37 | def self.default_mock 38 | ::Date.today 39 | end 40 | end 41 | 42 | class Time < Date 43 | def column_defaults 44 | {:type => :datetime} 45 | end 46 | 47 | def self.default_mock 48 | ::Time.now 49 | end 50 | end 51 | 52 | # Integers 53 | class Fixnum < Base 54 | def column_defaults 55 | {:type => :integer}.tap do |options| 56 | options.merge!(:limit => @value.size) if @value > 2147483647 # 32-bit limit. Not checking size here because a 64-bit OS always has at least 8 byte size 57 | end 58 | end 59 | 60 | def self.default_mock 61 | rand(999999).to_i 62 | end 63 | end 64 | 65 | class Bignum < Fixnum 66 | def column_defaults 67 | {:type => :integer, :limit => ((@value.size > 8)? @value.size : 8) } 68 | end 69 | end 70 | 71 | class Integer < Bignum 72 | end 73 | 74 | class Float < Base 75 | def column_defaults 76 | {:type => :float} 77 | end 78 | 79 | def self.default_mock 80 | rand(100).to_f-55.0 81 | end 82 | end 83 | 84 | # Range (0..10) 85 | class Range < Base 86 | def column_defaults 87 | definition = {:type => :integer} 88 | definition 89 | end 90 | end 91 | 92 | # Strings 93 | class String < Base 94 | def initialize(options) 95 | super(options) 96 | @value ||= '' 97 | end 98 | 99 | def column_defaults 100 | if @value.match(/[\d,]+\.\d{2}$/) 101 | return Currency.new(@options).column_defaults 102 | else 103 | return @value.match(/[\r\n\t]/)? { :type => :text }.merge(@options) : super 104 | end 105 | end 106 | 107 | def mock 108 | @value || ((self.column_defaults[:type] == :text)? self.class.long_text_mock : self.class.default_mock ) 109 | end 110 | end 111 | 112 | # Symbol (defaults, specified by user) 113 | class Symbol < Base 114 | def column_defaults 115 | # Just construct whatever the user wants 116 | {:type => ((serialized?)? :text : @value) || :string }.merge(@options) 117 | end 118 | 119 | def mock 120 | case @value || :string 121 | when :text then self.class.long_text_mock 122 | when :string then self.class.short_text_mock 123 | when :integer then Fixnum.default_mock 124 | when :decimal, :float then Float.default_mock 125 | when :datetime, :date then Date.default_mock 126 | when :serialized, :serialize then (@example)? @example : Hash.default_mock 127 | end 128 | end 129 | 130 | def serialized? 131 | %W{serialized serialize}.include?(@value.to_s) 132 | end 133 | 134 | def serialized_class_name 135 | klass_name = (@example)? @example.class.to_s : "Hash" 136 | 137 | klass_name.constantize 138 | end 139 | end 140 | 141 | # Objects 142 | class Object < Base 143 | def column_defaults 144 | {:type => :text } 145 | end 146 | 147 | def self.default_mock 148 | self.native_class.new 149 | end 150 | 151 | def mock 152 | @value || self.default_mock 153 | end 154 | 155 | def serialized? 156 | true 157 | end 158 | 159 | def serialized_class_name 160 | self.class.native_class 161 | end 162 | 163 | def self.native_class 164 | self.to_s.split('::').last.constantize 165 | end 166 | end 167 | 168 | # Store these objects serialized by default 169 | class Array < Object; end; 170 | class Hash < Object; end; 171 | class OpenStruct < Object; end; 172 | end 173 | 174 | 175 | -------------------------------------------------------------------------------- /lib/dsl/data_types/semantic.rb: -------------------------------------------------------------------------------- 1 | module DataType 2 | class Currency < Base 3 | def column_defaults 4 | {:type => :decimal, :precision => 10, :scale => 2} 5 | end 6 | 7 | def self.default_mock 8 | rand(9999999).to_f+0.51 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/migrations.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | 3 | class Migrations < Rails::Generators::Base 4 | def migrate 5 | Migrant::MigrationGenerator.new.run 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/model.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/active_record' 2 | 3 | module Migrant 4 | class Model < ActiveRecord::Generators::Base 5 | argument :attributes, :type => :array, :default => [], :banner => "field:type field:type" 6 | desc "The migrant:model generator creates a skeleton ActiveRecord model for use with Migrant." 7 | source_root File.expand_path("../templates", __FILE__) 8 | 9 | def create_model_file 10 | template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") 11 | end 12 | 13 | hook_for :test_framework 14 | 15 | def protip 16 | puts "\nNow, go and edit app/models/#{file_name}.rb and/or generate more models, then run 'rake db:upgrade' to generate your schema." 17 | end 18 | 19 | protected 20 | def parent_class_name 21 | options[:parent] || "ActiveRecord::Base" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/templates/change_migration.erb: -------------------------------------------------------------------------------- 1 | class <%= @activity.camelize.gsub(/\s/, '') %> < ActiveRecord::Migration<%= @class_suffix %> 2 | def self.up 3 | <% @columns[:added].each do |field, options| %> 4 | add_column :<%= @table_name %>, :<%= field %>, :<%= options.delete(:type) %><%= (options.blank?)? '': ", "+options.inspect[1..-2] %> 5 | <% end -%> 6 | <% @columns[:changed].each do |field, options, old_options| %> 7 | change_column :<%= @table_name %>, :<%= field %>, :<%= options.delete(:type) %><%= (options.blank?)? '': ", "+options.inspect[1..-2] %> 8 | <% end -%> 9 | <% @columns[:transferred].each do |source, target| %> 10 | puts "-- copy data from :<%= source %> to :<%= target %>" 11 | <%= @table_name.classify %>.update_all("<%= target %> = <%= source %>") 12 | <% end -%> 13 | <% @columns[:renamed].each do |old_name, new_name| %> 14 | rename_column :<%= @table_name %>, :<%= old_name %>, :<%= new_name %> 15 | <% end -%> 16 | <% @columns[:deleted].each do |field, options| %> 17 | remove_column :<%= @table_name %>, :<%= field %> 18 | <% end -%> 19 | <% @indexes.each do |index, options| %> 20 | add_index :<%= @table_name %>, <%= index.inspect %> 21 | <% end -%> 22 | end 23 | 24 | def self.down 25 | <% @columns[:deleted].each do |field, options| %> 26 | add_column :<%= @table_name %>, :<%= field %>, :<%= options.delete(:type) %><%= (options.blank?)? '': ", "+options.inspect[1..-2] %> 27 | <% end -%> 28 | <% @columns[:renamed].each do |old_name, new_name| %> 29 | rename_column :<%= @table_name %>, :<%= new_name %>, :<%= old_name %> 30 | <% end -%> 31 | <% @columns[:transferred].each do |source, target| %> 32 | puts "-- copy data from :<%= target %> to :<%= source %>" 33 | <%= @table_name.classify %>.update_all("<%= source %> = <%= target %>") 34 | <% end -%> 35 | <% @columns[:changed].each do |field, options, old_options| %> 36 | change_column :<%= @table_name %>, :<%= field %>, :<%= old_options.delete(:type) %><%= (old_options.blank?)? '': ", "+old_options.inspect[1..-2] %> 37 | <% end -%> 38 | <% @columns[:added].each do |field, options| %> 39 | remove_column :<%= @table_name %>, :<%= field %> 40 | <% end -%> 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/generators/templates/create_migration.erb: -------------------------------------------------------------------------------- 1 | class <%= @activity.camelize.gsub(/\s/, '') %> < ActiveRecord::Migration<%= @class_suffix %> 2 | def self.up 3 | create_table :<%= @table_name %> do |t| 4 | <% @columns.each do |field, options| %> 5 | t.<%= options.delete(:type) %> :<%= field %><%= (options.blank?)? '': ", "+options.to_a.collect { |o| ":#{o[0]}=>#{o[1].inspect}" }.sort.join(', ') %> 6 | <% end %> 7 | end 8 | <% @indexes.each do |index| %> 9 | add_index :<%= @table_name %>, <%= index.inspect %> 10 | <% end -%> 11 | end 12 | 13 | def self.down 14 | drop_table :<%= @table_name %> 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/templates/create_migration.rb: -------------------------------------------------------------------------------- 1 | class <%= @activity.camelize.gsub(/\s/, '') %> < ActiveRecord::Migration<%= @class_suffix %> 2 | def self.up 3 | create_table :<%= @table_name %> do |t| 4 | <% @columns.each do |field, options| %> 5 | t.<%= options.delete(:type) %> :<%= field %><%= (options.blank?)? '': ", "+options.inspect[1..-2] %> 6 | <% end %> 7 | end 8 | <% @indexes.each do |index, options| %> 9 | add_index :<%= @table_name %>, <%= index.inspect %> 10 | <% end -%> 11 | end 12 | 13 | def self.down 14 | drop_table :<%= @table_name %> 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/templates/model.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < <%= parent_class_name.classify %> 2 | <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> 3 | belongs_to :<%= attribute.name %> 4 | <% end -%> 5 | structure do 6 | <% unless attributes.blank? 7 | max_field_length = attributes.collect { |attribute| attribute.name.to_s}.max { |name| name.length }.length 8 | attributes.reject { |attr| attr.reference? }.each do |attribute| -%> 9 | <%= attribute.name.to_s.ljust(max_field_length) %> :<%= attribute.type %> 10 | <% end 11 | end -%> 12 | 13 | timestamps 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/migrant.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_record' 3 | require 'dsl/data_type' 4 | require 'migrant/schema' 5 | require 'migrant/model_extensions' 6 | require 'migrant/migration_generator' 7 | 8 | module Migrant 9 | require 'railtie' if defined?(Rails) 10 | 11 | ActiveSupport.on_load :active_record do 12 | ActiveRecord::Base.extend(Migrant::ModelExtensions) 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /lib/migrant/migration_generator.rb: -------------------------------------------------------------------------------- 1 | require 'erubi' 2 | require 'term/ansicolor' 3 | 4 | module Migrant 5 | class MigrationGenerator 6 | def run 7 | # Ensure db/migrate path exists before starting 8 | FileUtils.mkdir_p(Rails.root.join('db', 'migrate')) 9 | @possible_irreversible_migrations = false 10 | 11 | migrator = (ActiveRecord::Migrator.public_methods.include?(:open))? 12 | ActiveRecord::Migrator.open(migrations_path) : 13 | ActiveRecord::Migrator.new(:up, migrations_path) 14 | 15 | @class_suffix = defined?(ActiveRecord::Migration::Compatibility)? "[#{Rails.version[/^\d+\.\d+/]}]" : '' 16 | 17 | unless migrator.pending_migrations.blank? 18 | log "Aborting as this database has not yet run all the existing migrations.\n\nMost likely you just need to run rake db:migrate instead of rake db:upgrade in this environment.", :error 19 | return false 20 | end 21 | 22 | # Get all tables and compare to the desired schema 23 | # The next line is an evil hack to recursively load all model files in app/models 24 | # This needs to be done because Rails normally lazy-loads these files, resulting a blank descendants list of AR::Base 25 | model_root = "#{Rails.root.to_s}/app/models/" 26 | 27 | Dir["#{model_root}**/*.rb"].each do |file| 28 | if (model_name = file.sub(model_root, '').match(/(.*)?\.rb$/)) 29 | model_name[1].camelize.safe_constantize 30 | end 31 | end 32 | 33 | # Rails 3.2+ caches table (non) existence so this needs to be cleared before we start 34 | ActiveRecord::Base.connection.schema_cache.clear! if ActiveRecord::Base.connection.respond_to?(:schema_cache) 35 | 36 | ActiveRecord::Base.descendants.select { |model| model.structure_defined? && model.schema.requires_migration? }.each do |model| 37 | model.reset_column_information # db:migrate doesn't do this 38 | @table_name = model.table_name 39 | @columns = Hash[[:changed, :added, :deleted, :renamed, :transferred].collect { |a| [a,[]] }] 40 | 41 | if model.table_exists? 42 | # Structure ActiveRecord::Base's column information so we can compare it directly to the schema 43 | db_schema = Hash[*model.columns.collect {|c| [c.name.to_sym, Hash[*[:type, :limit, :default].map { |type| [type, c.send(type)] }.flatten] ] }.flatten] 44 | model.schema.columns.to_a.sort { |a,b| a.to_s <=> b.to_s }.each do |field_name, data_type| 45 | if data_type.dangerous_migration_from?(db_schema[field_name]) && 46 | ask_user("#{model}: '#{field_name}': Converting from ActiveRecord type #{db_schema[field_name][:type]} to #{data_type.column[:type]} could cause data loss. Continue?", %W{Yes No}, true) == "No" 47 | log "Aborting dangerous action on #{field_name}." 48 | elsif (options = data_type.structure_changes_from(db_schema[field_name])) 49 | if db_schema[field_name] 50 | change_column(field_name, options, db_schema[field_name]) 51 | else 52 | add_column(field_name, options) 53 | end 54 | end 55 | end 56 | 57 | # Removed rows 58 | unless model.schema.partial? 59 | db_schema.reject { |field_name, options| field_name.to_s == model.primary_key || model.schema.columns.keys.include?(field_name) }.each do |removed_field_name, options| 60 | case ask_user("#{model}: '#{removed_field_name}' is no longer in use.", (@columns[:added].blank?)? %W{Destroy Ignore} : %W{Destroy Move Ignore}) 61 | when 'Destroy' then delete_column(removed_field_name, db_schema[removed_field_name]) 62 | when 'Move' then 63 | target = ask_user("Move '#{removed_field_name}' to:", @columns[:added].collect(&:first)) 64 | target_column = model.schema.columns[target] 65 | 66 | unless target_column.dangerous_migration_from?(db_schema[removed_field_name]) 67 | target_column.structure_changes_from(db_schema[removed_field_name]) 68 | move_column(removed_field_name, target, db_schema[removed_field_name], target_column) 69 | else 70 | case ask_user("Unable to safely move '#{removed_field_name}' to '#{target}'. Keep the original column for now?", %W{Yes No}, true) 71 | when 'No' then delete_column(removed_field_name, db_schema[removed_field_name]) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | destroyed_columns = @columns[:deleted].reject { |field, options| @columns[:transferred].collect(&:first).include?(field) } 78 | unless destroyed_columns.blank? 79 | if ask_user("#{model}: '#{destroyed_columns.collect(&:first).join(', ')}' and associated data will be DESTROYED in all environments. Continue?", %W{Yes No}, true) == 'No' 80 | log "Okay, not removing anything for now." 81 | @columns[:deleted] = [] 82 | end 83 | end 84 | 85 | # For adapters that can report indexes, add as necessary 86 | if ActiveRecord::Base.connection.respond_to?(:indexes) 87 | current_indexes = ActiveRecord::Base.connection.indexes(model.table_name).collect { |index| (index.columns.length == 1)? index.columns.first.to_sym : index.columns.collect(&:to_sym) } 88 | @indexes = model.schema.indexes.uniq.reject { |index| current_indexes.include?(index) }.collect do |field_name| 89 | description = (field_name.respond_to?(:join))? field_name.join('_') : field_name.to_s 90 | 91 | [field_name, description] 92 | end 93 | 94 | # Don't spam the user with indexes that columns are being created with 95 | @new_indexes = @indexes.reject { |index, options| @columns[:changed].detect { |c| c.first == index } || @columns[:added].detect { |c| c.first == index } } 96 | end 97 | 98 | next if @columns[:changed].empty? && @columns[:added].empty? && @columns[:renamed].empty? && @columns[:transferred].empty? && @columns[:deleted].empty? && @indexes.empty? # Nothing to do for this table 99 | 100 | # Example: changed_table_added_something_and_modified_something 101 | @activity = 'changed_'+model.table_name+[['added', @columns[:added]], ['modified', @columns[:changed]], ['deleted', destroyed_columns], 102 | ['moved', @columns[:transferred]], ['renamed', @columns[:renamed]], ['indexed', @new_indexes]].reject { |v| v[1].empty? }.collect { |v| "_#{v[0]}_"+v[1].collect(&:last).join('_') }.join('_and') 103 | @activity = @activity.split('_')[0..2].join('_')+'_with_multiple_changes' if @activity.length >= 240 # Most filesystems will raise Errno::ENAMETOOLONG otherwise 104 | 105 | render('change_migration') 106 | else 107 | @activity = "create_#{model.table_name}" 108 | @columns = model.schema.column_migrations 109 | @indexes = model.schema.indexes.uniq 110 | 111 | render("create_migration") 112 | end 113 | 114 | filename = "#{migrations_path}/#{next_migration_number}_#{@activity}.rb" 115 | File.open(filename, 'w') { |migration| migration.write(@output) } 116 | log "Wrote #{filename}..." 117 | end 118 | 119 | if @possible_irreversible_migrations 120 | log "*** One or more move operations were performed, which potentially could cause data loss on db:rollback. \n*** Please review your migrations before committing!", :warning 121 | end 122 | 123 | true 124 | end 125 | 126 | private 127 | def add_column(name, options) 128 | @columns[:added] << [name, options, name] 129 | end 130 | 131 | def change_column(name, new_schema, old_schema) 132 | if new_schema[:default] && new_schema[:default].respond_to?(:to_s) && new_schema[:default].to_s.length < 31 133 | change_description = "#{name}_defaulted_to_#{new_schema[:default].to_s.underscore.gsub(/\./, '')}" 134 | else 135 | change_description = name 136 | end 137 | 138 | @columns[:changed] << [name, new_schema, old_schema, change_description] 139 | end 140 | 141 | def delete_column(name, current_structure) 142 | @columns[:deleted] << [name, current_structure, name] 143 | end 144 | 145 | def move_column(old_name, new_name, old_schema, new_schema) 146 | if new_schema == old_schema 147 | @columns[:renamed] << [old_name, new_name, old_name] 148 | @columns[:added].reject! { |a| a.first == new_name } # Don't add the column too 149 | else 150 | @possible_irreversible_migrations = true 151 | @columns[:transferred] << [old_name, new_name, old_name] # Still need to add the column, just transfer the data afterwards 152 | delete_column(old_name, old_schema) 153 | end 154 | end 155 | 156 | def migrations_path 157 | Rails.root.join(ActiveRecord::Migrator.migrations_paths.first) 158 | end 159 | 160 | include Term::ANSIColor 161 | def ask_user(message, choices, warning=false) 162 | mappings = choices.uniq.inject({}) do |mappings, choice| 163 | choice_string = choice.to_s 164 | choice_string.length.times do |i| 165 | mappings.merge!(choice_string[i..i] => choice) and break unless mappings.keys.include?(choice_string[i..i]) 166 | end 167 | mappings.merge!(choice_string => choice) unless mappings.values.include?(choice) 168 | mappings 169 | end 170 | 171 | begin 172 | prompt = "> #{message} [#{mappings.collect { |shortcut, choice| choice.to_s.sub(shortcut, '('+shortcut+')') }.join(' / ')}]: " 173 | if warning 174 | STDOUT.print red, bold, prompt, reset 175 | else 176 | STDOUT.print bold, prompt, reset 177 | end 178 | STDOUT.flush 179 | input = STDIN.gets.downcase 180 | end until (choice = mappings.detect { |shortcut, choice| [shortcut.downcase,choice.to_s.downcase].include?(input.downcase.strip) }) 181 | choice.last 182 | end 183 | 184 | def log(message, type=:info) 185 | STDOUT.puts( 186 | case type 187 | when :error 188 | [red, bold, message, reset] 189 | when :warning 190 | [yellow, message, reset] 191 | else 192 | message 193 | end 194 | ) 195 | end 196 | # See ActiveRecord::Generators::Migration 197 | # Only generating a migration to each second is a problem.. because we generate everything in the same second 198 | # So we have to add further "pretend" seconds. This WILL cause problems. 199 | # TODO: Patch ActiveRecord to end this nonsense. 200 | def next_migration_number #:nodoc: 201 | highest = Dir.glob(migrations_path.to_s+"/[0-9]*_*.rb").collect do |file| 202 | File.basename(file).split("_").first.to_i 203 | end.max 204 | 205 | if ActiveRecord::Base.timestamped_migrations 206 | base = Time.now.utc.strftime("%Y%m%d%H%M%S").to_s 207 | (highest.to_i >= base.to_i)? (highest + 1).to_s : base 208 | else 209 | (highest.to_i + 1).to_s 210 | end 211 | end 212 | 213 | def render(template_name) 214 | template = File.read(File.join(File.dirname(__FILE__), "../generators/templates/#{template_name}.erb")) 215 | erubi = Erubi::Engine.new(template, :trim => true) 216 | @output = eval(erubi.src, binding) 217 | end 218 | end 219 | end 220 | 221 | -------------------------------------------------------------------------------- /lib/migrant/model_extensions.rb: -------------------------------------------------------------------------------- 1 | module Migrant 2 | module ModelExtensions 3 | attr_accessor :schema 4 | 5 | def belongs_to(*args) 6 | super 7 | create_migrant_schema 8 | @schema.add_association(self.reflect_on_association(args.first)) 9 | end 10 | 11 | def create_migrant_schema 12 | if self.superclass == ActiveRecord::Base 13 | @schema ||= Schema.new 14 | else 15 | @schema ||= InheritedSchema.new(self.superclass.schema) 16 | end 17 | end 18 | 19 | def structure(type=nil, &block) 20 | # Using instance_*evil* to get the neater DSL on the models. 21 | # So, my_field in the structure block actually calls Migrant::Schema.my_field 22 | 23 | create_migrant_schema 24 | @structure_defined = true 25 | 26 | if self.superclass == ActiveRecord::Base 27 | @schema.define_structure(type, &block) 28 | @schema.validations.each do |field, validation_options| 29 | validations = (validation_options.class == Array)? validation_options : [validation_options] 30 | validations.each do |validation| 31 | validation = (validation.class == Hash)? validation : { validation => true } 32 | self.validates(field, validation) 33 | end 34 | end 35 | # Set up serialized columns as required 36 | @schema.columns.select do |name, column| 37 | if column.serialized? 38 | serialize(name, column.serialized_class_name) 39 | end 40 | end 41 | else 42 | self.superclass.structure(&block) # For STI, cascade all fields onto the parent model 43 | end 44 | end 45 | 46 | def structure_defined? 47 | @schema && @structure_defined || false 48 | end 49 | 50 | # Same as defining a structure block, but with no attributes besides 51 | # relationships (such as in a many-to-many) 52 | def no_structure 53 | structure {} 54 | end 55 | 56 | def reset_structure! 57 | @schema = nil 58 | end 59 | 60 | def mock(attributes={}, recursive=true) 61 | raise NoStructureDefined.new("In order to mock() #{self.to_s}, you need to define a Migrant structure block") unless @schema 62 | 63 | attribs = {} 64 | attribs.merge!(self.superclass.mock_attributes(attributes, recursive)) unless self.superclass == ActiveRecord::Base 65 | new attribs.merge(mock_attributes(attributes, recursive)) 66 | end 67 | 68 | def mock_attributes(attributes={}, recursive=true) 69 | attribs = @schema.columns.collect { |name, data_type| [name, data_type.mock] }.flatten(1) 70 | 71 | # Only recurse to one level, otherwise things get way too complicated 72 | if recursive 73 | attribs += self.reflect_on_all_associations(:belongs_to).collect do |association| 74 | begin 75 | (association.klass.respond_to?(:mock))? [association.name, association.klass.mock({}, false)] : nil 76 | rescue NameError; nil; end # User hasn't defined association, just skip it 77 | end.compact.flatten 78 | end 79 | Hash[*attribs].merge(attributes) 80 | end 81 | 82 | def mock!(attributes={}, recursive=true) 83 | mock(attributes, recursive).tap do |mock| 84 | mock.save! 85 | end 86 | end 87 | end 88 | 89 | class NoStructureDefined < Exception; end; 90 | end 91 | 92 | -------------------------------------------------------------------------------- /lib/migrant/schema.rb: -------------------------------------------------------------------------------- 1 | module Migrant 2 | # Converts the following DSL: 3 | # 4 | # class MyModel < ActiveRecord::Base 5 | # structure do 6 | # my_field "some string" 7 | # end 8 | # end 9 | # into a schema on that model class by calling method_missing(my_field) 10 | # and deciding what the best schema type is for the user's requiredments 11 | class Schema 12 | attr_accessor :indexes, :columns, :validations 13 | 14 | def initialize 15 | @proxy = SchemaProxy.new(self) 16 | @columns = Hash.new 17 | @indexes = Array.new 18 | @validations = Hash.new 19 | @type = :default 20 | end 21 | 22 | def define_structure(type, &block) 23 | @validations = Hash.new 24 | @type = type if type 25 | 26 | # Runs method_missing on columns given in the model "structure" DSL 27 | @proxy.translate_fancy_dsl(&block) if block_given? 28 | end 29 | 30 | def add_association(association) 31 | # Rails 3.1 changes primary_key_name to foreign_key (correct behaviour), so this is essentially backwards compatibility for Rails 3.0 32 | field = (association.respond_to?(:foreign_key))? association.foreign_key.to_sym : association.primary_key_name.to_sym 33 | 34 | case association.macro 35 | when :belongs_to 36 | if association.options[:polymorphic] 37 | @columns[(association.name.to_s+'_type').to_sym] = DataType::Polymorphic.new(:field => field) 38 | @indexes << [(association.name.to_s+'_type').to_sym, field] 39 | end 40 | @columns[field] = DataType::ForeignKey.new(:field => field) 41 | @indexes << field 42 | end 43 | end 44 | 45 | def requires_migration? 46 | true 47 | end 48 | 49 | # If the user defines structure(:partial), irreversible changes are ignored (removing a column, for example) 50 | def partial? 51 | @type == :partial 52 | end 53 | 54 | def column_migrations 55 | @columns.collect {|field, data| [field, data.column] } # All that needs to be migrated 56 | end 57 | 58 | # This is where we decide what the best schema is based on the structure requirements 59 | # The output of this is essentially a formatted schema hash that is processed 60 | # on each model by Migrant::MigrationGenerator 61 | 62 | def add_field(field, data_type = nil, options = {}) 63 | data_type = DataType::String if data_type.nil? 64 | puts [":#{field}", "#{data_type.class.to_s}", "#{options.inspect}"].collect { |s| s.ljust(25) }.join if ENV['DEBUG'] 65 | 66 | # Fields that do special things go here. 67 | if field == :timestamps 68 | add_field(:updated_at, :datetime) 69 | add_field(:created_at, :datetime) 70 | return true 71 | end 72 | 73 | # Add index if explicitly asked 74 | @indexes << field if options.delete(:index) || data_type.class.to_s == 'Hash' && data_type.delete(:index) 75 | @validations[field] = options.delete(:validates) if options[:validates] 76 | options.merge!(:field => field) 77 | 78 | # Matches: description DataType::Paragraph, :index => true 79 | if data_type.is_a?(Class) && data_type.respond_to?(:migrant_data_type?) 80 | @columns[field] = data_type.new(options) 81 | # Matches: description :index => true, :unique => true 82 | else 83 | begin 84 | # Eg. "My field" -> String -> DataType::String 85 | @columns[field] = "DataType::#{data_type.class.to_s}".constantize.new(options.merge(:value => data_type)) 86 | rescue NameError 87 | # We don't have a matching type, throw a warning and default to string 88 | puts "MIGRATION WARNING: No migration implementation for class #{data_type.class.to_s} on field '#{field}', defaulting to string..." 89 | @columns[field] = DataType::Base.new(options) 90 | end 91 | end 92 | end 93 | end 94 | 95 | class InheritedSchema < Schema 96 | attr_accessor :parent_schema 97 | 98 | def initialize(parent_schema) 99 | @parent_schema = parent_schema 100 | @columns = Hash.new 101 | @indexes = Array.new 102 | end 103 | 104 | def requires_migration? 105 | false # All added to base table 106 | end 107 | 108 | def add_association(association) 109 | parent_schema.add_association(association) 110 | end 111 | end 112 | 113 | # Why does this class exist? Excellent question. 114 | # Basically, Kernel gives a whole bunch of global methods, like system, puts, etc. This is bad because 115 | # our DSL relies on being able to translate any arbitrary method into a method_missing call. 116 | # So, we call method missing in this happy bubble where these magic methods don't exist. 117 | # The reason we don't inherit Schema itself in this way, is that we'd lose direct access to all other classes 118 | # derived from Object. Normally this would just mean scope resolution operators all over the class, but the real 119 | # killer is that the DSL would need to reference classes in that manner as well, which would reduce sexy factor by at least 100. 120 | class SchemaProxy < BasicObject 121 | def initialize(binding) 122 | @binding = binding 123 | end 124 | 125 | def translate_fancy_dsl(&block) 126 | self.instance_eval(&block) 127 | end 128 | 129 | # Provides a method for dynamically creating fields (i.e. not part of instance_eval) 130 | def property(*arguments) 131 | method_missing(*arguments) 132 | end 133 | 134 | def method_missing(*args, &block) 135 | field = args.slice!(0) 136 | data_type = args.slice!(0) unless args.first.nil? || args.first.respond_to?(:keys) 137 | 138 | @binding.add_field(field.to_sym, data_type, args.extract_options!) 139 | end 140 | end 141 | end 142 | 143 | -------------------------------------------------------------------------------- /lib/pickle/migrant.rb: -------------------------------------------------------------------------------- 1 | module Pickle 2 | class Migrant < Adapter 3 | def self.factories 4 | model_classes.select { |model| model.respond_to?(:mock) }.collect { |model| new(model) } 5 | end 6 | 7 | def initialize(klass) 8 | @klass, @name = klass, klass.name.underscore.gsub('/', '_') 9 | end 10 | 11 | def create(attrs={}) 12 | @klass.mock!(Hash[attrs.collect { |k,v| [k.to_sym, v] }]) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/railtie.rb: -------------------------------------------------------------------------------- 1 | # lib/my_gem/railtie.rb 2 | #require 'migrant' 3 | #require 'rails' 4 | 5 | module Migrant 6 | class Railtie < Rails::Railtie 7 | rake_tasks do 8 | load "tasks/db.rake" 9 | end 10 | 11 | generators do 12 | load "generators/migrations.rb" 13 | load "generators/model.rb" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/tasks/db.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | desc "Generates migrations as per structure design in your models and runs them" 3 | task :upgrade => :environment do 4 | if Migrant::MigrationGenerator.new.run 5 | puts "\nInvoking db:migrate for #{Rails.env} environment." 6 | Rake::Task['db:migrate'].invoke 7 | end 8 | end 9 | 10 | desc "Provides a shortcut to rolling back and discarding the last migration" 11 | task :downgrade => :environment do 12 | Rake::Task['db:rollback'].invoke 13 | Dir.chdir(Rails.root.join('db', 'migrate')) do 14 | last_migration = Dir.glob('*.rb').sort.last and 15 | File.unlink(last_migration) and 16 | puts "Removed #{Dir.pwd}/#{last_migration}." 17 | end 18 | 19 | Rake::Task['db:test:clone'].invoke unless ENV['RAILS_ENV'] 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /migrant.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "migrant" 7 | spec.version = File.read('VERSION') 8 | spec.authors = ["Pascal Houliston"] 9 | spec.email = ["101pascal@gmail.com"] 10 | 11 | spec.summary = %q{All the fun of ActiveRecord without writing your migrations, and with a dash of mocking.} 12 | spec.description = %q{Easier schema management for Rails that complements your domain model.} 13 | spec.homepage = %q{http://github.com/pascalh1011/migrant} 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 17 | f.match(%r{^(test|spec|features)/}) 18 | end 19 | spec.bindir = "exe" 20 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 21 | spec.require_paths = ["lib"] 22 | 23 | rails_version = ENV['RAILS_VERSION'] || '5.1' 24 | 25 | spec.add_development_dependency "rails", "~> #{rails_version}" 26 | spec.add_development_dependency "bundler", "~> 1.15" 27 | spec.add_development_dependency "rake", "~> 12.0" 28 | spec.add_development_dependency "rspec", "~> 3.0" 29 | spec.add_development_dependency "sqlite3", ">= 1.3.13" 30 | spec.add_development_dependency "coveralls", ">= 0.7.1" 31 | 32 | spec.add_runtime_dependency "erubi", ">= 1.7.0" 33 | spec.add_runtime_dependency "term-ansicolor", ">= 1.6.0" 34 | spec.add_runtime_dependency "faker", ">= 1.8.4" 35 | end 36 | -------------------------------------------------------------------------------- /spec/migration_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec::Matchers.define :have_matching_migration do 4 | match do |template_name| 5 | matched_migration_file = Dir.glob(Rails.root.join('db', 'migrate', '*.rb')). 6 | detect { |f| f.include?(template_name) } 7 | 8 | if matched_migration_file 9 | migration = File.read(matched_migration_file).strip.gsub(/[\.\(\)]/, '') 10 | verified_migration_lines = File.read(File.join(File.dirname(__FILE__), 'support', 'verified_migrations', "#{template_name}.rb")).split("\n") 11 | 12 | missing_lines = verified_migration_lines.select { |line| !migration.match(line.gsub(/[\.\(\)]/, '').strip) } 13 | STDERR.puts "The following lines were missing:\n #{missing_lines.join("\n")}" if missing_lines.any? 14 | 15 | missing_lines.none? 16 | end 17 | end 18 | end 19 | 20 | RSpec.describe Migrant::MigrationGenerator do 21 | context 'from scratch' do 22 | before(:all) { reset_database! } 23 | 24 | it 'creates migrations for all new tables' do 25 | expect(described_class.new.run).to be true 26 | 27 | generated_migrations = Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) 28 | 29 | expect(generated_migrations.length).to eq(5) 30 | 31 | # For all the migrations generated into db/migrate, verify 32 | # them against the pre-verified template 33 | generated_migrations.each do |migration_file| 34 | template_name = migration_file.sub(/^.*\d+_/, '').sub('.rb', '') 35 | 36 | expect(template_name).to have_matching_migration 37 | end 38 | end 39 | end 40 | 41 | def generated_migrations 42 | Dir.glob(Rails.root.join('db', 'migrate', '*.rb')) 43 | end 44 | 45 | context 'after initial migration' do 46 | before(:all) do 47 | # Do initial migration 48 | reset_database! 49 | run_db_upgrade! 50 | end 51 | 52 | it 'generates a migration for new added fields' do 53 | Business.structure do 54 | estimated_value 5000.0 55 | notes 56 | end 57 | 58 | run_db_upgrade! 59 | expect('estimated_value_notes').to have_matching_migration 60 | end 61 | 62 | it 'generates a migration to alter existing columns where no data loss would occur' do 63 | Business.structure do 64 | landline :text 65 | end 66 | 67 | run_db_upgrade! 68 | expect('landline').to have_matching_migration 69 | end 70 | 71 | it 'generates created_at and updated_at when given the column timestamps' do 72 | Business.structure do 73 | timestamps 74 | end 75 | 76 | run_db_upgrade! 77 | expect('created_at').to have_matching_migration 78 | end 79 | 80 | it 'prompts the user to confirm changing existing columns where data loss may occur' do 81 | STDIN._mock_responses('N') 82 | 83 | Business.structure do 84 | landline :integer # Was previously a string, which obviously may incur data loss 85 | end 86 | 87 | expect { 88 | described_class.new.run 89 | }.not_to change { 90 | generated_migrations.length 91 | } 92 | 93 | Business.structure do 94 | landline :text # Undo our bad for the next tests 95 | end 96 | end 97 | 98 | it 'exits immediately if there are pending migrations' do 99 | manual_migration = Rails.root.join("db/migrate/9999999999999999_my_new_migration.rb") 100 | 101 | File.open(manual_migration, 'w') { |f| f.write ' ' } 102 | 103 | ran = described_class.new.run 104 | File.delete(manual_migration) 105 | 106 | expect(ran).to be false 107 | end 108 | 109 | it 'still creates sequential migrations for the folks not using timestamps' do 110 | Business.structure do 111 | new_field_i_made_up 112 | end 113 | 114 | # Remove migrations 115 | ActiveRecord::Base.timestamped_migrations = false 116 | run_db_upgrade! 117 | ActiveRecord::Base.timestamped_migrations = true 118 | 119 | new_migrations = generated_migrations.select { |migration_file| migration_file.include?('new_field_i_made_up') } 120 | 121 | expect(new_migrations.length).to eq(1) 122 | end 123 | 124 | it 'updates a column to include a new default' do 125 | Business.structure do 126 | verified true, :default => true 127 | end 128 | 129 | run_db_upgrade! 130 | expect('modified_verified').to have_matching_migration 131 | end 132 | 133 | it 'updates indexes on a model' do 134 | Business.structure do 135 | name "The Kernel's favourite fried chickens.", :index => true 136 | end 137 | 138 | run_db_upgrade! 139 | expect('businesses_indexed_name').to have_matching_migration 140 | end 141 | 142 | it 'does not remove columns when the user does not confirm' do 143 | Chameleon.reset_structure! 144 | Chameleon.no_structure 145 | 146 | STDIN._mock_responses('D', 'n') 147 | 148 | expect { 149 | run_db_upgrade! 150 | }.not_to change { generated_migrations.length } 151 | end 152 | 153 | it 'removes columns when requested and confirmed by the user' do 154 | Chameleon.reset_structure! 155 | Chameleon.no_structure 156 | 157 | STDIN._mock_responses('D', 'y') 158 | 159 | run_db_upgrade! 160 | expect('deleted_spots').to have_matching_migration 161 | end 162 | 163 | it 'renames a column missing from the schema to a new column specified by the user' do 164 | Chameleon.structure do 165 | old_spots 166 | end 167 | 168 | run_db_upgrade! 169 | 170 | Chameleon.reset_structure! 171 | Chameleon.structure do 172 | new_spots 173 | end 174 | STDIN._mock_responses('M', 'new_spots') 175 | 176 | run_db_upgrade! 177 | expect('renamed_old_spots').to have_matching_migration 178 | end 179 | 180 | it 'transfers data to an new incompatible column if the operation is safe' do 181 | Chameleon.reset_column_information 182 | Chameleon.create!(:new_spots => "22") 183 | Chameleon.reset_structure! 184 | Chameleon.structure do 185 | new_longer_spots "100", :as => :text 186 | end 187 | STDIN._mock_responses('M', 'new_longer_spots', 'M') 188 | 189 | run_db_upgrade! 190 | expect('chameleons_added_new_longer_spots_and_moved_new_spots').to have_matching_migration 191 | 192 | Chameleon.reset_column_information 193 | expect(Chameleon.first.new_longer_spots).to eq("22") 194 | end 195 | 196 | it "removes any column if a user elects to when a column can't be moved due to incompatible types" do 197 | Chameleon.reset_structure! 198 | Chameleon.structure do 199 | incompatible_spot 5 200 | end 201 | 202 | STDIN._mock_responses('M', 'incompatible_spot', 'N', 'Y') 203 | run_db_upgrade! 204 | expect('added_incompatible_spot_and_deleted_new_longer_spots').to have_matching_migration 205 | end 206 | 207 | it 'recursively generates mocks for every model' do 208 | BusinessCategory.structure do 209 | test_mockup_of_text :text 210 | test_mockup_of_string :string 211 | test_mockup_of_integer :integer 212 | test_mockup_of_float :float 213 | test_mockup_of_datetime :datetime 214 | test_mockup_of_currency DataType::Currency 215 | test_mockup_serialized :serialized 216 | test_mockup_hash OpenStruct.new({'a' => 'b'}) 217 | test_mockup_serialized_example :serialized, :example => OpenStruct.new({'c' => 'd'}) 218 | end 219 | 220 | BusinessCategory.belongs_to(:notaclass, :polymorphic => true) 221 | run_db_upgrade! 222 | 223 | BusinessCategory.reset_column_information 224 | m = BusinessCategory.mock! 225 | mock = BusinessCategory.last 226 | 227 | expect(mock).to_not be_nil 228 | expect(mock.test_mockup_of_text).to be_a(String) 229 | expect(mock.test_mockup_of_string).to be_a(String) 230 | expect(mock.test_mockup_of_integer).to be_a(Fixnum) 231 | expect(mock.test_mockup_of_float).to be_a(Float) 232 | expect(mock.test_mockup_of_currency).to be_a(BigDecimal) 233 | expect(mock.test_mockup_of_datetime).to be_a(Time) 234 | expect(DataType::Base.default_mock).to be_a(String) 235 | expect(mock.test_mockup_serialized).to be_a(Hash) 236 | expect(mock.test_mockup_hash).to be_a(OpenStruct) 237 | expect(mock.test_mockup_hash.a).to eq('b') 238 | expect(mock.test_mockup_serialized_example).to be_a(OpenStruct) 239 | expect(mock.test_mockup_serialized_example.c).to eq('d') 240 | end 241 | 242 | it 'not rescursively generate mocks for an inherited model when prohibited by the user' do 243 | category_mock = BusinessCategory.mock!({}, false) 244 | expect(category_mock).to_not be_nil 245 | end 246 | 247 | it 'generates example mocks for an inherited model when STI is in effect' do 248 | expect(Customer.mock.average_rating).to eq(5.0) 249 | expect(Customer.mock.email).to eq("somebody@somewhere.com") 250 | expect(Customer.mock).to be_a(Customer) 251 | end 252 | end 253 | end -------------------------------------------------------------------------------- /spec/model_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec::Matchers.define :include_matching_schema do |name, options| 4 | match do |model| 5 | field = model.schema.column_migrations.detect {|m| m.first == name } 6 | 7 | field && options.all? do |key, expected_value| 8 | field[1][key] == expected_value 9 | end 10 | end 11 | end 12 | 13 | RSpec::Matchers.define :include_schema_index_on do |column_name| 14 | match do |model| 15 | model.schema.indexes.include?(column_name) 16 | end 17 | end 18 | 19 | RSpec.describe Migrant::ModelExtensions do 20 | context "The schema generator" do 21 | it "generates a foreign key field for a belongs_to association" do 22 | expect(Business).to include_matching_schema(:user_id, type: :integer) 23 | expect(BusinessCategory).to include_matching_schema(:business_id, type: :integer) 24 | expect(BusinessCategory).to include_matching_schema(:category_id, type: :integer) 25 | expect(User).to include_matching_schema(:category_id, type: :integer) 26 | end 27 | 28 | it "generates foreign key fields for a *polymorphic* belongs_to association" do 29 | expect(Business).to include_matching_schema(:owner_id, type: :integer) 30 | expect(Business).to include_matching_schema(:owner_type, type: :string) 31 | end 32 | 33 | it "generates a string column when given a string example" do 34 | expect(Business).to include_matching_schema(:name, {}) 35 | end 36 | 37 | it "generates a datetime column when given a date or time example" do 38 | expect(Business).to include_matching_schema(:date_established, type: :datetime) 39 | expect(Business).to include_matching_schema(:next_sale, type: :datetime) 40 | expect(Business).to include_matching_schema(:date_registered, type: :date) 41 | end 42 | 43 | it "generates a smallint column when given a small range" do 44 | expect(Business).to include_matching_schema(:operating_days, type: :integer) 45 | end 46 | 47 | it "generates a large integer (size 8) for any bignum types" do 48 | expect(Category).to include_matching_schema(:serial_number, { 49 | type: :integer, 50 | limit: 8 51 | }) 52 | end 53 | 54 | it "generates a string column when given a sentence" do 55 | expect(Business).to include_matching_schema(:summary, type: :string) 56 | end 57 | 58 | it "generates a text column when given a long paragraph" do 59 | expect(Business).to include_matching_schema(:address, type: :text) 60 | end 61 | 62 | it "passes on any options provided in a structure block" do 63 | expect(User).to include_matching_schema(:average_rating, { 64 | type: :float, 65 | default: 0.0 66 | }) 67 | end 68 | 69 | it "generates a boolean column when a true or false is given" do 70 | expect(Business).to include_matching_schema(:verified, type: :boolean) 71 | end 72 | 73 | it "generates a column verbatim if no type is specified" do 74 | expect(Business).to include_matching_schema(:location, { 75 | type: :string, 76 | limit: 127 77 | }) 78 | end 79 | 80 | it "generates a string column when no options are given" do 81 | expect(Category).to include_matching_schema(:title, type: :string) 82 | expect(Category).to include_matching_schema(:summary, type: :string) 83 | end 84 | 85 | it "generates a decimal column when a currency is given" do 86 | expect(User).to include_matching_schema(:money_spent, type: :decimal, scale: 2) 87 | expect(User).to include_matching_schema(:money_gifted, type: :decimal, scale: 2) 88 | end 89 | 90 | it "generates a floating point column when a decimal is given" do 91 | expect(User).to include_matching_schema(:average_rating, type: :float) 92 | end 93 | 94 | it "generates a string column when an email example or class is given" do 95 | expect(User).to include_matching_schema(:email, type: :string) 96 | end 97 | 98 | it "generates indexes for all foreign keys automatically" do 99 | expect(Business).to include_schema_index_on(:user_id) 100 | expect(Business).to include_schema_index_on([:owner_type, :owner_id]) 101 | 102 | expect(BusinessCategory).to include_schema_index_on(:business_id) 103 | expect(BusinessCategory).to include_schema_index_on(:category_id) 104 | end 105 | 106 | it "generates indexes on any column when explicitly asked to" do 107 | expect(Category).to include_schema_index_on(:title) 108 | end 109 | 110 | it "generates a text column for serialized fields" do 111 | expect(Business).to include_matching_schema(:awards, type: :text) 112 | expect(Business).to include_matching_schema(:managers, type: :text) 113 | 114 | expect(Business.schema.columns[:awards].mock.class).to eq(Array) 115 | expect(Business.schema.columns[:managers].mock.class).to eq(Hash) 116 | 117 | expect(Business.schema.columns[:awards].serialized?).to be true 118 | expect(Business.schema.columns[:managers].serialized?).to be true 119 | end 120 | 121 | it "still indicates a structure is not defined if a belongs_to association is added" do 122 | expect(NonMigrantModel.structure_defined?).to be false 123 | expect(Business.structure_defined?).to be true 124 | end 125 | end 126 | 127 | context "validations" do 128 | before(:all) do 129 | reset_database! 130 | run_db_upgrade! 131 | end 132 | 133 | it 'validates via ActiveRecord when the validates symbol is supplied' do 134 | Business.structure do 135 | website :string, :validates => :presence 136 | end 137 | 138 | business = Business.create 139 | expect(business.errors).to include(:website) 140 | end 141 | 142 | it 'validate via ActiveRecord when the full validation hash is supplied' do 143 | Category.structure do 144 | summary :string, :validates => { :format => { :with => /Symphony\d/ } } 145 | end 146 | 147 | bad_category = Category.create 148 | good_category = Category.create(:summary => "Symphony5") 149 | 150 | expect(bad_category.errors).to include(:summary) 151 | expect(good_category.errors).to_not include(:summary) 152 | end 153 | 154 | it 'validates via ActiveRecord when no field name is given' do 155 | User.structure do 156 | email :validates => :presence 157 | end 158 | 159 | user = User.create 160 | expect(user.errors).to include(:email) 161 | end 162 | 163 | it 'validate multiple validations via ActiveRecord when an array is given' do 164 | User.structure do 165 | name "ABCD", :validates => [:presence, {:length => {:maximum => 4}}] 166 | end 167 | 168 | not_present = User.create 169 | too_long = User.create(:name => "Textthatistoolong") 170 | correct = User.create(:name => "ABC") 171 | 172 | expect(not_present.errors).to include(:name) 173 | expect(too_long.errors).to include(:name) 174 | expect(correct.errors).to_not include(:name) 175 | end 176 | end 177 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | require 'simplecov' 4 | require 'coveralls' 5 | 6 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 7 | SimpleCov::Formatter::HTMLFormatter, 8 | Coveralls::SimpleCov::Formatter 9 | ] 10 | 11 | SimpleCov.start do 12 | add_filter '/spec' 13 | add_filter '/lib/tasks' 14 | add_filter '/lib/railtie' 15 | add_filter '/tmp' 16 | 17 | add_group 'Core Extensions', '/lib/migrant' 18 | add_group 'DSL', '/lib/datatype' 19 | end 20 | 21 | require "migrant" 22 | require 'rails' 23 | 24 | # Create a blank Rails app so Rails.root etc. works 25 | class App < Rails::Application 26 | config.eager_load = false 27 | config.root = 'tmp' 28 | end 29 | 30 | GEM_ROOT = File.join(File.dirname(__FILE__), '..') 31 | Dir[File.join(GEM_ROOT, 'spec', 'support', '*.rb')].each { |f| require f } 32 | 33 | module DatabaseManagement 34 | def reset_database! 35 | begin 36 | ActiveRecord::Base.connection_pool.disconnect! 37 | rescue ActiveRecord::ConnectionNotEstablished 38 | # Already disconnected, mostly likely on startup 39 | end 40 | 41 | db_config = { 42 | adapter: 'sqlite3', 43 | database: ':memory:' 44 | } 45 | 46 | ActiveRecord::Base.establish_connection(db_config) 47 | 48 | # Remove any migrations from previous tests 49 | Dir.glob(File.join(GEM_ROOT, 'tmp', 'db', 'migrate', '*')).each do |migration| 50 | File.unlink(migration) 51 | end 52 | end 53 | 54 | def run_db_upgrade! 55 | Migrant::MigrationGenerator.new.run or raise "Failed to run migration generator" 56 | ActiveRecord::Tasks::DatabaseTasks.migrate 57 | end 58 | end 59 | 60 | # Database setup 61 | log_directory = File.join(GEM_ROOT, 'log') 62 | FileUtils.mkdir_p(log_directory) 63 | ActiveRecord::Base.logger = Logger.new(File.join(log_directory, 'sql.log')) 64 | 65 | Class.new.extend(DatabaseManagement).reset_database! 66 | 67 | # Mock some stubs on STDIN's eigenclass so we can fake user input 68 | class << STDIN 69 | # Simple mock for simulating user inputs 70 | def _mock_responses(*responses) 71 | @_responses ||= [] 72 | @_responses += responses 73 | end 74 | 75 | def gets 76 | raise "STDIN.gets() called but no mocks to return. Did you set them up with _mock_responses()?" if @_responses.blank? 77 | @_responses.slice!(0).tap do |response| 78 | STDOUT.puts "{ANSWERED WITH: #{response}}" 79 | end 80 | end 81 | end 82 | 83 | RSpec.configure do |config| 84 | # Enable flags like --only-failures and --next-failure 85 | config.example_status_persistence_file_path = ".rspec_status" 86 | 87 | # Disable RSpec exposing methods globally on `Module` and `main` 88 | config.disable_monkey_patching! 89 | 90 | config.expect_with :rspec do |c| 91 | c.syntax = :expect 92 | end 93 | 94 | config.include DatabaseManagement 95 | 96 | config.before :suite do 97 | App.initialize! 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/support/models.rb: -------------------------------------------------------------------------------- 1 | class Business < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :owner, :polymorphic => true 4 | 5 | has_many :business_categories 6 | has_many :categories, :through => :business_categories 7 | 8 | structure do 9 | name "The Kernel's favourite fried chickens", :was => :title 10 | website "http://www.google.co.za/", :was => [:site, :homepage], :validates => :presence 11 | address ["11 Test Drive", "Gardens", "Cape Town" ,"South Africa"].join("\n") 12 | summary :string 13 | description "Founded in 1898", :as => :text 14 | landline :string 15 | mobile :string 16 | operating_days 0..6 17 | date_established :datetime 18 | date_registered Date.today - 10.years 19 | next_sale (Time.now + 10.days) 20 | verified false, :default => false 21 | location :type => :string, :limit => 127 22 | awards ["Best business 2007", "Tastiest Chicken 2008"] 23 | managers :serialized 24 | end 25 | end 26 | 27 | class BusinessCategory < ActiveRecord::Base 28 | belongs_to :business 29 | belongs_to :category 30 | 31 | no_structure 32 | end 33 | 34 | class Category < ActiveRecord::Base 35 | has_many :business_categories 36 | has_many :businesses, :through => :business_categories 37 | 38 | structure do 39 | title :index => true # Default type is a good 'ol varchar(255) 40 | summary 41 | serial_number 1234567891011121314 42 | end 43 | end 44 | 45 | class Chameleon < ActiveRecord::Base 46 | structure do 47 | spots 48 | end 49 | end 50 | 51 | class User < ActiveRecord::Base 52 | structure do 53 | name nil # Testing creating from an unknown class 54 | email "somebody@somewhere.com" 55 | encrypted_password :limit => 48 56 | password_salt :limit => 42 57 | end 58 | end 59 | 60 | class Customer < User 61 | belongs_to :category 62 | 63 | structure do 64 | money_spent "$5.00" 65 | money_gifted "NOK 550.00" 66 | average_rating 5.00, :default => 0.0 67 | end 68 | end 69 | 70 | class NonMigrantModel < ActiveRecord::Base 71 | belongs_to :business 72 | end 73 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/added_incompatible_spot_and_deleted_new_longer_spots.rb: -------------------------------------------------------------------------------- 1 | class ChangedChameleonsAddedIncompatibleSpotAndDeletedNewLongerSpots < ActiveRecord::Migration 2 | def self.up 3 | add_column :chameleons, :incompatible_spot, :integer 4 | remove_column :chameleons, :new_longer_spots 5 | end 6 | 7 | def self.down 8 | add_column :chameleons, :new_longer_spots, :text, :limit=>nil 9 | remove_column :chameleons, :incompatible_spot 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/business_id.rb: -------------------------------------------------------------------------------- 1 | class ChangedUsersAddedBusinessId < ActiveRecord::Migration 2 | def self.up 3 | add_column :users, :business_id, :integer 4 | add_index :users, :business_id 5 | end 6 | 7 | def self.down 8 | remove_column :users, :business_id 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/businesses_indexed_name.rb: -------------------------------------------------------------------------------- 1 | class ChangedBusinessesIndexedName < ActiveRecord::Migration 2 | def self.up 3 | add_index :businesses, :name 4 | end 5 | 6 | def self.down 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/chameleons_added_new_longer_spots_and_moved_new_spots.rb: -------------------------------------------------------------------------------- 1 | class ChangedChameleonsAddedNewLongerSpotsAndMovedNewSpots < ActiveRecord::Migration 2 | def self.up 3 | add_column :chameleons, :new_longer_spots, :text 4 | puts "-- copy data from :new_spots to :new_longer_spots" 5 | Chameleon.update_all("new_longer_spots = new_spots") 6 | remove_column :chameleons, :new_spots 7 | end 8 | 9 | def self.down 10 | add_column :chameleons, :new_spots, :string, :limit=>nil 11 | puts "-- copy data from :new_longer_spots to :new_spots" 12 | Chameleon.update_all("new_spots = new_longer_spots") 13 | remove_column :chameleons, :new_longer_spots 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/create_business_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateBusinessCategories < ActiveRecord::Migration 2 | def self.up 3 | create_table :business_categories do |t| 4 | t.integer :business_id 5 | t.integer :category_id 6 | end 7 | add_index :business_categories, :business_id 8 | add_index :business_categories, :category_id 9 | end 10 | 11 | def self.down 12 | drop_table :business_categories 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/create_businesses.rb: -------------------------------------------------------------------------------- 1 | class CreateBusinesses < ActiveRecord::Migration 2 | def self.up 3 | create_table :businesses do |t| 4 | t.integer :user_id 5 | t.string :owner_type 6 | t.integer :owner_id 7 | t.string :name 8 | t.string :website 9 | t.text :address 10 | t.string :summary 11 | t.text :description 12 | t.string :landline 13 | t.string :mobile 14 | t.integer :operating_days 15 | t.datetime :date_established 16 | t.datetime :next_sale 17 | t.date :date_registered 18 | t.boolean :verified 19 | t.string :location, :limit=>127 20 | t.text :awards 21 | t.text :managers 22 | end 23 | add_index :businesses, :user_id 24 | add_index :businesses, [:owner_type, :owner_id] 25 | add_index :businesses, :owner_id 26 | end 27 | 28 | def self.down 29 | drop_table :businesses 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration 2 | def self.up 3 | create_table :categories do |t| 4 | t.string :title 5 | t.string :summary 6 | end 7 | add_index :categories, :title 8 | end 9 | 10 | def self.down 11 | drop_table :categories 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/create_chameleons.rb: -------------------------------------------------------------------------------- 1 | class CreateChameleons < ActiveRecord::Migration 2 | def self.up 3 | create_table :chameleons do |t| 4 | t.string :spots 5 | end 6 | end 7 | 8 | def self.down 9 | drop_table :chameleons 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/create_reviews.rb: -------------------------------------------------------------------------------- 1 | class CreateReviews < ActiveRecord::Migration 2 | def self.up 3 | create_table :reviews do |t| 4 | t.integer :business_id 5 | t.integer :user_id 6 | t.string :name 7 | t.integer :rating 8 | t.text :body 9 | t.integer :views 10 | end 11 | add_index :reviews, :business_id 12 | add_index :reviews, :user_id 13 | end 14 | 15 | def self.down 16 | drop_table :reviews 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email 6 | t.integer :category_id 7 | t.string :encrypted_password, :limit=>48 8 | t.string :password_salt, :limit=>42 9 | t.decimal :money_spent, :precision=>10, :scale=>2 10 | t.decimal :money_gifted, :precision=>10, :scale=>2 11 | t.float :average_rating 12 | end 13 | 14 | end 15 | 16 | def self.down 17 | drop_table :users 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/created_at.rb: -------------------------------------------------------------------------------- 1 | class ChangedBusinessesAddedCreatedAtUpdatedAt < ActiveRecord::Migration 2 | def self.up 3 | add_column :businesses, :updated_at, :datetime 4 | add_column :businesses, :created_at, :datetime 5 | end 6 | 7 | def self.down 8 | remove_column :businesses, :updated_at 9 | remove_column :businesses, :created_at 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/deleted_incompatible_spot.rb: -------------------------------------------------------------------------------- 1 | class ChangedChameleonsDeletedIncompatibleSpot < ActiveRecord::Migration 2 | def self.up 3 | remove_column :chameleons, :incompatible_spot 4 | end 5 | 6 | def self.down 7 | add_column :chameleons, :incompatible_spot, :integer, :limit=>nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/deleted_spots.rb: -------------------------------------------------------------------------------- 1 | class ChangedChameleonsDeletedSpots < ActiveRecord::Migration 2 | def self.up 3 | remove_column :chameleons, :spots 4 | end 5 | 6 | def self.down 7 | add_column :chameleons, :spots, :string, :limit=>nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/estimated_value_notes.rb: -------------------------------------------------------------------------------- 1 | class ChangedBusinessesAddedEstimatedValueNotes < ActiveRecord::Migration 2 | def self.up 3 | add_column :businesses, :estimated_value, :float 4 | add_column :businesses, :notes, :string 5 | end 6 | 7 | def self.down 8 | remove_column :businesses, :estimated_value 9 | remove_column :businesses, :notes 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/landline.rb: -------------------------------------------------------------------------------- 1 | class ChangedBusinessesModifiedLandline < ActiveRecord::Migration 2 | def self.up 3 | change_column :businesses, :landline, :text 4 | end 5 | 6 | def self.down 7 | change_column :businesses, :landline, :string, :limit=>nil, :default=>nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/modified_verified.rb: -------------------------------------------------------------------------------- 1 | class ChangedBusinessesModifiedVerifiedDefaultedToTrue < ActiveRecord::Migration 2 | def self.up 3 | change_column :businesses, :verified, :boolean, :default=>true 4 | end 5 | 6 | def self.down 7 | change_column :businesses, :verified, :boolean, :limit=>nil, :default=>"f" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/verified_migrations/renamed_old_spots.rb: -------------------------------------------------------------------------------- 1 | class ChangedChameleonsRenamedOldSpots < ActiveRecord::Migration 2 | def self.up 3 | rename_column :chameleons, :old_spots, :new_spots 4 | end 5 | 6 | def self.down 7 | rename_column :chameleons, :new_spots, :old_spots 8 | end 9 | end 10 | --------------------------------------------------------------------------------