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.
Files changed (73) hide show
  1. data/.gitignore +24 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +124 -0
  4. data/Rakefile +46 -0
  5. data/VERSION +1 -0
  6. data/lib/datatype/base.rb +65 -0
  7. data/lib/datatype/boolean.rb +15 -0
  8. data/lib/datatype/currency.rb +11 -0
  9. data/lib/datatype/date.rb +13 -0
  10. data/lib/datatype/float.rb +11 -0
  11. data/lib/datatype/foreign_key.rb +9 -0
  12. data/lib/datatype/hash.rb +10 -0
  13. data/lib/datatype/polymorphic.rb +8 -0
  14. data/lib/datatype/range.rb +13 -0
  15. data/lib/datatype/string.rb +22 -0
  16. data/lib/datatype/symbol.rb +10 -0
  17. data/lib/datatype/time.rb +5 -0
  18. data/lib/migrant/migration_generator.rb +123 -0
  19. data/lib/migrant/model_extensions.rb +37 -0
  20. data/lib/migrant/schema.rb +91 -0
  21. data/lib/migrant.rb +16 -0
  22. data/lib/railtie.rb +11 -0
  23. data/lib/tasks/db.rake +11 -0
  24. data/migrant.gemspec +167 -0
  25. data/test/additional_models/review.rb +10 -0
  26. data/test/helper.rb +38 -0
  27. data/test/rails_app/.gitignore +4 -0
  28. data/test/rails_app/README +256 -0
  29. data/test/rails_app/Rakefile +7 -0
  30. data/test/rails_app/app/controllers/application_controller.rb +3 -0
  31. data/test/rails_app/app/helpers/application_helper.rb +2 -0
  32. data/test/rails_app/app/models/business.rb +22 -0
  33. data/test/rails_app/app/models/business_category.rb +6 -0
  34. data/test/rails_app/app/models/category.rb +9 -0
  35. data/test/rails_app/app/models/customer.rb +7 -0
  36. data/test/rails_app/app/models/user.rb +8 -0
  37. data/test/rails_app/app/views/layouts/application.html.erb +14 -0
  38. data/test/rails_app/config/application.rb +16 -0
  39. data/test/rails_app/config/boot.rb +13 -0
  40. data/test/rails_app/config/database.yml +7 -0
  41. data/test/rails_app/config/environment.rb +5 -0
  42. data/test/rails_app/config/environments/development.rb +26 -0
  43. data/test/rails_app/config/environments/production.rb +49 -0
  44. data/test/rails_app/config/environments/test.rb +35 -0
  45. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  46. data/test/rails_app/config/initializers/inflections.rb +10 -0
  47. data/test/rails_app/config/initializers/mime_types.rb +5 -0
  48. data/test/rails_app/config/initializers/secret_token.rb +7 -0
  49. data/test/rails_app/config/initializers/session_store.rb +8 -0
  50. data/test/rails_app/config/locales/en.yml +5 -0
  51. data/test/rails_app/config/routes.rb +58 -0
  52. data/test/rails_app/config.ru +4 -0
  53. data/test/rails_app/db/schema.rb +82 -0
  54. data/test/rails_app/db/seeds.rb +7 -0
  55. data/test/rails_app/lib/tasks/.gitkeep +0 -0
  56. data/test/rails_app/public/404.html +26 -0
  57. data/test/rails_app/public/422.html +26 -0
  58. data/test/rails_app/public/500.html +26 -0
  59. data/test/rails_app/script/rails +6 -0
  60. data/test/rails_app/test/performance/browsing_test.rb +9 -0
  61. data/test/rails_app/test/test_helper.rb +13 -0
  62. data/test/rails_app/vendor/plugins/.gitkeep +0 -0
  63. data/test/test_data_schema.rb +85 -0
  64. data/test/test_migration_generator.rb +114 -0
  65. data/test/verified_output/migrations/business_id.rb +10 -0
  66. data/test/verified_output/migrations/create_business_categories.rb +14 -0
  67. data/test/verified_output/migrations/create_businesses.rb +28 -0
  68. data/test/verified_output/migrations/create_categories.rb +13 -0
  69. data/test/verified_output/migrations/create_reviews.rb +17 -0
  70. data/test/verified_output/migrations/create_users.rb +18 -0
  71. data/test/verified_output/migrations/estimated_value_notes.rb +11 -0
  72. data/test/verified_output/migrations/landline.rb +9 -0
  73. 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,15 @@
1
+ module DataType
2
+ class TrueClass < Base
3
+ def column
4
+ {:type => :boolean}
5
+ end
6
+
7
+ def self.default_mock
8
+ true
9
+ end
10
+ end
11
+
12
+ class FalseClass < TrueClass; end;
13
+ end
14
+
15
+
@@ -0,0 +1,11 @@
1
+ module DataType
2
+ class Currency < Base
3
+ def column
4
+ {:type => :decimal, :precision => 10, :scale => 2}
5
+ end
6
+
7
+ def self.default_mock
8
+ rand(9999999).to_f+0.51
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module DataType
2
+ class Date < Base
3
+ def column
4
+ {:type => :datetime}
5
+ end
6
+
7
+ def self.default_mock
8
+ ::Time.now
9
+ end
10
+ end
11
+ end
12
+
13
+
@@ -0,0 +1,11 @@
1
+ module DataType
2
+ class Float < Base
3
+ def column
4
+ {:type => :float}
5
+ end
6
+
7
+ def self.default_mock
8
+ rand(100).to_f-55.0
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module DataType
2
+ class ForeignKey < Base
3
+ def column
4
+ {:type => :integer}
5
+ end
6
+ end
7
+ end
8
+
9
+
@@ -0,0 +1,10 @@
1
+ module DataType
2
+ class Hash < Base
3
+ def column
4
+ @options = @value # Assign developer's options verbatim
5
+ super
6
+ end
7
+ end
8
+ end
9
+
10
+
@@ -0,0 +1,8 @@
1
+ module DataType
2
+ class Polymorphic < Base
3
+ def mock
4
+ # Eek, can't mock an unknown type
5
+ nil
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ module DataType
2
+ class Range < Base
3
+ def column
4
+ definition = {:type => :integer}
5
+ definition[:limit] = @value.max.to_s.length if @value.respond_to?(:max)
6
+ definition
7
+ end
8
+
9
+ def self.default_mock
10
+ 0..100
11
+ end
12
+ end
13
+ end
@@ -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,10 @@
1
+ module DataType
2
+ class Symbol < Base
3
+ def column
4
+ # Just construct whatever the user wants
5
+ {:type => @value || :string }.merge(@options)
6
+ end
7
+ end
8
+ end
9
+
10
+
@@ -0,0 +1,5 @@
1
+ module DataType
2
+ class Time < Date
3
+ # No different to date type
4
+ end
5
+ end
@@ -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