migrant 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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