migrant 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|