migrant 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/LICENSE +20 -0
- data/README.rdoc +124 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/lib/datatype/base.rb +65 -0
- data/lib/datatype/boolean.rb +15 -0
- data/lib/datatype/currency.rb +11 -0
- data/lib/datatype/date.rb +13 -0
- data/lib/datatype/float.rb +11 -0
- data/lib/datatype/foreign_key.rb +9 -0
- data/lib/datatype/hash.rb +10 -0
- data/lib/datatype/polymorphic.rb +8 -0
- data/lib/datatype/range.rb +13 -0
- data/lib/datatype/string.rb +22 -0
- data/lib/datatype/symbol.rb +10 -0
- data/lib/datatype/time.rb +5 -0
- data/lib/migrant/migration_generator.rb +123 -0
- data/lib/migrant/model_extensions.rb +37 -0
- data/lib/migrant/schema.rb +91 -0
- data/lib/migrant.rb +16 -0
- data/lib/railtie.rb +11 -0
- data/lib/tasks/db.rake +11 -0
- data/migrant.gemspec +167 -0
- data/test/additional_models/review.rb +10 -0
- data/test/helper.rb +38 -0
- data/test/rails_app/.gitignore +4 -0
- data/test/rails_app/README +256 -0
- data/test/rails_app/Rakefile +7 -0
- data/test/rails_app/app/controllers/application_controller.rb +3 -0
- data/test/rails_app/app/helpers/application_helper.rb +2 -0
- data/test/rails_app/app/models/business.rb +22 -0
- data/test/rails_app/app/models/business_category.rb +6 -0
- data/test/rails_app/app/models/category.rb +9 -0
- data/test/rails_app/app/models/customer.rb +7 -0
- data/test/rails_app/app/models/user.rb +8 -0
- data/test/rails_app/app/views/layouts/application.html.erb +14 -0
- data/test/rails_app/config/application.rb +16 -0
- data/test/rails_app/config/boot.rb +13 -0
- data/test/rails_app/config/database.yml +7 -0
- data/test/rails_app/config/environment.rb +5 -0
- data/test/rails_app/config/environments/development.rb +26 -0
- data/test/rails_app/config/environments/production.rb +49 -0
- data/test/rails_app/config/environments/test.rb +35 -0
- data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
- data/test/rails_app/config/initializers/inflections.rb +10 -0
- data/test/rails_app/config/initializers/mime_types.rb +5 -0
- data/test/rails_app/config/initializers/secret_token.rb +7 -0
- data/test/rails_app/config/initializers/session_store.rb +8 -0
- data/test/rails_app/config/locales/en.yml +5 -0
- data/test/rails_app/config/routes.rb +58 -0
- data/test/rails_app/config.ru +4 -0
- data/test/rails_app/db/schema.rb +82 -0
- data/test/rails_app/db/seeds.rb +7 -0
- data/test/rails_app/lib/tasks/.gitkeep +0 -0
- data/test/rails_app/public/404.html +26 -0
- data/test/rails_app/public/422.html +26 -0
- data/test/rails_app/public/500.html +26 -0
- data/test/rails_app/script/rails +6 -0
- data/test/rails_app/test/performance/browsing_test.rb +9 -0
- data/test/rails_app/test/test_helper.rb +13 -0
- data/test/rails_app/vendor/plugins/.gitkeep +0 -0
- data/test/test_data_schema.rb +85 -0
- data/test/test_migration_generator.rb +114 -0
- data/test/verified_output/migrations/business_id.rb +10 -0
- data/test/verified_output/migrations/create_business_categories.rb +14 -0
- data/test/verified_output/migrations/create_businesses.rb +28 -0
- data/test/verified_output/migrations/create_categories.rb +13 -0
- data/test/verified_output/migrations/create_reviews.rb +17 -0
- data/test/verified_output/migrations/create_users.rb +18 -0
- data/test/verified_output/migrations/estimated_value_notes.rb +11 -0
- data/test/verified_output/migrations/landline.rb +9 -0
- metadata +270 -0
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
pkg
|
20
|
+
|
21
|
+
## PROJECT::SPECIFIC
|
22
|
+
tmp/*
|
23
|
+
test/rails_app/db/migrate/*.rb
|
24
|
+
test/rails_app/db/schema.rb
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Pascal Houliston
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
= Migrant
|
2
|
+
|
3
|
+
== Summary
|
4
|
+
|
5
|
+
Migrant gives you a super-clean DSL to describe your ActiveRecord models (somewhat similar to DataMapper)
|
6
|
+
and generates all your migrations for you so you can spend more time coding the stuff that counts!
|
7
|
+
|
8
|
+
You'll also get a handy .mock method to instantiate a filled in model for testing or debugging purposes.
|
9
|
+
|
10
|
+
== Getting Started
|
11
|
+
|
12
|
+
Only for Rails 3 and Ruby 1.9+ folks. Simply install the gem "migrant", or add it to your bundler Gemfile
|
13
|
+
|
14
|
+
== Jumping right in
|
15
|
+
|
16
|
+
Start by creating some models with the structure you need:
|
17
|
+
|
18
|
+
class Business < ActiveRecord::Base
|
19
|
+
belongs_to :user
|
20
|
+
|
21
|
+
# Here's where you describe the columns in your model
|
22
|
+
# You can provide a symbol (like a migration) or better still, give an example
|
23
|
+
# of the data that will go in there
|
24
|
+
|
25
|
+
structure do
|
26
|
+
name "The kernel's favourite fried chickens"
|
27
|
+
website "http://www.google.co.za/"
|
28
|
+
address ["11 Test Drive", "Gardens", "Cape Town" ,"South Africa"].join("\n")
|
29
|
+
date_established Time.now - 300.years
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
And another for good measure:
|
34
|
+
|
35
|
+
class User < ActiveRecord::Base
|
36
|
+
has_many :businesses
|
37
|
+
|
38
|
+
structure do
|
39
|
+
name "John"
|
40
|
+
surname "Smith"
|
41
|
+
description :string
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Now, to get your database up to date simply run:
|
46
|
+
|
47
|
+
> rake db:upgrade
|
48
|
+
|
49
|
+
Wrote db/migrate/20101028192913_create_businesses.rb...
|
50
|
+
Wrote db/migrate/20101028192916_create_users.rb...
|
51
|
+
|
52
|
+
OR, if you'd prefer to look over the migrations yourself first, run:
|
53
|
+
|
54
|
+
> rails generate migrations
|
55
|
+
|
56
|
+
Result:
|
57
|
+
|
58
|
+
irb(main):001:0> Business
|
59
|
+
=> Business(id: integer, user_id: integer, name: string, website: string, address: text, date_established: datetime)
|
60
|
+
|
61
|
+
irb(main):002:0> Awesome!!!!
|
62
|
+
NoMethodError: undefined method `Awesome!!!!' for main:Object
|
63
|
+
|
64
|
+
== Want more examples?
|
65
|
+
|
66
|
+
Check out the test models in test/rails_app/app/models/*
|
67
|
+
|
68
|
+
== What will happen seamlessly
|
69
|
+
|
70
|
+
* Creating tables or adding columns (as appropriate)
|
71
|
+
* Adding indexes (happens on foreign keys automatically)
|
72
|
+
* Changing column types
|
73
|
+
* Rollbacks for all the above
|
74
|
+
|
75
|
+
== What won't
|
76
|
+
|
77
|
+
These actions won't be performed (because we don't want to hurt your data):
|
78
|
+
|
79
|
+
* Remove tables/columns
|
80
|
+
* Changing column types where data loss may occur (e.g. varchar -> int)
|
81
|
+
|
82
|
+
== Getting a mock of your model
|
83
|
+
|
84
|
+
> rails console
|
85
|
+
|
86
|
+
irb(main):002:0> my_business = Business.mock
|
87
|
+
=> #<Business id: nil, name: "The Kernel's favourite fried chickens", website: "http://www.google.co.za/",
|
88
|
+
address: "11 Test Drive\nGardens\nCape Town\nSouth Africa", date_established: "1710-10-28 21:03:31">
|
89
|
+
|
90
|
+
irb(main):003:0> my_business.user
|
91
|
+
=> #<User id: nil, name: "John", surname: "Smith", description: "Some string">
|
92
|
+
|
93
|
+
== License
|
94
|
+
|
95
|
+
Copyright (c) 2010 Pascal Houliston
|
96
|
+
|
97
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
98
|
+
a copy of this software and associated documentation files (the
|
99
|
+
"Software"), to deal in the Software without restriction, including
|
100
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
101
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
102
|
+
permit persons to whom the Software is furnished to do so, subject to
|
103
|
+
the following conditions:
|
104
|
+
|
105
|
+
The above copyright notice and this permission notice shall be
|
106
|
+
included in all copies or substantial portions of the Software.
|
107
|
+
|
108
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
109
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
110
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
111
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
112
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
113
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
114
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
115
|
+
|
116
|
+
== Development
|
117
|
+
|
118
|
+
Please be sure to install all the development dependencies from the gemspec, then to run tests do:
|
119
|
+
|
120
|
+
> rake test
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "migrant"
|
8
|
+
gem.summary = %Q{All the fun of ActiveRecord, without writing your migrations, and a dash of mocking.}
|
9
|
+
gem.description = %Q{Migrant gives you a super-clean DSL to describe your ActiveRecord models (somewhat similar to DataMapper) and generates all your migrations for you so you can spend more time coding the stuff that counts!}
|
10
|
+
gem.email = "101pascal@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/pascalh1011/migrant"
|
12
|
+
gem.authors = ["Pascal Houliston"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
gem.add_development_dependency "ansi", "= 1.2.2"
|
15
|
+
gem.add_development_dependency "turn", "= 0.8.1"
|
16
|
+
gem.add_development_dependency "sqlite3-ruby", ">= 0"
|
17
|
+
gem.add_development_dependency "simplecov", ">= 0.3.5"
|
18
|
+
gem.add_dependency "activerecord", ">= 3.0.0"
|
19
|
+
gem.add_dependency "activesupport", ">= 3.0.0"
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
Rake::TestTask.new(:test) do |test|
|
29
|
+
test.libs << 'lib' << 'test'
|
30
|
+
test.pattern = 'test/**/test_*.rb'
|
31
|
+
test.verbose = true
|
32
|
+
end
|
33
|
+
|
34
|
+
task :test => :check_dependencies
|
35
|
+
|
36
|
+
task :default => :test
|
37
|
+
|
38
|
+
require 'rake/rdoctask'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "migrant #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module DataType
|
2
|
+
class DangerousMigration < Exception; end;
|
3
|
+
|
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
|
+
@field = options.delete(:field)
|
13
|
+
@aliases = options.delete(:was) || Array.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Default is 'ye good ol varchar(255)
|
17
|
+
def column
|
18
|
+
{:type => :string}.merge(@options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def mock
|
22
|
+
@value || self.class.default_mock
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.default_mock
|
26
|
+
"Some string"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Decides if and how a column will be changed
|
30
|
+
# Provide the details of a previously column, or simply nil to create a new column
|
31
|
+
def structure_changes_from(current_structure = nil)
|
32
|
+
new_structure = column
|
33
|
+
|
34
|
+
if current_structure
|
35
|
+
# General RDBMS data loss scenarios
|
36
|
+
raise DataType::DangerousMigration if (new_structure[:type] != :text && [:string, :text].include?(current_structure[:type]) && new_structure[:type] != current_structure[:type])
|
37
|
+
|
38
|
+
if new_structure[:limit] && current_structure[:limit].to_i != new_structure[:limit].to_i ||
|
39
|
+
new_structure[:default] && current_structure[:default].to_s != new_structure[:default].to_s ||
|
40
|
+
new_structure[:type] != current_structure[:type]
|
41
|
+
column
|
42
|
+
else
|
43
|
+
nil # No changes
|
44
|
+
end
|
45
|
+
else
|
46
|
+
column
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.migrant_data_type?; true; end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# And all the data types we offer...
|
55
|
+
require 'datatype/boolean'
|
56
|
+
require 'datatype/currency'
|
57
|
+
require 'datatype/date'
|
58
|
+
require 'datatype/float'
|
59
|
+
require 'datatype/foreign_key'
|
60
|
+
require 'datatype/hash'
|
61
|
+
require 'datatype/polymorphic'
|
62
|
+
require 'datatype/range'
|
63
|
+
require 'datatype/string'
|
64
|
+
require 'datatype/symbol'
|
65
|
+
require 'datatype/time'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module DataType
|
2
|
+
class String < Base
|
3
|
+
def initialize(options)
|
4
|
+
super(options)
|
5
|
+
@value ||= ''
|
6
|
+
end
|
7
|
+
|
8
|
+
def column
|
9
|
+
if @value.match(/[\d,]+\.\d{2}$/)
|
10
|
+
return Currency.new(@options).column
|
11
|
+
else
|
12
|
+
return @value.match(/[\r\n\t]/)? { :type => :text }.merge(@options) : super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def mock
|
17
|
+
@value || ((self.column[:type] == :text)? %W{Several lines of long text.}.join("\n") : "Some string")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Migrant
|
2
|
+
class MigrationGenerator
|
3
|
+
TABS = ' ' # Tabs to spaces * 2
|
4
|
+
NEWLINE = "\n "
|
5
|
+
def run
|
6
|
+
migrator = ActiveRecord::Migrator.new(:up, migrations_path)
|
7
|
+
|
8
|
+
unless migrator.pending_migrations.blank?
|
9
|
+
puts "You have some pending database migrations. You can either:\n1. Run them with rake db:migrate\n2. Delete them, in which case this task will probably recreate their actions (DON'T do this if they've been in SCM)."
|
10
|
+
return false
|
11
|
+
end
|
12
|
+
|
13
|
+
# Get all tables and compare to the desired schema
|
14
|
+
# The next line is an evil hack to recursively load all model files in app/models
|
15
|
+
# This needs to be done because Rails normally lazy-loads these files, resulting a blank descendants list of AR::Base
|
16
|
+
Dir["#{Rails.root.to_s}/app/models/**/*.rb"].each { |f| load(f) } if ActiveRecord::Base.descendants.blank?
|
17
|
+
|
18
|
+
ActiveRecord::Base.descendants.each do |model|
|
19
|
+
next if model.schema.nil? || !model.schema.requires_migration? # Skips inherited schemas (such as models with STI)
|
20
|
+
model.reset_column_information # db:migrate doesn't do this
|
21
|
+
model_schema = model.schema.column_migrations
|
22
|
+
|
23
|
+
if model.table_exists?
|
24
|
+
# Structure ActiveRecord::Base's column information so we can compare it directly to the schema
|
25
|
+
db_schema = Hash[*model.columns.collect {|c| [c.name.to_sym, Hash[*[:type, :limit].map { |type| [type, c.send(type)] }.flatten] ] }.flatten]
|
26
|
+
changes = model.schema.columns.collect do |name, data_type|
|
27
|
+
begin
|
28
|
+
[name, data_type.structure_changes_from(db_schema[name])]
|
29
|
+
rescue DataType::DangerousMigration
|
30
|
+
puts "Cannot generate migration automatically for #{model.table_name}, this would involve possible data loss on column: #{name}\nOld structure: #{db_schema[name].inspect}. New structure: #{data_type.column.inspect}\nPlease create and run this migration yourself (with the appropriate data integrity checks)"
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
end.reject { |change| change[1].nil? }
|
34
|
+
next if changes.blank?
|
35
|
+
activity = model.table_name+'_modify_fields_'+changes.collect { |field, options| field.to_s }.join('_')
|
36
|
+
|
37
|
+
up_code = changes.collect do |field, options|
|
38
|
+
type = options.delete(:type)
|
39
|
+
arguments = (options.blank?)? "" : ", #{options.inspect[1..-2]}"
|
40
|
+
|
41
|
+
if db_schema[field]
|
42
|
+
"change_column :#{model.table_name}, :#{field}, :#{type}#{arguments}"
|
43
|
+
else
|
44
|
+
"add_column :#{model.table_name}, :#{field}, :#{type}#{arguments}"
|
45
|
+
end
|
46
|
+
end.join(NEWLINE+TABS)
|
47
|
+
|
48
|
+
down_code = changes.collect do |field, options|
|
49
|
+
if db_schema[field]
|
50
|
+
type = db_schema[field].delete(:type)
|
51
|
+
arguments = (db_schema[field].blank?)? "" : ", #{db_schema[field].inspect[1..-2]}"
|
52
|
+
"change_column :#{model.table_name}, :#{field}, :#{type}#{arguments}"
|
53
|
+
else
|
54
|
+
"remove_column :#{model.table_name}, :#{field}"
|
55
|
+
end
|
56
|
+
end.join(NEWLINE+TABS)
|
57
|
+
|
58
|
+
# For adapters that can report indexes, add as necessary
|
59
|
+
if ActiveRecord::Base.connection.respond_to?(:indexes)
|
60
|
+
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) }
|
61
|
+
up_code += model.schema.indexes.uniq.collect do |index|
|
62
|
+
unless current_indexes.include?(index)
|
63
|
+
NEWLINE+TABS+"add_index :#{model.table_name}, #{index.inspect}"
|
64
|
+
end
|
65
|
+
end.compact.join
|
66
|
+
end
|
67
|
+
else
|
68
|
+
activity = "create_#{model.table_name}"
|
69
|
+
up_code = "create_table :#{model.table_name} do |t|"+NEWLINE+model_schema.collect do |field, options|
|
70
|
+
type = options.delete(:type)
|
71
|
+
options.delete(:was) # Aliases not relevant when creating a new table
|
72
|
+
arguments = (options.blank?)? "" : ", #{options.inspect[1..-2]}"
|
73
|
+
(TABS*2)+"t.#{type} :#{field}#{arguments}"
|
74
|
+
end.join(NEWLINE)+NEWLINE+TABS+"end"
|
75
|
+
|
76
|
+
down_code = "drop_table :#{model.table_name}"
|
77
|
+
up_code += NEWLINE+TABS+model.schema.indexes.collect { |fields| "add_index :#{model.table_name}, #{fields.inspect}"}.join(NEWLINE+TABS)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Indexes
|
81
|
+
# down_code += NEWLINE+TABS+model.schema.indexes.collect { |fields| "remove_index :#{model.table_name}, #{fields.inspect}"}.join(NEWLINE+TABS)
|
82
|
+
filename = "#{migrations_path}/#{next_migration_number}_#{activity}.rb"
|
83
|
+
File.open(filename, 'w') { |migration| migration.write(migration_template(activity, up_code, down_code)) }
|
84
|
+
puts "Wrote #{filename}..."
|
85
|
+
end
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
def migrations_path
|
91
|
+
Rails.root.join(ActiveRecord::Migrator.migrations_path)
|
92
|
+
end
|
93
|
+
|
94
|
+
# See ActiveRecord::Generators::Migration
|
95
|
+
# Only generating a migration to each second is a problem.. because we generate everything in the same second
|
96
|
+
# So we have to add further "pretend" seconds. This WILL cause problems.
|
97
|
+
# TODO: Patch ActiveRecord to end this nonsense.
|
98
|
+
def next_migration_number #:nodoc:
|
99
|
+
highest = Dir.glob(migrations_path.to_s+"/[0-9]*_*.rb").collect do |file|
|
100
|
+
File.basename(file).split("_").first.to_i
|
101
|
+
end.max
|
102
|
+
|
103
|
+
if ActiveRecord::Base.timestamped_migrations
|
104
|
+
base = Time.now.utc.strftime("%Y%m%d%H%M%S").to_s
|
105
|
+
(highest.to_i >= base.to_i)? (highest + 1).to_s : base
|
106
|
+
else
|
107
|
+
(highest.to_i + 1).to_s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def migration_template(activity, up_code, down_code)
|
112
|
+
"class #{activity.camelize.gsub(/\s/, '')} < ActiveRecord::Migration
|
113
|
+
def self.up
|
114
|
+
#{up_code}
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.down
|
118
|
+
#{down_code}
|
119
|
+
end
|
120
|
+
end"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Migrant
|
2
|
+
module ModelExtensions
|
3
|
+
attr_accessor :schema
|
4
|
+
def structure(&block)
|
5
|
+
# Using instance_*evil* to get the neater DSL on the models.
|
6
|
+
# So, my_field in the structure block actually calls Migrant::Schema.my_field
|
7
|
+
|
8
|
+
if self.superclass == ActiveRecord::Base
|
9
|
+
@schema ||= Schema.new
|
10
|
+
@schema.add_associations(self.reflect_on_all_associations)
|
11
|
+
@schema.define_structure(&block)
|
12
|
+
else
|
13
|
+
self.superclass.structure(&block) # For STI, cascade all fields onto the parent model
|
14
|
+
@schema = InheritedSchema.new(self.superclass.schema)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Same as defining a structure block, but with no attributes besides
|
19
|
+
# relationships (such as in a many-to-many)
|
20
|
+
def no_structure
|
21
|
+
structure {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def mock(recursive=true)
|
25
|
+
attribs = @schema.columns.reject { |column| column.is_a?(DataType::ForeignKey)}.collect { |name, data_type| [name, data_type.mock] }.flatten
|
26
|
+
# Only recurse to one level, otherwise things get way too complicated
|
27
|
+
if recursive
|
28
|
+
attribs += self.reflect_on_all_associations(:belongs_to).collect do |association|
|
29
|
+
begin
|
30
|
+
(association.klass.respond_to?(:mock))? [association.name, association.klass.mock(false)] : nil
|
31
|
+
rescue NameError; nil; end # User hasn't defined association, just skip it
|
32
|
+
end.compact.flatten
|
33
|
+
end
|
34
|
+
new Hash[*attribs]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,91 @@
|
|
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, :methods_to_alias
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@columns = Hash.new
|
16
|
+
@indexes = Array.new
|
17
|
+
@methods_to_alias = Array.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_structure(&block)
|
21
|
+
# Runs method_missing on columns given in the model "structure" DSL
|
22
|
+
self.instance_eval(&block) if block_given?
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_associations(associations)
|
26
|
+
associations.each do |association|
|
27
|
+
field = association.options[:foreign_key] || (association.name.to_s+'_id').to_sym
|
28
|
+
case association.macro
|
29
|
+
when :belongs_to
|
30
|
+
if association.options[:polymorphic]
|
31
|
+
@columns[(association.name.to_s+'_type').to_sym] = DataType::Polymorphic.new(:field => field)
|
32
|
+
@indexes << [(association.name.to_s+'_type').to_sym, field]
|
33
|
+
end
|
34
|
+
@columns[field] = DataType::ForeignKey.new(:field => field)
|
35
|
+
@indexes << (association.name.to_s+'_id').to_sym
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def requires_migration?
|
41
|
+
!(@columns.blank? && @indexes.blank?)
|
42
|
+
end
|
43
|
+
|
44
|
+
def column_migrations
|
45
|
+
@columns.collect {|field, data| [field, data.column] } # All that needs to be migrated
|
46
|
+
end
|
47
|
+
|
48
|
+
# This is where we decide what the best schema is based on the structure requirements
|
49
|
+
# The output of this is essentially a formatted schema hash that is processed
|
50
|
+
# on each model by Migrant::MigrationGenerator
|
51
|
+
def method_missing(*args, &block)
|
52
|
+
field = args.slice!(0)
|
53
|
+
data_type = (args.first.nil?)? DataType::String : args.slice!(0)
|
54
|
+
options = args.extract_options!
|
55
|
+
|
56
|
+
# Add index if explicitly asked
|
57
|
+
@indexes << field if options.delete(:index) || data_type.class.to_s == 'Hash' && data_type.delete(:index)
|
58
|
+
options.merge!(:field => field)
|
59
|
+
|
60
|
+
# Matches: description DataType::Paragraph, :index => true
|
61
|
+
if data_type.is_a?(Class) && data_type.respond_to?(:migrant_data_type?)
|
62
|
+
@columns[field] = data_type.new(options)
|
63
|
+
# Matches: description :index => true, :unique => true
|
64
|
+
else
|
65
|
+
begin
|
66
|
+
# Eg. "My field" -> String -> DataType::String
|
67
|
+
@columns[field] = "DataType::#{data_type.class.to_s}".constantize.new(options.merge(:value => data_type))
|
68
|
+
rescue NameError
|
69
|
+
# We don't have a matching type, throw a warning and default to string
|
70
|
+
puts "MIGRATION WARNING: No migration implementation for class #{data_type.class.to_s} on field '#{field}', defaulting to string..."
|
71
|
+
@columns[field] = DataType::Base.new(options)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
puts [":#{field}", "#{@columns[field].class}", "#{options.inspect}"].collect { |s| s.ljust(25) }.join if ENV['DEBUG']
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class InheritedSchema < Schema
|
79
|
+
attr_accessor :parent_schema
|
80
|
+
|
81
|
+
def initialize(parent_schema)
|
82
|
+
@parent_schema = parent_schema
|
83
|
+
@columns = Array.new
|
84
|
+
@indexes = Array.new
|
85
|
+
end
|
86
|
+
|
87
|
+
def requires_migration?
|
88
|
+
false # All added to base table
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|