├── lib ├── mini_record │ ├── version.rb │ ├── configuration.rb │ └── auto_schema.rb └── mini_record.rb ├── .gitignore ├── .travis.yml ├── Gemfile ├── Rakefile ├── mini_record.gemspec ├── test ├── models.rb ├── helper.rb └── test_mini_record.rb └── README.md /lib/mini_record/version.rb: -------------------------------------------------------------------------------- 1 | module MiniRecord 2 | VERSION = "0.4.7" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .rvmrc 6 | *.swp 7 | *.swo 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem update --system 4 | - gem update bundler 5 | rvm: 6 | - 1.9.3 7 | - 2.1.0 8 | - 2.1.2 9 | notifications: 10 | recipients: 11 | - info@daddye.it 12 | -------------------------------------------------------------------------------- /lib/mini_record.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' unless defined?(Gem) 2 | require 'active_record' 3 | require 'mini_record/configuration' 4 | require 'mini_record/auto_schema' 5 | 6 | MiniRecord.configure do |config| 7 | end 8 | ActiveRecord::Base.send(:include, MiniRecord::AutoSchema) 9 | -------------------------------------------------------------------------------- /lib/mini_record/configuration.rb: -------------------------------------------------------------------------------- 1 | module MiniRecord 2 | class << self 3 | attr_accessor :configuration 4 | end 5 | 6 | def self.configure 7 | self.configuration ||= Configuration.new 8 | yield(configuration) 9 | end 10 | 11 | def self.reset_configuration! 12 | self.configuration = Configuration.new 13 | end 14 | 15 | class Configuration 16 | attr_accessor :destructive 17 | 18 | def initialize 19 | @destructive = true 20 | end 21 | end 22 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in mini_record.gemspec 4 | gem 'rake' 5 | gem 'minitest' 6 | 7 | # Test database adapters with "rake test DB=mysql2" 8 | gem 'sqlite3' 9 | gem 'pg' 10 | gem 'mysql' 11 | gem 'mysql2' 12 | 13 | # Uncomment to test older versions, then "bundle update" 14 | # gem 'activerecord', '<= 3.2' 15 | # gem 'activerecord', '~> 4.0.0' 16 | # gem 'activerecord', '~> 4.1.0' 17 | gem 'activerecord', '>= 4.2.0' 18 | 19 | group :test do 20 | gem 'foreigner', '>= 1.4.2' 21 | end 22 | 23 | gemspec 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/gem_tasks' 3 | require 'rake' 4 | require 'rake/testtask' 5 | 6 | %w(install release).each do |task| 7 | Rake::Task[task].enhance do 8 | sh "rm -rf pkg" 9 | end 10 | end 11 | 12 | desc "Bump version on github" 13 | task :bump do 14 | if `git status -s`.strip == "" 15 | puts "\e[31mNothing to commit (working directory clean)\e[0m" 16 | else 17 | version = Bundler.load_gemspec(Dir[File.expand_path('../*.gemspec', __FILE__)].first).version 18 | sh "git add .; git commit -a -m \"Bump to version #{version}\"" 19 | end 20 | end 21 | 22 | task :release => :bump 23 | Rake::TestTask.new(:test) do |test| 24 | test.libs << 'test' 25 | test.test_files = Dir['test/**/test_*.rb'] 26 | test.verbose = true 27 | end 28 | 29 | task :default => :test 30 | task :spec => :test 31 | -------------------------------------------------------------------------------- /mini_record.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "mini_record/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "mini_record" 7 | s.version = MiniRecord::VERSION 8 | s.authors = ["Davide D'Agostino"] 9 | s.email = ["info@daddye.it"] 10 | s.homepage = "https://github.com/DAddYE/mini_record" 11 | s.summary = %q{MiniRecord is a micro gem that allow you to write schema inside your model as you can do in DataMapper.} 12 | s.description = %q{ 13 | With it you can add the ability to create columns outside the default schema, directly 14 | in your model in a similar way that you just know in others projects 15 | like DataMapper or MongoMapper. 16 | }.gsub(/^ {4}/, '') 17 | 18 | s.rubyforge_project = "mini_record" 19 | 20 | s.files = `git ls-files`.split("\n") 21 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 22 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 23 | s.require_paths = ["lib"] 24 | 25 | # specify any dependencies here; for example: 26 | # s.add_development_dependency "rspec" 27 | s.add_dependency "activerecord", ">=3.2.0" 28 | end 29 | -------------------------------------------------------------------------------- /test/models.rb: -------------------------------------------------------------------------------- 1 | class Person < ActiveRecord::Base 2 | schema do |s| 3 | s.string :name 4 | end 5 | timestamps 6 | end 7 | 8 | class Post < ActiveRecord::Base 9 | key :title 10 | key :body 11 | key :category, :as => :references 12 | belongs_to :category 13 | end 14 | 15 | class Category < ActiveRecord::Base 16 | key :title 17 | has_many :articles 18 | has_many :posts 19 | has_many :items 20 | end 21 | 22 | class Animal < ActiveRecord::Base 23 | key :name, :index => true 24 | index :id 25 | end 26 | 27 | class Pet < ActiveRecord::Base 28 | key :name, :index => true 29 | end 30 | 31 | class Tool < ActiveRecord::Base 32 | has_and_belongs_to_many :purposes 33 | end 34 | 35 | class Purpose < ActiveRecord::Base 36 | has_and_belongs_to_many :tools 37 | end 38 | 39 | class Publisher < ActiveRecord::Base 40 | has_many :articles 41 | col :name 42 | end 43 | 44 | class Article < ActiveRecord::Base 45 | key :title 46 | belongs_to :publisher 47 | end 48 | 49 | class Attachment < ActiveRecord::Base 50 | key :name 51 | belongs_to :attachable, :polymorphic => true 52 | end 53 | 54 | class Account < ActiveRecord::Base 55 | key :name 56 | end 57 | 58 | class Task < ActiveRecord::Base 59 | belongs_to :author, :class_name => 'Account' 60 | end 61 | 62 | class Activity < ActiveRecord::Base 63 | belongs_to :author, :class_name => 'Account', :foreign_key => 'custom_id' 64 | end 65 | 66 | class Page < ActiveRecord::Base 67 | key :title 68 | has_and_belongs_to_many :photogalleries 69 | end 70 | 71 | class Photogallery < ActiveRecord::Base 72 | key :title 73 | has_and_belongs_to_many :pages 74 | end 75 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' unless defined?(Gem) 2 | require 'bundler/setup' 3 | require 'logger' 4 | require 'mini_record' 5 | require 'minitest/autorun' 6 | 7 | class ActiveRecord::Base 8 | class << self 9 | attr_accessor :logs 10 | 11 | def db_fields 12 | connection.columns(table_name).inject({}) do |hash, column| 13 | hash[column.name.to_sym] = column 14 | hash 15 | end 16 | end 17 | 18 | def db_columns 19 | connection.columns(table_name).map(&:name).sort 20 | end 21 | 22 | def db_indexes 23 | connection.indexes(table_name).map(&:name).sort 24 | end 25 | 26 | def schema_columns 27 | table_definition.columns.map { |c| c.name.to_s }.sort 28 | end 29 | 30 | def schema_fields 31 | table_definition.columns.inject({}) do |hash, column| 32 | hash[column.name.to_sym] = column 33 | hash 34 | end 35 | end 36 | 37 | def queries(pragma=false) 38 | ActiveRecord::Base.logs.string.gsub(/\e\[[\d;]+m/, '').lines.reject { |l| !pragma && l =~ /pragma/i }.join("\n") 39 | end 40 | 41 | def auto_upgrade!(*args) 42 | ActiveRecord::Base.logs = StringIO.new 43 | ActiveRecord::Base.logger = Logger.new(ActiveRecord::Base.logs) 44 | silence_stream(STDERR) { super } 45 | end 46 | 47 | def auto_upgrade_dry 48 | ActiveRecord::Base.logs = StringIO.new 49 | ActiveRecord::Base.logger = Logger.new(ActiveRecord::Base.logs) 50 | silence_stream(STDERR) { super } 51 | end 52 | end 53 | end # ActiveRecord::Base 54 | 55 | # Setup Adapter 56 | puts "Testing with DB=#{ENV['DB'] || 'sqlite'}" 57 | case ENV['DB'] 58 | when 'mysql' 59 | ActiveRecord::Base.establish_connection(:adapter => 'mysql', :database => 'test', :username => 'root') 60 | when 'mysql2' 61 | ActiveRecord::Base.establish_connection(:adapter => 'mysql2', :database => 'test', :username => 'root') 62 | Bundler.require(:test) # require 'foreigner' 63 | Foreigner.load 64 | when 'pg', 'postgresql' 65 | ActiveRecord::Base.establish_connection(:adapter => 'postgresql', :database => 'test', :user => 'postgres', :host => 'localhost') 66 | else 67 | ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:') 68 | end 69 | 70 | 71 | # Some helpers to minitest 72 | class MiniTest::Spec 73 | def connection 74 | ActiveRecord::Base.connection 75 | end 76 | alias :conn :connection 77 | end 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/DAddYE/mini_record.png)](http://travis-ci.org/DAddYE/mini_record) 2 | 3 | 4 | MiniRecord is a **micro** extension for the `ActiveRecord` gem. 5 | 6 | MiniRecord will allow you to create/edit/manage columns, directly in your **model**. 7 | 8 | 9 | ## Features 10 | 11 | * Define columns/properties inside your model 12 | * Perform migrations automatically 13 | * Auto upgrade your schema 14 | * Add, Remove, Change **columns** 15 | * Add, Remove, Change **indexes** 16 | 17 | ## Instructions 18 | 19 | What you need is to move/remove your `db/schema.rb`. 20 | This avoid conflicts. 21 | 22 | Add to your `Gemfile`: 23 | 24 | ```sh 25 | gem 'mini_record' 26 | ``` 27 | 28 | To optionally block any destructive actions on the database, create a file `config/initializers/mini_record.rb` and add: 29 | 30 | ```ruby 31 | MiniRecord.configure do |config| 32 | config.destructive = false 33 | end 34 | ``` 35 | 36 | That's all! 37 | 38 | ## Examples 39 | 40 | Remember that inside properties you can use all migrations methods, 41 | see [documentation](http://api.rubyonrails.org/classes/ActiveRecord/Migration.html) 42 | 43 | ```ruby 44 | class Post < ActiveRecord::Base 45 | field :title_en, :title_jp 46 | field :description_en, :description_jp, as: :text 47 | field :permalink, index: true, limit: 50 48 | field :comments_count, as: :integer 49 | field :category, as: :references, index: true 50 | end 51 | Post.auto_upgrade! 52 | ``` 53 | 54 | Instead of `field` you can pick an alias: `key, field, property, col` 55 | 56 | If the option `:as` is omitted, minirecord will assume it's a `:string`. 57 | 58 | Remember that as for `ActiveRecord` you can choose different types: 59 | 60 | ```ruby 61 | :primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, 62 | :date, :binary, :boolean, :references, :belongs_to, :timestamp 63 | ``` 64 | 65 | You can also provide other options like: 66 | 67 | ```ruby 68 | :limit, :default, :null, :precision, :scale 69 | 70 | # example 71 | class Foo < ActiveRecord::Base 72 | field :title, default: "MyTitle" # as: :string is not necessary since is a default 73 | field :price, as: :decimal, scale: 8, precision: 2 74 | end 75 | ``` 76 | 77 | See [ActiveRecord::TableDefinition](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html) 78 | for more details. 79 | 80 | ### Perform upgrades 81 | 82 | Finally, when you execute `MyModel.auto_upgrade!`, missing columns, indexes and tables will be created on the fly. 83 | 84 | Indexes and columns present in the db but **not** in your model schema/definition will be **deleted** also from your db. 85 | 86 | ### Single Table Inheritance 87 | 88 | MiniRecord as ActiveRecord support STI: 89 | 90 | ```ruby 91 | class Pet < ActiveRecord::Base; end 92 | class Dog < Pet; end 93 | class Cat < Pet; end 94 | ActiveRecord::Base.auto_upgrade! 95 | ``` 96 | 97 | When you perform `ActiveRecord::Base.auto_upgrade!`, just **1** table will be created with the `type` column (indexed as well). 98 | 99 | ### ActiveRecord Relations 100 | 101 | MiniRecord has built-in support of `belongs_to`, _polymorphic associations_ as well with _habtm_ relations. 102 | 103 | You don't need to do anything in particular, is not even necessary define the field for them since they will be handled automatically. 104 | 105 | #### belongs_to 106 | ```ruby 107 | class Address < ActiveRecord::Base 108 | belongs_to :person 109 | end 110 | ``` 111 | Will result in a indexed `person_id` column. You can use a different one using the `foreign_key` option: 112 | 113 | ```ruby 114 | belongs_to :person, foreign_key: :person_pk 115 | ``` 116 | 117 | #### belongs_to with foreign key in database 118 | 119 | ```ruby 120 | class Address < ActiveRecord::Base 121 | belongs_to :person 122 | index :person_id, foreign: true 123 | end 124 | ``` 125 | 126 | This is the same example, but foreign key will be added to the database with help of 127 | [foreigner](https://github.com/matthuhiggins/foreigner) gem. 128 | 129 | In this case you have more control (if needed). 130 | 131 | To remove the key please use `:foreign => false` 132 | If you simple remove the index, the foreign key will not be removed. 133 | 134 | #### belongs_to (polymorphic) 135 | 136 | ```ruby 137 | class Address < ActiveRecord::Base 138 | belongs_to :addressable, polymorphic: true 139 | end 140 | ``` 141 | 142 | Will create an `addressable_id` and an `addressable_type` column with composite indexes: 143 | 144 | ```ruby 145 | add_index(:addresses), [:addressable_id, :addressable_type] 146 | ``` 147 | 148 | #### habtm 149 | ```ruby 150 | class Address < ActiveRecord::Base 151 | has_and_belongs_to_many :people 152 | end 153 | ``` 154 | 155 | Will generate a "addresses_people" (aka: join table) with indexes on the id columns 156 | 157 | ### Adding a new column 158 | 159 | Super easy, open your model and just add it: 160 | 161 | ```ruby 162 | class Post < ActiveRecord::Base 163 | field :title 164 | field :body, as: :text # <<- this 165 | field :permalink, index: true 166 | field :comments_count, as: :integer 167 | field :category, as: :references, index: true 168 | end 169 | Post.auto_upgrade! 170 | ``` 171 | 172 | So now when you invoke `MyModel.auto_upgrade!` a diff between the old schema an the new one will detect changes and create the new column. 173 | 174 | ### Removing a column 175 | 176 | It's exactly the same as in the previous example. 177 | 178 | ### Rename columns 179 | 180 | Simply adding a `rename_field` declaration and mini_record will do a `connection.rename_column` in the next `auto_upgrade!` but **only** if the db has the old column and not the new column. 181 | 182 | This means that you still need to have a `field` declaration for the new column name so subsequent `MyModel.auto_upgrade!` will not remove the column. 183 | 184 | You are free to leave the `rename_field` declaration in place or you can remove it once the new column exists in the db. 185 | 186 | Moving from: 187 | ```ruby 188 | class Vehicle < ActiveRecord::Base 189 | field :color 190 | end 191 | ``` 192 | 193 | To: 194 | ```ruby 195 | class Vehicle < ActiveRecord::Base 196 | rename_field :color, new_name: :body_color 197 | field :body_color 198 | end 199 | ``` 200 | 201 | Then perhaps later: 202 | ```ruby 203 | class Vehicle < ActiveRecord::Base 204 | rename_field :color, new_name: :body_color 205 | rename_field :body_color, new_name: :chassis_color 206 | field :chassis_color 207 | end 208 | ``` 209 | 210 | ### Change the type of columns 211 | 212 | Where when you rename a column the task should be _explicit_ changing the type is _implicit_. 213 | 214 | This means that if you have 215 | 216 | ```ruby 217 | field :total, as: :integer 218 | ``` 219 | 220 | and later on you'll figure out that you wanted a `float` 221 | 222 | ```ruby 223 | field :total, as: :float 224 | ``` 225 | 226 | Will automatically change the type the the first time you'll invoke `auto_upgrade`. 227 | 228 | 229 | ### Add/Remove indexes 230 | 231 | In the same way we manage columns MiniRecord will detect new indexes and indexes that needs to be removed. 232 | 233 | So when you perform `MyModel.auto_upgrade!` a SQL command like: 234 | 235 | ```SQL 236 | PRAGMA index_info('index_people_on_name') 237 | CREATE INDEX "index_people_on_surname" ON "people" ("surname") 238 | ``` 239 | 240 | A quick hint, sometimes index gets too verbose/long: 241 | 242 | ```ruby 243 | class Fox < ActiveRecord::Base 244 | field :foo, index: true 245 | field :foo, index: :custom_name 246 | field :foo, index: [:foo, :bar] 247 | field :foo, index: { column: [:branch_id, :party_id], unique: true, name: 'by_branch_party' } 248 | end 249 | ``` 250 | 251 | Here is where `add_index` comes handy, so you can rewrite the above in: 252 | 253 | ```ruby 254 | class Fox < ActiveRecord::Base 255 | field :foo 256 | add_index :foo 257 | add_index :custom_name 258 | add_index [:foo, :bar] 259 | add_index [:branch_id, :party_id], unique: true, name: 'by_branch_party' 260 | end 261 | ``` 262 | 263 | ### Suppress default indexes for associations 264 | 265 | If you do not need the default index for a `belongs_to` or `has_and_belongs_to_many` relationship, such as if you are using a composite index instead, you can suppress it from being created (or remove it) using `suppress_index` on the association: 266 | 267 | ```ruby 268 | class PhoneNumber < ActiveRecord::Base 269 | field :position 270 | belongs_to :person 271 | suppress_index :person 272 | add_index [:person_id, :position] 273 | end 274 | ``` 275 | 276 | ### Passing options to Create Table 277 | 278 | If you need to pass particular options to your `CREATE TABLE` statement, you can do so with `create_table` in the Model: 279 | 280 | ```ruby 281 | class Fox < ActiveRecord::Base 282 | create_table :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci' 283 | field :foo 284 | end 285 | ``` 286 | 287 | ## Contributors 288 | 289 | A special thanks to all who have contributed in this project: 290 | 291 | * Dmitriy Partsyrniy 292 | * Steven Garcia 293 | * Carlo Bertini 294 | * Nate Wiger 295 | * Dan Watson 296 | * Guy Boertje 297 | * virtax 298 | * Nagy Bence 299 | * Takeshi Yabe 300 | * blahutka 301 | * 4r2r 302 | 303 | ## Author 304 | 305 | DAddYE, you can follow me on twitter [@daddye](http://twitter.com/daddye) or take a look at my site [daddye.it](http://www.daddye.it) 306 | 307 | ## Copyright 308 | 309 | Copyright (C) 2011-2014 Davide D'Agostino - [@daddye](http://twitter.com/daddye) 310 | 311 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 312 | associated documentation files (the “Software”), to deal in the Software without restriction, including without 313 | limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 314 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 315 | 316 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 317 | 318 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 319 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, 320 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 321 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 322 | -------------------------------------------------------------------------------- /lib/mini_record/auto_schema.rb: -------------------------------------------------------------------------------- 1 | module MiniRecord 2 | module AutoSchema 3 | def self.included(base) 4 | base.extend(ClassMethods) 5 | end 6 | 7 | module ClassMethods 8 | def init_table_definition(connection) 9 | #connection.create_table(table_name) unless connection.table_exists?(table_name) 10 | 11 | case ActiveRecord::ConnectionAdapters::TableDefinition.instance_method(:initialize).arity 12 | when 1 13 | # Rails 3.2 and earlier 14 | ActiveRecord::ConnectionAdapters::TableDefinition.new(connection) 15 | when 4 16 | # Rails 4 17 | ActiveRecord::ConnectionAdapters::TableDefinition.new(connection.native_database_types, table_name, false, {}) 18 | when -5 19 | # Rails 4.1 20 | ActiveRecord::ConnectionAdapters::TableDefinition.new(connection.native_database_types, table_name, false, {}, nil) 21 | else 22 | raise ArgumentError, 23 | "Unsupported number of args for ActiveRecord::ConnectionAdapters::TableDefinition.new()" 24 | end 25 | end 26 | 27 | def schema_tables 28 | @@_schema_tables ||= [] 29 | end 30 | 31 | def table_definition 32 | return superclass.table_definition unless (superclass == ActiveRecord::Base) || (superclass.respond_to?(:abstract_class?) && superclass.abstract_class?) 33 | 34 | @_table_definition ||= begin 35 | tb = init_table_definition(connection) 36 | tb.primary_key(primary_key) 37 | tb 38 | end 39 | end 40 | 41 | def indexes 42 | return superclass.indexes unless (superclass == ActiveRecord::Base) || (superclass.respond_to?(:abstract_class?) && superclass.abstract_class?) 43 | 44 | @_indexes ||= {} 45 | end 46 | 47 | def suppressed_indexes 48 | return superclass.suppressed_indexes unless (superclass == ActiveRecord::Base) || (superclass.respond_to?(:abstract_class?) && superclass.abstract_class?) 49 | 50 | @_suppressed_indexes ||= {} 51 | end 52 | 53 | def indexes_in_db 54 | connection.indexes(table_name).inject({}) do |hash, index| 55 | hash[index.name] = index 56 | hash 57 | end 58 | end 59 | 60 | def get_sql_field_type(field) 61 | if ActiveRecord::VERSION::MAJOR.to_i < 4 62 | field.sql_type.to_s.downcase 63 | else 64 | connection.type_to_sql(field.type.to_sym, field.limit, field.precision, field.scale) 65 | end 66 | end 67 | 68 | def create_table_options 69 | @create_table_options ||= [] 70 | end 71 | 72 | def rename_fields 73 | @rename_fields ||= {} 74 | end 75 | 76 | def fields 77 | table_definition.columns.inject({}) do |hash, column| 78 | hash[column.name] = column 79 | hash 80 | end 81 | end 82 | 83 | def fields_in_db 84 | connection.columns(table_name).inject({}) do |hash, column| 85 | hash[column.name] = column 86 | hash 87 | end 88 | end 89 | 90 | def rename_field(*args) 91 | return unless connection? 92 | 93 | options = args.extract_options! 94 | new_name = options.delete(:new_name) 95 | old_name = args.first 96 | if old_name && new_name 97 | rename_fields[old_name] = new_name 98 | end 99 | end 100 | alias :rename_key :rename_field 101 | alias :rename_property :rename_field 102 | alias :rename_col :rename_field 103 | 104 | def field(*args) 105 | return unless connection? 106 | 107 | options = args.extract_options! 108 | type = options.delete(:as) || options.delete(:type) || :string 109 | index = options.delete(:index) 110 | 111 | args.each do |column_name| 112 | 113 | # Allow custom types like: 114 | # t.column :type, "ENUM('EMPLOYEE','CLIENT','SUPERUSER','DEVELOPER')" 115 | if type.is_a?(String) 116 | # will be converted in: t.column :type, "ENUM('EMPLOYEE','CLIENT')" 117 | options.reverse_merge!(:limit => 0) unless postgresql_limitless_column?(type) 118 | table_definition.column(column_name, type, options) 119 | else 120 | # wil be converted in: t.string :name 121 | table_definition.send(type, column_name, options) 122 | end 123 | 124 | # Get the correct column_name i.e. in field :category, :as => :references 125 | column_name = table_definition.columns[-1].name 126 | 127 | # Parse indexes 128 | case index 129 | when Hash 130 | add_index(options.delete(:column) || column_name, index) 131 | when TrueClass 132 | add_index(column_name) 133 | when String, Symbol, Array 134 | add_index(index) 135 | end 136 | end 137 | end 138 | alias :key :field 139 | alias :property :field 140 | alias :col :field 141 | 142 | def postgresql_limitless_column? type 143 | return unless connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) 144 | type =~ /range|json/i 145 | end 146 | 147 | def timestamps 148 | field :created_at, :updated_at, :as => :datetime 149 | end 150 | 151 | def reset_table_definition! 152 | @_table_definition = nil 153 | end 154 | alias :reset_schema! :reset_table_definition! 155 | 156 | def schema 157 | reset_table_definition! 158 | yield table_definition 159 | table_definition 160 | end 161 | 162 | def create_table(*options) 163 | @create_table_options = options 164 | end 165 | 166 | def add_index(column_name, options={}) 167 | index_name = connection.index_name(table_name, :column => column_name) 168 | indexes[index_name] = options.merge(:column => column_name) unless indexes.key?(index_name) 169 | index_name 170 | end 171 | alias :index :add_index 172 | 173 | def suppress_index(*associations) 174 | associations.each do |association| 175 | suppressed_indexes[association] = true 176 | end 177 | end 178 | 179 | def connection? 180 | !!connection 181 | rescue Exception => e 182 | puts "\e[31m%s\e[0m" % e.message.strip 183 | false 184 | end 185 | 186 | def clear_tables!(dry_run = false) 187 | return unless MiniRecord.configuration.destructive == true 188 | (connection.tables - schema_tables).each do |name| 189 | logger.debug "[MiniRecord] Dropping table #{name}" if logger 190 | unless dry_run 191 | connection.drop_table(name) 192 | schema_tables.delete(name) 193 | end 194 | end 195 | end 196 | 197 | def foreign_keys 198 | # fk cache to minimize quantity of sql queries 199 | @foreign_keys ||= {} 200 | @foreign_keys[:table_name] ||= connection.foreign_keys(table_name) 201 | end 202 | 203 | # Remove foreign keys for indexes with :foreign=>false option 204 | def remove_foreign_keys(dry_run) 205 | return unless MiniRecord.configuration.destructive == true 206 | indexes.each do |name, options| 207 | if options[:foreign]==false 208 | foreign_key = foreign_keys.detect { |fk| fk.options[:column] == options[:column].to_s } 209 | if foreign_key 210 | logger.debug "[MiniRecord] Removing Foreign Key #{foreign_key.options[:name]} on table #{table_name}" if logger 211 | connection.remove_foreign_key(table_name, :name => foreign_key.options[:name]) unless dry_run 212 | foreign_keys.delete(foreign_key) 213 | end 214 | end 215 | end 216 | end 217 | 218 | # Add foreign keys for indexes with :foreign=>true option, if the key doesn't exists 219 | def add_foreign_keys(dry_run) 220 | indexes.each do |name, options| 221 | if options[:foreign] 222 | column = options[:column].to_s 223 | unless foreign_keys.detect { |fk| fk[:options][:column] == column } 224 | to_table = reflect_on_all_associations.detect { |a| a.foreign_key.to_s==column }.table_name 225 | logger.debug "[MiniRecord] Adding Foreign Key on #{table_name} to #{to_table}" if logger 226 | connection.add_foreign_key(table_name, to_table, options) unless dry_run 227 | foreign_keys << { :options=> { :column=>column } } 228 | end 229 | end 230 | end 231 | end 232 | 233 | # Helper to determine if/how a field will change 234 | def field_attr_changes(field_name) 235 | field = field_name.to_s 236 | changed = false # flag 237 | new_attr = {} 238 | 239 | # Next, iterate through our extended attributes, looking for any differences 240 | # This catches stuff like :null, :precision, etc 241 | # Ignore junk attributes that different versions of Rails include 242 | [:name, :limit, :precision, :scale, :default, :null].each do |att| 243 | value = fields[field][att] 244 | value = true if att == :null && value.nil? 245 | 246 | # Skip unspecified limit/precision/scale as DB will set them to defaults, 247 | # and on subsequent runs, this will be erroneously detected as a change. 248 | next if value.nil? and [:limit, :precision, :scale].include?(att) 249 | 250 | old_value = fields_in_db[field].send(att) 251 | # puts "#{field_name}[#{att}] = #{value.inspect} vs #{old_value.inspect}" 252 | 253 | attr_changed = false 254 | if att == :default 255 | # Rails 4.2 changed behavior to pass DB values directly through, so we must re-map 256 | if value.to_s =~ /^(false|f|0)$/i 257 | attr_changed = true if old_value.to_s !~ /^(false|f|0)$/i 258 | elsif value.to_s =~ /^(true|t|1)$/i 259 | attr_changed = true if old_value.to_s !~ /^(true|t|1)$/i 260 | elsif value.to_s != old_value.to_s 261 | attr_changed = true 262 | end 263 | elsif value != old_value 264 | attr_changed = true 265 | end 266 | 267 | if attr_changed 268 | logger.debug "[MiniRecord] Detected schema change for #{table_name}.#{field}##{att} " + 269 | "from #{old_value.inspect} to #{value.inspect}" if logger 270 | new_attr[att] = value 271 | changed ||= attr_changed 272 | end 273 | end 274 | 275 | [new_attr, changed] 276 | end 277 | 278 | # dry-run 279 | def auto_upgrade_dry 280 | auto_upgrade!(true) 281 | end 282 | 283 | def auto_upgrade!(dry_run = false) 284 | return unless connection? 285 | return if respond_to?(:abstract_class?) && abstract_class? 286 | 287 | if self == ActiveRecord::Base 288 | descendants.each { |model| model.auto_upgrade!(dry_run) } 289 | clear_tables!(dry_run) 290 | else 291 | # If table doesn't exist, create it 292 | unless connection.tables.include?(table_name) 293 | class << connection; attr_accessor :table_definition; end unless connection.respond_to?(:table_definition=) 294 | logger.debug "[MiniRecord] Creating Table #{table_name}" if logger 295 | unless dry_run 296 | connection.table_definition = table_definition 297 | connection.create_table(table_name, *create_table_options) 298 | connection.table_definition = init_table_definition(connection) 299 | end 300 | end 301 | 302 | # Add this to our schema tables 303 | schema_tables << table_name unless schema_tables.include?(table_name) 304 | 305 | # Generate fields from associations 306 | if reflect_on_all_associations.any? 307 | reflect_on_all_associations.each do |association| 308 | foreign_key = association.options[:foreign_key] || "#{association.name}_id" 309 | type_key = "#{association.name.to_s}_type" 310 | case association.macro 311 | when :belongs_to 312 | field foreign_key, :as => :integer unless fields.key?(foreign_key.to_s) 313 | if association.options[:polymorphic] 314 | field type_key, :as => :string unless fields.key?(type_key.to_s) 315 | index [foreign_key, type_key] unless suppressed_indexes[association.name] 316 | else 317 | index foreign_key unless suppressed_indexes[association.name] 318 | end 319 | when :has_and_belongs_to_many 320 | table = if name = association.options[:join_table] 321 | name.to_s 322 | else 323 | association_table_name = association.name.to_s.classify.constantize.table_name 324 | table_name_substrings = [table_name,association_table_name].collect { |string| string.split('_') } 325 | common_substrings = Array.new 326 | table_name_substrings.first.each_index { |i| table_name_substrings.first[i] == table_name_substrings.last[i] ? common_substrings.push(table_name_substrings.first[i]) : break } 327 | common_prefix = common_substrings.join('_') 328 | table_names = [table_name.clone,association_table_name.clone].sort 329 | table_names.last.gsub!(/^#{common_prefix}_/,'') 330 | table_names.join("_") 331 | end 332 | unless connection.tables.include?(table.to_s) 333 | foreign_key = association.options[:foreign_key] || association.foreign_key 334 | association_foreign_key = association.options[:association_foreign_key] || association.association_foreign_key 335 | logger.debug "[MiniRecord] Creating Join Table #{table} with keys #{foreign_key} and #{association_foreign_key}" if logger 336 | unless dry_run 337 | connection.create_table(table, :id => false) do |t| 338 | t.integer foreign_key 339 | t.integer association_foreign_key 340 | end 341 | end 342 | index_name = connection.index_name(table, :column => [foreign_key, association_foreign_key]) 343 | index_name = index_name[0...connection.index_name_length] if index_name.length > connection.index_name_length 344 | logger.debug "[MiniRecord] Creating Join Table Index #{index_name} (#{foreign_key}, #{association_foreign_key}) on #{table}" if logger 345 | connection.add_index table, [foreign_key, association_foreign_key], :name => index_name, :unique => true unless dry_run or suppressed_indexes[association.name] 346 | end 347 | # Add join table to our schema tables 348 | schema_tables << table unless schema_tables.include?(table) 349 | end 350 | end 351 | end 352 | 353 | # Add to schema inheritance column if necessary 354 | if descendants.present? 355 | field inheritance_column, :as => :string unless fields.key?(inheritance_column.to_s) 356 | index inheritance_column 357 | end 358 | 359 | # Group Destructive Actions 360 | if MiniRecord.configuration.destructive == true and connection.tables.include?(table_name) 361 | 362 | # Rename fields 363 | rename_fields.each do |old_name, new_name| 364 | old_column = fields_in_db[old_name.to_s] 365 | new_column = fields_in_db[new_name.to_s] 366 | if old_column && !new_column 367 | logger.debug "[MiniRecord] Renaming column #{table_name}.#{old_column.name} to #{new_name}" if logger 368 | connection.rename_column(table_name, old_column.name, new_name) unless dry_run 369 | end 370 | end 371 | 372 | # Remove fields from db no longer in schema 373 | columns_to_delete = fields_in_db.keys - fields.keys & fields_in_db.keys 374 | columns_to_delete.each do |field| 375 | column = fields_in_db[field] 376 | logger.debug "[MiniRecord] Removing column #{table_name}.#{column.name}" if logger 377 | connection.remove_column table_name, column.name unless dry_run 378 | end 379 | 380 | # Change attributes of existent columns 381 | (fields.keys & fields_in_db.keys).each do |field| 382 | if field != primary_key #ActiveRecord::Base.get_primary_key(table_name) 383 | new_attr, changed = field_attr_changes(field) 384 | 385 | # Change the column if applicable 386 | new_type = fields[field].type.to_sym 387 | if changed 388 | logger.debug "[MiniRecord] Changing column #{table_name}.#{field} to new type #{new_type}" if logger 389 | connection.change_column table_name, field, new_type, new_attr unless dry_run 390 | end 391 | end 392 | end 393 | 394 | remove_foreign_keys(dry_run) if connection.respond_to?(:foreign_keys) 395 | 396 | # Remove old index 397 | index_names = indexes.collect{ |name, opts| (opts[:name] || name).to_s } 398 | (indexes_in_db.keys - index_names).each do |name| 399 | logger.debug "[MiniRecord] Removing index #{name} on #{table_name}" if logger 400 | connection.remove_index(table_name, :name => name) unless dry_run 401 | end 402 | 403 | end 404 | 405 | if connection.tables.include?(table_name) 406 | # Add fields to db new to schema 407 | columns_to_add = fields.keys - fields_in_db.keys 408 | columns_to_add.each do |field| 409 | column = fields[field] 410 | options = {:limit => column.limit, :precision => column.precision, :scale => column.scale} 411 | options[:default] = column.default unless column.default.nil? 412 | options[:null] = column.null unless column.null.nil? 413 | logger.debug "[MiniRecord] Adding column #{table_name}.#{column.name}" if logger 414 | connection.add_column table_name, column.name, column.type.to_sym, options unless dry_run 415 | end 416 | end 417 | 418 | # Add indexes 419 | indexes.each do |name, options| 420 | options = options.dup 421 | options.delete(:foreign) 422 | adjusted_index_name = "index_#{table_name}_on_" + (options[:column].is_a?(Array) ? options[:column].join('_and_') : options[:column]).to_s 423 | index_name = (options[:name] || adjusted_index_name).to_s 424 | unless connection.indexes(table_name).detect { |i| i.name == index_name } 425 | logger.debug "[MiniRecord] Adding index #{index_name} #{options[:column].inspect} on #{table_name}" if logger 426 | connection.add_index(table_name, options.delete(:column), options) unless dry_run 427 | end 428 | end 429 | 430 | add_foreign_keys(dry_run) if connection.respond_to?(:foreign_keys) 431 | 432 | # Reload column information 433 | reset_column_information 434 | end 435 | end 436 | end # ClassMethods 437 | end # AutoSchema 438 | end # MiniRecord 439 | -------------------------------------------------------------------------------- /test/test_mini_record.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', __FILE__) 2 | 3 | describe MiniRecord do 4 | 5 | def clear_active_record!(options = {}) 6 | unless options[:keep_tables] 7 | ActiveRecord::Base.clear_reloadable_connections! 8 | ActiveRecord::Base.clear_cache! 9 | ActiveRecord::Base.clear_active_connections! 10 | conn.tables.each { |table| silence_stream(STDERR) { conn.execute "DROP TABLE IF EXISTS #{table}" } } 11 | end 12 | 13 | ActiveRecord::Base.descendants.each { |klass| Object.send(:remove_const, klass.to_s) if Object.const_defined?(klass.name.to_s) } 14 | ActiveSupport::DescendantsTracker.direct_descendants(ActiveRecord::Base).clear 15 | end 16 | 17 | before do 18 | clear_active_record! 19 | load File.expand_path('../models.rb', __FILE__) 20 | ActiveRecord::Base.auto_upgrade! 21 | MiniRecord.reset_configuration! 22 | end 23 | 24 | it 'has #schema inside model' do 25 | assert_equal 'people', Person.table_name 26 | assert_equal %w[created_at id name updated_at], Person.db_columns.sort 27 | assert_equal Person.db_columns, Person.column_names.sort 28 | assert_equal Person.schema_columns, Person.column_names.sort 29 | 30 | # Check surname attribute 31 | person = Person.create(:name => 'foo') 32 | assert_equal 'foo', person.name 33 | assert_raises(NoMethodError){ person.surname } 34 | 35 | # Test the timestamp columns exist 36 | assert_respond_to person, :created_at 37 | assert_respond_to person, :updated_at 38 | 39 | # Add a column without lost data 40 | Person.class_eval do 41 | schema do |p| 42 | p.string :name 43 | p.string :surname 44 | end 45 | timestamps 46 | end 47 | Person.auto_upgrade! 48 | assert_equal 1, Person.count 49 | 50 | person = Person.last 51 | assert_equal 'foo', person.name 52 | assert_nil person.surname 53 | 54 | person.update_column(:surname, 'bar') 55 | assert_equal %w[created_at id name surname updated_at], Person.db_columns.sort 56 | 57 | # Remove a column without lost data 58 | Person.class_eval do 59 | schema do |p| 60 | p.string :name 61 | end 62 | timestamps 63 | end 64 | Person.auto_upgrade! 65 | person = Person.last 66 | assert_equal 'foo', person.name 67 | assert_raises(NoMethodError) { person.surname } 68 | assert_equal %w[created_at id name updated_at], Person.db_columns 69 | assert_equal Person.column_names.sort, Person.db_columns 70 | assert_equal Person.column_names.sort, Person.schema_columns 71 | 72 | # Change column without lost data 73 | Person.class_eval do 74 | schema do |p| 75 | p.text :name 76 | end 77 | end 78 | person = Person.last 79 | assert_equal 'foo', person.name 80 | end 81 | 82 | it 'has #key,col,property,attribute inside model' do 83 | assert_equal Post.column_names.sort, Post.db_columns 84 | assert_equal Category.column_names.sort, Category.schema_columns 85 | 86 | # Check default properties 87 | category = Category.create(:title => 'category') 88 | post = Post.create(:title => 'foo', :body => 'bar', :category_id => category.id) 89 | post = Post.first 90 | assert_equal 'foo', post.title 91 | assert_equal 'bar', post.body 92 | assert_equal category, post.category 93 | 94 | 95 | # Remove a column 96 | Post.reset_table_definition! 97 | Post.class_eval do 98 | col :name 99 | col :category, :as => :references 100 | end 101 | Post.auto_upgrade! 102 | refute_includes %w[title body], Post.db_columns 103 | 104 | post = Post.first 105 | assert_nil post.name 106 | assert_equal category, post.category 107 | assert_raises(NoMethodError, ActiveModel::MissingAttributeError) { post.title } 108 | end 109 | 110 | it 'has indexes inside model' do 111 | # Check indexes 112 | assert Animal.db_indexes.size > 0 113 | assert_equal Animal.db_indexes, Animal.indexes.keys.sort 114 | 115 | 116 | # Remove an index 117 | indexes_was = Animal.db_indexes 118 | Animal.indexes.delete(indexes_was.pop) 119 | Animal.auto_upgrade! 120 | assert_equal indexes_was, Animal.indexes.keys 121 | assert_equal indexes_was, Animal.db_indexes 122 | 123 | # Add a new index 124 | Animal.class_eval do 125 | col :category, :as => :references, :index => true 126 | end 127 | Animal.auto_upgrade! 128 | new_indexes = indexes_was + %w[index_animals_on_category_id] 129 | assert_includes Animal.db_columns, 'category_id' 130 | assert_equal new_indexes.sort, Animal.db_indexes 131 | end 132 | 133 | it 'not add already defined indexes' do 134 | class Foo < ActiveRecord::Base 135 | index :customer_id, :unique => true, :name => 'by_customer' 136 | belongs_to :customer 137 | end 138 | # Run auto_upgrade! once to create table and index. 139 | Foo.auto_upgrade! 140 | assert_equal 1, Foo.db_indexes.size 141 | assert_includes Foo.db_indexes, 'by_customer' 142 | # Run auto_upgrade! again and ensure no statements issued. 143 | Foo.auto_upgrade! 144 | refute_match /schema\s+change/, Foo.queries 145 | end 146 | 147 | it 'does not add already defined composite indexes' do 148 | class Foo < ActiveRecord::Base 149 | belongs_to :region 150 | belongs_to :customer 151 | add_index [:region_id, :customer_id], :unique => true, :name => 'by_region_and_customer' 152 | end 153 | # Run auto_upgrade! once to create table and index. 154 | Foo.auto_upgrade! 155 | assert_equal 3, Foo.db_indexes.size 156 | assert_includes Foo.db_indexes, 'by_region_and_customer' 157 | # Run auto_upgrade! again and ensure no statements issued. 158 | Foo.auto_upgrade! 159 | refute_match /schema\s+change/, Foo.queries 160 | end 161 | 162 | it 'supports indexes with symbols for names' do 163 | class Foo < ActiveRecord::Base 164 | col :some_field, :index => {:name => :idx_for_some_field} 165 | end 166 | # Run auto_upgrade! once to create table and index. 167 | Foo.auto_upgrade! 168 | assert_equal 1, Foo.db_indexes.size 169 | assert_includes Foo.db_indexes, 'idx_for_some_field' 170 | # Run auto_upgrade! again and ensure no statements issued. 171 | Foo.auto_upgrade! 172 | refute_match /schema\s+change/, Foo.queries 173 | end 174 | 175 | it 'works with STI' do 176 | class Dog < Pet; end 177 | class Cat < Pet; end 178 | class Kitten < Cat; end 179 | ActiveRecord::Base.auto_upgrade! 180 | 181 | # Check inheritance column 182 | assert_includes Pet.db_columns, "type" 183 | 184 | # Now, let's we know if STI is working 185 | Pet.create(:name => "foo") 186 | Dog.create(:name => "bar") 187 | Kitten.create(:name => 'foxy') 188 | assert_equal 1, Dog.count 189 | assert_equal 'bar', Dog.first.name 190 | assert_equal 3, Pet.count 191 | assert_equal %w[foo bar foxy], Pet.all.map(&:name) 192 | assert_equal 'bar', Dog.first.name 193 | 194 | # What's happen if we change schema? 195 | assert_equal Dog.table_definition, Pet.table_definition 196 | assert_equal Dog.indexes, Pet.indexes 197 | 198 | Dog.class_eval do 199 | col :bau 200 | end 201 | ActiveRecord::Base.auto_upgrade! 202 | assert_includes Dog.schema_columns, 'bau' 203 | assert_includes Pet.db_columns, 'bau' 204 | end 205 | 206 | it 'works with custom inheritance column' do 207 | class User < ActiveRecord::Base 208 | col :name 209 | col :surname 210 | col :role 211 | def self.inheritance_column; 'role'; end 212 | end 213 | 214 | class Administrator < User; end 215 | class Customer < User; end 216 | 217 | User.auto_upgrade! 218 | assert_equal 'role', User.inheritance_column 219 | 220 | Administrator.create(:name => "Davide", :surname => 'DAddYE') 221 | Customer.create(:name => "Foo", :surname => "Bar") 222 | assert_equal 1, Administrator.count 223 | assert_equal 'Davide', Administrator.first.name 224 | assert_equal 1, Customer.count 225 | assert_equal 'Foo', Customer.first.name 226 | assert_equal 2, User.count 227 | assert_equal 'Administrator', User.first.role 228 | assert_equal 'Customer', User.last.role 229 | assert_includes User.db_indexes, 'index_users_on_role' 230 | end 231 | 232 | it 'allow multiple columns definitions' do 233 | class Fake < ActiveRecord::Base 234 | col :name, :surname 235 | col :category, :group, :as => :references 236 | end 237 | Fake.auto_upgrade! 238 | Fake.create(:name => 'foo', :surname => 'bar', :category_id => 1, :group_id => 2) 239 | fake = Fake.first 240 | assert_equal 'foo', fake.name 241 | assert_equal 'bar', fake.surname 242 | assert_equal 1, fake.category_id 243 | assert_equal 2, fake.group_id 244 | end 245 | 246 | it 'allow custom query' do 247 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 248 | 249 | class Foo < ActiveRecord::Base 250 | col :name, :as => "ENUM('foo','bar')" 251 | end 252 | Foo.auto_upgrade! 253 | assert_match /ENUM/, Foo.queries 254 | 255 | Foo.auto_upgrade! 256 | refute_match /schema\s+change/, Foo.queries 257 | assert_equal %w[id name], Foo.db_columns 258 | assert_equal %w[id name], Foo.schema_columns 259 | 260 | foo = Foo.create(:name => 'test') 261 | assert_empty Foo.first.name 262 | 263 | foo.update_column(:name, 'foo') 264 | 265 | assert_equal 'foo', Foo.first.name 266 | end 267 | 268 | describe 'relation #belongs_to' do 269 | 270 | it 'creates a column and index based on relation' do 271 | Article.create(:title => 'Hello', :publisher_id => 1) 272 | Article.first.tap do |a| 273 | assert_equal 'Hello', a.title 274 | assert_equal 1, a.publisher_id 275 | end 276 | assert_includes Article.db_indexes, 'index_articles_on_publisher_id' 277 | 278 | # Ensure that associated field/index is not deleted on upgrade 279 | Article.auto_upgrade! 280 | assert_equal 1, Article.first.publisher_id 281 | assert_includes Article.db_indexes, 'index_articles_on_publisher_id' 282 | end 283 | 284 | it 'removes a column and index when relation is removed' do 285 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 286 | 287 | class Foo < ActiveRecord::Base 288 | key :name 289 | belongs_to :image, :polymorphic => true 290 | end 291 | Foo.auto_upgrade! 292 | assert_includes Foo.db_columns, 'name' 293 | assert_includes Foo.db_columns, 'image_type' 294 | assert_includes Foo.db_columns, 'image_id' 295 | assert_includes Foo.db_indexes, 'index_foos_on_image_id_and_image_type' 296 | 297 | Foo.class_eval do 298 | reset_table_definition! 299 | reflections.clear 300 | indexes.clear 301 | key :name 302 | end 303 | Foo.auto_upgrade! 304 | assert_includes Foo.db_columns, 'name' 305 | refute_includes Foo.db_columns, 'image_type' 306 | refute_includes Foo.db_columns, 'image_id' 307 | assert_empty Foo.db_indexes 308 | end 309 | 310 | it 'doesnt remove a column and index when relation is removed and destructive is false' do 311 | MiniRecord.configuration.destructive = false 312 | class Foo < ActiveRecord::Base 313 | key :name 314 | belongs_to :image, :polymorphic => true 315 | end 316 | Foo.auto_upgrade! 317 | assert_includes Foo.db_columns, 'name' 318 | assert_includes Foo.db_columns, 'image_type' 319 | assert_includes Foo.db_columns, 'image_id' 320 | assert_includes Foo.db_indexes, 'index_foos_on_image_id_and_image_type' 321 | 322 | Foo.class_eval do 323 | reset_table_definition! 324 | reflections.clear 325 | indexes.clear 326 | key :name 327 | end 328 | Foo.auto_upgrade! 329 | assert_includes Foo.db_columns, 'name' 330 | assert_includes Foo.db_columns, 'image_type' 331 | assert_includes Foo.db_columns, 'image_id' 332 | assert_includes Foo.db_indexes, 'index_foos_on_image_id_and_image_type' 333 | end 334 | 335 | it 'creates columns and index based on polymorphic relation' do 336 | Attachment.create(:name => 'Avatar', :attachable_id => 1, :attachable_type => 'Post') 337 | Attachment.first.tap do |attachment| 338 | assert_equal 'Avatar', attachment.name 339 | assert_equal 1, attachment.attachable_id 340 | assert_equal 'Post', attachment.attachable_type 341 | end 342 | index = 'index_attachments_on_attachable_id_and_attachable_type' 343 | assert_includes Attachment.db_indexes, index 344 | 345 | # Ensure that associated fields/indexes are not deleted on subsequent upgrade 346 | Attachment.auto_upgrade! 347 | assert_equal 1, Attachment.first.attachable_id 348 | assert_equal 'Post', Attachment.first.attachable_type 349 | assert_includes Attachment.db_indexes, index 350 | end 351 | 352 | it 'should support :class_name' do 353 | assert_includes Task.schema_columns, 'author_id' 354 | assert_includes Task.db_columns, 'author_id' 355 | end 356 | 357 | it 'should support :foreign_key' do 358 | assert_includes Activity.schema_columns, 'custom_id' 359 | assert_includes Activity.db_columns, 'custom_id' 360 | end 361 | 362 | it 'should memonize in schema relationships' do 363 | silence_stream(STDERR) { conn.create_table('foos') } 364 | conn.add_column :foos, :name, :string 365 | conn.add_column :foos, :bar_id, :integer 366 | conn.add_index :foos, :bar_id 367 | class Foo < ActiveRecord::Base 368 | col :name 369 | belongs_to :bar 370 | end 371 | assert_includes Foo.db_columns, 'name' 372 | assert_includes Foo.db_columns, 'bar_id' 373 | assert_includes Foo.db_indexes, 'index_foos_on_bar_id' 374 | 375 | Foo.auto_upgrade! 376 | assert_includes Foo.schema_columns, 'name' 377 | assert_includes Foo.schema_columns, 'bar_id' 378 | assert_includes Foo.indexes, 'index_foos_on_bar_id' 379 | end 380 | 381 | it 'should add new columns without lost belongs_to schema' do 382 | publisher = Publisher.create(:name => 'foo') 383 | article = Article.create(:title => 'bar', :publisher => publisher) 384 | assert article.valid? 385 | assert_includes Article.indexes, 'index_articles_on_publisher_id' 386 | 387 | # Here we perform a schema change 388 | Article.key :body 389 | Article.auto_upgrade! 390 | article.reload 391 | assert_nil article.body 392 | 393 | article.update_column(:body, 'null') 394 | assert_equal 'null', article.body 395 | 396 | # Finally check the index existance 397 | assert_includes Article.db_indexes, 'index_articles_on_publisher_id' 398 | end 399 | 400 | it 'should not override previous defined column relation' do 401 | class Foo < ActiveRecord::Base 402 | key :user, :as => :references, :null => false, :limit => 4, :default => 42 403 | belongs_to :user 404 | end 405 | Foo.auto_upgrade! 406 | assert_equal 4, Foo.db_fields[:user_id].limit 407 | assert_equal false, Foo.db_fields[:user_id].null 408 | assert_equal "42", Foo.db_fields[:user_id].default.to_s 409 | end 410 | 411 | it 'add/remove foreign key with :foreign option, when Foreigner gem used on mysql' do 412 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 413 | 414 | class Book < ActiveRecord::Base 415 | belongs_to :publisher 416 | index :publisher_id, :foreign => true 417 | end 418 | Book.auto_upgrade! 419 | 420 | assert_includes Book.db_columns, 'publisher_id' 421 | assert_includes Book.db_indexes, 'index_books_on_publisher_id' 422 | 423 | assert connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'publisher_id'} 424 | 425 | Object.send(:remove_const, :Book) 426 | class Book < ActiveRecord::Base 427 | belongs_to :publisher 428 | index :publisher_id, :foreign => false 429 | end 430 | Book.auto_upgrade! 431 | 432 | assert_nil connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'publisher_id'} 433 | end 434 | 435 | it 'doesnt remove foreign key with :foreign option, when Foreigner gem used on mysql and destructive = false' do 436 | MiniRecord.configuration.destructive = false 437 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 438 | 439 | class Book < ActiveRecord::Base 440 | belongs_to :publisher 441 | index :publisher_id, :foreign => true 442 | end 443 | Book.auto_upgrade! 444 | 445 | assert_includes Book.db_columns, 'publisher_id' 446 | assert_includes Book.db_indexes, 'index_books_on_publisher_id' 447 | 448 | assert connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'publisher_id'} 449 | 450 | Object.send(:remove_const, :Book) 451 | class Book < ActiveRecord::Base 452 | belongs_to :publisher 453 | index :publisher_id, :foreign => false 454 | end 455 | Book.auto_upgrade! 456 | 457 | assert_includes Book.db_columns, 'publisher_id' 458 | assert_includes Book.db_indexes, 'index_books_on_publisher_id' 459 | end 460 | 461 | it 'add/remove named foreign key with :foreign option, when Foreigner gem used on mysql' do 462 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 463 | 464 | class Book < ActiveRecord::Base 465 | belongs_to :publisher 466 | index :publisher_id, :name => 'my_super_publisher_id_fk', :foreign => true 467 | end 468 | Book.auto_upgrade! 469 | 470 | assert_includes Book.db_columns, 'publisher_id' 471 | assert_includes Book.db_indexes, 'my_super_publisher_id_fk' 472 | 473 | assert connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'publisher_id'} 474 | 475 | Object.send(:remove_const, :Book) 476 | class Book < ActiveRecord::Base 477 | belongs_to :publisher 478 | index :publisher_id, :name => 'my_super_publisher_id_fk', :foreign => false 479 | end 480 | Book.auto_upgrade! 481 | 482 | assert_nil connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'publisher_id'} 483 | Object.send(:remove_const, :Book) 484 | end 485 | 486 | it 'doesnt remove named foreign key with :foreign option, when Foreigner gem used on mysql and destructive = false' do 487 | MiniRecord.configuration.destructive = false 488 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 489 | 490 | class Book < ActiveRecord::Base 491 | belongs_to :publisher 492 | index :publisher_id, :name => 'my_super_publisher_id_fk', :foreign => true 493 | end 494 | Book.auto_upgrade! 495 | 496 | assert_includes Book.db_columns, 'publisher_id' 497 | assert_includes Book.db_indexes, 'my_super_publisher_id_fk' 498 | 499 | assert connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'publisher_id'} 500 | 501 | Object.send(:remove_const, :Book) 502 | class Book < ActiveRecord::Base 503 | belongs_to :publisher 504 | index :publisher_id, :name => 'my_super_publisher_id_fk', :foreign => false 505 | end 506 | Book.auto_upgrade! 507 | 508 | assert_includes Book.db_columns, 'publisher_id' 509 | assert_includes Book.db_indexes, 'my_super_publisher_id_fk' 510 | Object.send(:remove_const, :Book) 511 | end 512 | 513 | it 'support :foreign option in the index with custom :foreign_key in the belong_to association' do 514 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 515 | 516 | class Book < ActiveRecord::Base 517 | belongs_to :second_publisher, :foreign_key => 'second_publisher_id', :class_name => 'Publisher' 518 | index :second_publisher_id, :foreign => true 519 | end 520 | Book.auto_upgrade! 521 | 522 | assert_includes Book.db_columns, 'second_publisher_id' 523 | assert_includes Book.db_indexes, 'index_books_on_second_publisher_id' 524 | 525 | assert connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'second_publisher_id'} 526 | 527 | Object.send(:remove_const, :Book) 528 | class Book < ActiveRecord::Base 529 | belongs_to :second_publisher, :foreign_key => 'second_publisher_id', :class_name => 'Publisher' 530 | index :second_publisher_id, :foreign => false 531 | end 532 | Book.auto_upgrade! 533 | 534 | assert_nil connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'second_publisher_id'} 535 | end 536 | 537 | it 'support :foreign option in the index with custom :foreign_key in the belong_to association and wont remove if destructive = false' do 538 | MiniRecord.configuration.destructive = false 539 | skip "foreign key tests only for mysql" unless conn.adapter_name =~ /mysql/i 540 | 541 | class Book < ActiveRecord::Base 542 | belongs_to :second_publisher, :foreign_key => 'second_publisher_id', :class_name => 'Publisher' 543 | index :second_publisher_id, :foreign => true 544 | end 545 | Book.auto_upgrade! 546 | 547 | assert_includes Book.db_columns, 'second_publisher_id' 548 | assert_includes Book.db_indexes, 'index_books_on_second_publisher_id' 549 | 550 | assert connection.foreign_keys(:books).detect {|fk| fk.options[:column] == 'second_publisher_id'} 551 | 552 | Object.send(:remove_const, :Book) 553 | class Book < ActiveRecord::Base 554 | belongs_to :second_publisher, :foreign_key => 'second_publisher_id', :class_name => 'Publisher' 555 | index :second_publisher_id, :foreign => false 556 | end 557 | Book.auto_upgrade! 558 | 559 | assert_includes Book.db_columns, 'second_publisher_id' 560 | assert_includes Book.db_indexes, 'index_books_on_second_publisher_id' 561 | end 562 | 563 | it "does not add suppressed index" do 564 | class Foo < ActiveRecord::Base 565 | belongs_to :customer 566 | suppress_index :customer 567 | end 568 | Foo.auto_upgrade! 569 | assert_equal 0, Foo.db_indexes.size 570 | end 571 | 572 | it "does not add suppressed index from polymorphic relation" do 573 | class Foo < ActiveRecord::Base 574 | belongs_to :customer, :polymorphic => true 575 | suppress_index :customer 576 | end 577 | Foo.auto_upgrade! 578 | assert_equal 0, Foo.db_indexes.size 579 | end 580 | 581 | end 582 | 583 | describe 'relation #habtm' do 584 | it 'creates a join table with indexes for has_and_belongs_to_many relations' do 585 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 586 | 587 | tables = Tool.connection.tables 588 | assert_includes tables, 'purposes_tools' 589 | 590 | index = 'index_purposes_tools_on_tool_id_and_purpose_id' 591 | assert_includes Tool.connection.indexes('purposes_tools').map(&:name), index 592 | 593 | # Ensure that join table is not deleted on subsequent upgrade 594 | Tool.auto_upgrade! 595 | assert_includes tables, 'purposes_tools' 596 | assert_includes Tool.connection.indexes('purposes_tools').map(&:name), index 597 | end 598 | 599 | it 'drops join table if has_and_belongs_to_many relation is deleted' do 600 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 601 | 602 | Tool.schema_tables.delete('purposes_tools') 603 | refute_includes ActiveRecord::Base.schema_tables, 'purposes_tools' 604 | 605 | ActiveRecord::Base.clear_tables! 606 | refute_includes Tool.connection.tables, 'purposes_tools' 607 | end 608 | 609 | it 'keeps join table if has_and_belongs_to_many relation is deleted and destructive = false' do 610 | MiniRecord.configuration.destructive = false 611 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 612 | 613 | tables = Tool.connection.tables 614 | assert_includes tables, 'purposes_tools' 615 | 616 | Tool.schema_tables.delete('purposes_tools') 617 | refute_includes ActiveRecord::Base.schema_tables, 'purposes_tools' 618 | 619 | ActiveRecord::Base.clear_tables! 620 | assert_includes Tool.connection.tables, 'purposes_tools' 621 | end 622 | 623 | it 'has_and_belongs_to_many with custom join_table and foreign keys' do 624 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 625 | 626 | class Foo < ActiveRecord::Base 627 | has_and_belongs_to_many :watchers, :join_table => :watching, :foreign_key => :custom_foo_id, :association_foreign_key => :customer_id 628 | end 629 | Foo.auto_upgrade! 630 | assert_includes conn.tables, 'watching' 631 | 632 | cols = conn.columns('watching').map(&:name) 633 | refute_includes cols, 'id' 634 | assert_includes cols, 'custom_foo_id' 635 | assert_includes cols, 'customer_id' 636 | end 637 | 638 | it 'creates a join table with indexes with long indexes names' do 639 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 640 | 641 | class Foo < ActiveRecord::Base 642 | has_and_belongs_to_many :people, :join_table => :long_people, 643 | :foreign_key => :custom_long_long_long_long_id, 644 | :association_foreign_key => :customer_super_long_very_long_trust_me_id 645 | end 646 | Foo.auto_upgrade! 647 | index_name = 'index_long_people_on_custom_long_long_long_long_id_and_customer_super_long_very_long_trust_me_id'[0...conn.index_name_length] 648 | assert_includes conn.tables, 'people' 649 | assert_includes conn.indexes(:long_people).map(&:name), index_name 650 | end 651 | 652 | it 'creates a join table without an index when suppressed for has_and_belongs_to_many relations' do 653 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 654 | 655 | class Foo < ActiveRecord::Base 656 | has_and_belongs_to_many :bars 657 | suppress_index :bars 658 | end 659 | Foo.auto_upgrade! 660 | assert_includes conn.tables, 'bars_foos' 661 | assert_equal 0, conn.indexes(:bars_foos).size 662 | end 663 | 664 | it 'adds unique index' do 665 | skip "habtm key tests only for mysql" unless conn.adapter_name =~ /mysql/i 666 | 667 | page = Page.create(:title => 'Foo') 668 | photogallery = Photogallery.create(:title => 'Bar') 669 | assert photogallery.valid? 670 | 671 | photogallery.pages << page 672 | refute_empty Photogallery.queries 673 | assert_includes photogallery.reload.pages, page 674 | assert_raises(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid){ photogallery.pages << page } 675 | end 676 | end 677 | 678 | it 'should add multiple index' do 679 | class Foo < ActiveRecord::Base 680 | key :name, :surname, :index => true 681 | end 682 | Foo.auto_upgrade! 683 | assert_includes Foo.db_indexes, 'index_foos_on_name' 684 | assert_includes Foo.db_indexes, 'index_foos_on_surname' 685 | end 686 | 687 | it 'should create a unique index' do 688 | class Foo < ActiveRecord::Base 689 | key :name, :surname 690 | add_index([:name, :surname], :unique => true) 691 | end 692 | Foo.auto_upgrade! 693 | db_indexes = Foo.connection.indexes('foos')[0] 694 | assert_equal 'index_foos_on_name_and_surname', db_indexes.name 695 | assert db_indexes.unique 696 | assert_equal %w[name surname], db_indexes.columns.sort 697 | end 698 | 699 | it 'should change #limit' do 700 | class Foo < ActiveRecord::Base 701 | key :number, :as => :integer 702 | key :string, :limit => 100 703 | end 704 | Foo.auto_upgrade! 705 | assert_match /CREATE TABLE/, Foo.queries 706 | 707 | Foo.auto_upgrade! 708 | refute_match /alter/i, Foo.queries 709 | 710 | # According to this: 711 | # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L476-487 712 | Foo.key :number, :as => :integer, :limit => 4 713 | Foo.auto_upgrade! 714 | case conn.adapter_name 715 | when /sqlite/i 716 | # In sqlite there is a difference between limit: 4 and limit: 11 717 | assert_match 'foos.number#limit', Foo.queries 718 | assert_equal 4, Foo.schema_fields[:number].limit 719 | assert_equal 4, Foo.db_fields[:number].limit 720 | when /mysql/i 721 | # In mysql according to this: http://goo.gl/bjZE7 limit: 4 is same of limit:11 722 | refute_match /schema\s+change/, Foo.queries 723 | assert_equal 4, Foo.schema_fields[:number].limit 724 | assert_equal 4, Foo.db_fields[:number].limit 725 | when /postgres/i 726 | # In postgres limit: 4 will be translated to nil 727 | assert_match /ALTER COLUMN "number" TYPE integer$/, Foo.queries 728 | assert_equal 4, Foo.schema_fields[:number].limit 729 | assert_equal nil, Foo.db_fields[:number].limit 730 | end 731 | 732 | # Change limit to string 733 | Foo.key :string, :limit => 255 734 | Foo.auto_upgrade! 735 | refute_empty Foo.queries 736 | assert_equal 255, Foo.db_fields[:string].limit 737 | end 738 | 739 | it 'should not change #limit if destructive = false' do 740 | MiniRecord.configuration.destructive = false 741 | class Foo < ActiveRecord::Base 742 | key :number, :as => :integer 743 | key :string, :limit => 100 744 | end 745 | Foo.auto_upgrade! 746 | assert_match /CREATE TABLE/, Foo.queries 747 | 748 | Foo.auto_upgrade! 749 | refute_match /alter/i, Foo.queries 750 | 751 | # According to this: 752 | # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L476-487 753 | Foo.key :number, :as => :integer, :limit => 4 754 | Foo.auto_upgrade! 755 | case conn.adapter_name 756 | when /sqlite/i 757 | # In sqlite there is a difference between limit: 4 and limit: 11 758 | assert_match Foo.queries, "" 759 | assert_equal nil, Foo.db_fields[:number].limit 760 | assert_equal 4, Foo.schema_fields[:number].limit 761 | when /mysql/i 762 | # In mysql according to this: http://goo.gl/bjZE7 limit: 4 is same of limit:11 763 | refute_match /schema\s+change/, Foo.queries 764 | assert_equal nil, Foo.db_fields[:number].limit 765 | assert_equal 4, Foo.schema_fields[:number].limit 766 | when /postgres/i 767 | # In postgres limit: 4 will be translated to nil 768 | assert_match Foo.queries, "" 769 | assert_equal nil, Foo.db_fields[:number].limit 770 | assert_equal 4, Foo.schema_fields[:number].limit 771 | end 772 | 773 | # Change limit to string 774 | Foo.key :string, :limit => 255 775 | Foo.auto_upgrade! 776 | refute_match /schema\s+change/, Foo.queries 777 | assert_equal 100, Foo.db_fields[:string].limit 778 | end 779 | 780 | it 'should handle integer defaults correctly' do 781 | class Foo < ActiveRecord::Base 782 | field :some_int, type: :integer, default: 33 783 | field :some_bool, type: :boolean, default: false 784 | field :some_bool2, type: :boolean, default: false 785 | field :some_bool3, type: :boolean, default: true 786 | auto_upgrade! 787 | end 788 | 789 | # Reopen class 790 | class Foo < ActiveRecord::Base 791 | field :some_int, type: :integer, default: 66 792 | field :some_bool, type: :boolean, default: true 793 | end 794 | 795 | new_attr, changed = Foo.field_attr_changes(:some_int) 796 | assert_equal 66, new_attr[:default] 797 | assert_equal true, changed 798 | 799 | new_attr, changed = Foo.field_attr_changes(:some_bool) 800 | assert_equal true, changed 801 | assert_equal true, new_attr[:default] 802 | 803 | new_attr, changed = Foo.field_attr_changes(:some_bool2) 804 | assert_equal false, changed 805 | assert_empty new_attr 806 | 807 | new_attr, changed = Foo.field_attr_changes(:some_bool3) 808 | assert_equal false, changed 809 | assert_empty new_attr 810 | end 811 | 812 | it 'should change #null' do 813 | class Foo < ActiveRecord::Base 814 | key :string 815 | end 816 | Foo.auto_upgrade! 817 | assert Foo.db_fields[:string].null 818 | 819 | # Same as above 820 | Foo.key :string, :null => true 821 | Foo.auto_upgrade! 822 | refute_match /alter/i, Foo.queries 823 | assert Foo.db_fields[:string].null 824 | 825 | Foo.key :string, :null => nil 826 | Foo.auto_upgrade! 827 | refute_match /alter/i, Foo.queries 828 | assert Foo.db_fields[:string].null 829 | 830 | Foo.key :string, :null => false 831 | Foo.auto_upgrade! 832 | assert_match /foos.string#null/i, Foo.queries 833 | refute Foo.db_fields[:string].null 834 | end 835 | 836 | it 'should not change #null if destructive = false' do 837 | MiniRecord.configuration.destructive = false 838 | class Foo < ActiveRecord::Base 839 | key :string 840 | end 841 | Foo.auto_upgrade! 842 | assert Foo.db_fields[:string].null 843 | 844 | # Same as above 845 | Foo.key :string, :null => true 846 | Foo.auto_upgrade! 847 | refute_match /alter/i, Foo.queries 848 | assert Foo.db_fields[:string].null 849 | 850 | Foo.key :string, :null => nil 851 | Foo.auto_upgrade! 852 | refute_match /alter/i, Foo.queries 853 | assert Foo.db_fields[:string].null 854 | 855 | Foo.key :string, :null => false 856 | Foo.auto_upgrade! 857 | assert_match "", Foo.queries 858 | assert Foo.db_fields[:string].null 859 | end 860 | 861 | it 'should change #scale #precision' do 862 | class Foo < ActiveRecord::Base 863 | field :currency, :as => :decimal, :precision => 8, :scale => 2 864 | end 865 | Foo.auto_upgrade! 866 | assert_equal 8, Foo.db_fields[:currency].precision 867 | assert_equal 2, Foo.db_fields[:currency].scale 868 | 869 | Foo.auto_upgrade! 870 | new_attr, changed = Foo.field_attr_changes(:currency) 871 | assert_equal false, changed 872 | 873 | Foo.field :currency, :as => :decimal, :precision => 4, :scale => 2, :limit => 5 874 | Foo.auto_upgrade! 875 | assert_match /foos.currency#limit/i, Foo.queries 876 | assert_equal 4, Foo.db_fields[:currency].precision 877 | assert_equal 2, Foo.db_fields[:currency].scale 878 | end 879 | 880 | it 'should not change #scale #precision if destructive = false' do 881 | MiniRecord.configuration.destructive = false 882 | class Foo < ActiveRecord::Base 883 | field :currency, :as => :decimal, :precision => 8, :scale => 2 884 | end 885 | Foo.auto_upgrade! 886 | assert_equal 8, Foo.db_fields[:currency].precision 887 | assert_equal 2, Foo.db_fields[:currency].scale 888 | 889 | Foo.auto_upgrade! 890 | refute_match /alter/i, Foo.queries 891 | 892 | Foo.field :currency, :as => :decimal, :precision => 4, :scale => 2, :limit => 5 893 | Foo.auto_upgrade! 894 | assert_match "", Foo.queries 895 | assert_equal 8, Foo.db_fields[:currency].precision 896 | assert_equal 2, Foo.db_fields[:currency].scale 897 | end 898 | 899 | it 'should ignore abstract classes' do 900 | class Foo < ActiveRecord::Base 901 | self.abstract_class = true 902 | end 903 | 904 | class Bar < Foo 905 | end 906 | 907 | Foo.auto_upgrade! 908 | Bar.auto_upgrade! 909 | 910 | tables = Foo.connection.tables 911 | 912 | refute_includes tables, 'foos' 913 | refute_includes tables, '' 914 | assert_includes tables, 'bars' 915 | end 916 | 917 | it 'should prevent abstract table class to leak columns to other tables' do 918 | 919 | class Base < ActiveRecord::Base 920 | self.abstract_class = true 921 | end 922 | 923 | class User < Base 924 | col :name 925 | end 926 | 927 | class Book < Base 928 | col :title 929 | col :author 930 | end 931 | 932 | User.auto_upgrade! 933 | Book.auto_upgrade! 934 | 935 | assert_equal ['id', 'name'], User.db_columns.sort 936 | assert_equal ['author', 'id', 'title'], Book.db_columns.sort 937 | end 938 | 939 | it 'should rename a column specified by rename_field' do 940 | class Foo < ActiveRecord::Base 941 | field :currency, :limit => 3 942 | end 943 | Foo.auto_upgrade! 944 | assert_match /CREATE TABLE/, Foo.queries 945 | 946 | Foo.create :currency => 'USD' 947 | 948 | Foo.rename_field :currency, :new_name => :currency_iso 949 | Foo.field :currency_iso, :limit => 3 950 | 951 | Foo.auto_upgrade! 952 | 953 | assert_match /foos.currency to currency_iso/i, Foo.queries 954 | 955 | foo = Foo.first 956 | assert_equal 'USD', foo.currency_iso 957 | 958 | Foo.auto_upgrade! 959 | assert_match '', Foo.queries 960 | 961 | end 962 | 963 | it 'should note rename a column specified by rename_field if destructive = false' do 964 | MiniRecord.configuration.destructive = false 965 | class Foo < ActiveRecord::Base 966 | field :currency, :limit => 3 967 | end 968 | Foo.auto_upgrade! 969 | assert_match /CREATE TABLE/, Foo.queries 970 | 971 | Foo.create :currency => 'USD' 972 | 973 | Foo.rename_field :currency, :new_name => :currency_iso 974 | Foo.field :currency_iso, :limit => 3 975 | 976 | Foo.auto_upgrade! 977 | 978 | case conn.adapter_name 979 | when /sqlite/i 980 | assert_match "", Foo.queries 981 | when /mysql/i 982 | assert_match "", Foo.queries 983 | when /postgres/i 984 | assert_match "", Foo.queries 985 | end 986 | 987 | cols = conn.columns('foos').map(&:name) 988 | assert_includes cols, "currency_iso" 989 | assert_includes cols, "currency" 990 | 991 | Foo.auto_upgrade! 992 | assert_match '', Foo.queries 993 | 994 | end 995 | 996 | it 'accepts create_table options' do 997 | class Foo < ActiveRecord::Base 998 | create_table options: "extra options" 999 | end 1000 | Foo.auto_upgrade! rescue nil # eat the exception from invalid options 1001 | assert_match /CREATE TABLE.* extra options\Z/, Foo.queries 1002 | end 1003 | 1004 | it 'can do a dry run' do 1005 | class Foo < ActiveRecord::Base 1006 | end 1007 | 1008 | ActiveRecord::Base.auto_upgrade_dry 1009 | refute_match /\bcreate\b/i, Foo.queries 1010 | refute_match /\balter\b/i, Foo.queries 1011 | 1012 | ActiveRecord::Base.auto_upgrade! 1013 | assert_match /\bcreate\b/i, Foo.queries 1014 | refute_match /\balter\b/i, Foo.queries 1015 | 1016 | clear_active_record!(:keep_tables => true) 1017 | class Foo < ActiveRecord::Base 1018 | property :new_field, :index => true 1019 | end 1020 | 1021 | ActiveRecord::Base.auto_upgrade_dry 1022 | refute_match /\bcreate\b/i, Foo.queries 1023 | refute_match /\balter\b/i, Foo.queries 1024 | 1025 | ActiveRecord::Base.auto_upgrade! 1026 | assert_match /\bcreate\b/i, Foo.queries 1027 | assert_match /\balter\b/i, Foo.queries 1028 | end 1029 | end 1030 | --------------------------------------------------------------------------------