declare_schema 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dependabot/config.yml +10 -0
- data/.github/workflows/gem_release.yml +38 -0
- data/.gitignore +14 -0
- data/.jenkins/Jenkinsfile +72 -0
- data/.jenkins/ruby_build_pod.yml +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +189 -0
- data/.ruby-version +1 -0
- data/Appraisals +14 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +203 -0
- data/LICENSE.txt +22 -0
- data/README.md +11 -0
- data/Rakefile +56 -0
- data/bin/declare_schema +11 -0
- data/declare_schema.gemspec +25 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_4.gemfile +25 -0
- data/gemfiles/rails_5.gemfile +25 -0
- data/gemfiles/rails_6.gemfile +25 -0
- data/lib/declare_schema.rb +44 -0
- data/lib/declare_schema/command.rb +65 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
- data/lib/declare_schema/extensions/module.rb +36 -0
- data/lib/declare_schema/field_declaration_dsl.rb +40 -0
- data/lib/declare_schema/model.rb +242 -0
- data/lib/declare_schema/model/field_spec.rb +162 -0
- data/lib/declare_schema/model/index_spec.rb +175 -0
- data/lib/declare_schema/railtie.rb +12 -0
- data/lib/declare_schema/version.rb +5 -0
- data/lib/generators/declare_schema/migration/USAGE +47 -0
- data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
- data/lib/generators/declare_schema/migration/migrator.rb +567 -0
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
- data/lib/generators/declare_schema/model/USAGE +19 -0
- data/lib/generators/declare_schema/model/model_generator.rb +12 -0
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
- data/lib/generators/declare_schema/support/eval_template.rb +21 -0
- data/lib/generators/declare_schema/support/model.rb +64 -0
- data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
- data/spec/spec_helper.rb +28 -0
- data/test/api.rdoctest +136 -0
- data/test/doc-only.rdoctest +76 -0
- data/test/generators.rdoctest +60 -0
- data/test/interactive_primary_key.rdoctest +56 -0
- data/test/migration_generator.rdoctest +846 -0
- data/test/migration_generator_comments.rdoctestDISABLED +74 -0
- data/test/prepare_testapp.rb +15 -0
- data/test_responses.txt +2 -0
- metadata +109 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclareSchema
|
4
|
+
module Model
|
5
|
+
class FieldSpec
|
6
|
+
class UnknownSqlTypeError < RuntimeError; end
|
7
|
+
|
8
|
+
MYSQL_TINYTEXT_LIMIT = 0xff
|
9
|
+
MYSQL_TEXT_LIMIT = 0xffff
|
10
|
+
MYSQL_MEDIUMTEXT_LIMIT = 0xff_ffff
|
11
|
+
MYSQL_LONGTEXT_LIMIT = 0xffff_ffff
|
12
|
+
|
13
|
+
MYSQL_TEXT_LIMITS_ASCENDING = [MYSQL_TINYTEXT_LIMIT, MYSQL_TEXT_LIMIT, MYSQL_MEDIUMTEXT_LIMIT, MYSQL_LONGTEXT_LIMIT].freeze
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# method for easy stubbing in tests
|
17
|
+
def mysql_text_limits?
|
18
|
+
if defined?(@mysql_text_limits)
|
19
|
+
@mysql_text_limits
|
20
|
+
else
|
21
|
+
@mysql_text_limits = ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def round_up_mysql_text_limit(limit)
|
26
|
+
MYSQL_TEXT_LIMITS_ASCENDING.find do |mysql_supported_text_limit|
|
27
|
+
if limit <= mysql_supported_text_limit
|
28
|
+
mysql_supported_text_limit
|
29
|
+
end
|
30
|
+
end or raise ArgumentError, "limit of #{limit} is too large for MySQL"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :model, :name, :type, :position, :options
|
35
|
+
|
36
|
+
def initialize(model, name, type, options = {})
|
37
|
+
# Invoca change - searching for the primary key was causing an additional database read on every model load. Assume
|
38
|
+
# "id" which works for invoca.
|
39
|
+
# raise ArgumentError, "you cannot provide a field spec for the primary key" if name == model.primary_key
|
40
|
+
name == "id" and raise ArgumentError, "you cannot provide a field spec for the primary key"
|
41
|
+
|
42
|
+
@model = model
|
43
|
+
@name = name.to_sym
|
44
|
+
@type = type.is_a?(String) ? type.to_sym : type
|
45
|
+
position_option = options.delete(:position)
|
46
|
+
@options = options
|
47
|
+
|
48
|
+
case type
|
49
|
+
when :text
|
50
|
+
@options[:default] and raise "default may not be given for :text field #{model}##{@name}"
|
51
|
+
if self.class.mysql_text_limits?
|
52
|
+
@options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit] || MYSQL_LONGTEXT_LIMIT)
|
53
|
+
end
|
54
|
+
when :string
|
55
|
+
@options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want 255?"
|
56
|
+
end
|
57
|
+
@position = position_option || model.field_specs.length
|
58
|
+
end
|
59
|
+
|
60
|
+
TYPE_SYNONYMS = { timestamp: :datetime }.freeze
|
61
|
+
|
62
|
+
SQLITE_COLUMN_CLASS =
|
63
|
+
begin
|
64
|
+
ActiveRecord::ConnectionAdapters::SQLiteColumn
|
65
|
+
rescue NameError
|
66
|
+
NilClass
|
67
|
+
end
|
68
|
+
|
69
|
+
def sql_type
|
70
|
+
@options[:sql_type] || begin
|
71
|
+
if native_type?(type)
|
72
|
+
type
|
73
|
+
else
|
74
|
+
field_class = DeclareSchema.to_class(type)
|
75
|
+
field_class && field_class::COLUMN_TYPE or raise UnknownSqlTypeError, "#{type.inspect} for #{model}.#{@name}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def sql_options
|
81
|
+
@options.except(:ruby_default, :validates)
|
82
|
+
end
|
83
|
+
|
84
|
+
def limit
|
85
|
+
@options[:limit] || native_types[sql_type][:limit]
|
86
|
+
end
|
87
|
+
|
88
|
+
def precision
|
89
|
+
@options[:precision]
|
90
|
+
end
|
91
|
+
|
92
|
+
def scale
|
93
|
+
@options[:scale]
|
94
|
+
end
|
95
|
+
|
96
|
+
def null
|
97
|
+
!:null.in?(@options) || @options[:null]
|
98
|
+
end
|
99
|
+
|
100
|
+
def default
|
101
|
+
@options[:default]
|
102
|
+
end
|
103
|
+
|
104
|
+
def comment
|
105
|
+
@options[:comment]
|
106
|
+
end
|
107
|
+
|
108
|
+
def same_type?(col_spec)
|
109
|
+
type = sql_type
|
110
|
+
normalized_type = TYPE_SYNONYMS[type] || type
|
111
|
+
normalized_col_spec_type = TYPE_SYNONYMS[col_spec.type] || col_spec.type
|
112
|
+
normalized_type == normalized_col_spec_type
|
113
|
+
end
|
114
|
+
|
115
|
+
def different_to?(col_spec)
|
116
|
+
!same_type?(col_spec) ||
|
117
|
+
# we should be able to use col_spec.comment, but col_spec has
|
118
|
+
# a nil table_name for some strange reason.
|
119
|
+
(model.table_exists? &&
|
120
|
+
ActiveRecord::Base.respond_to?(:column_comment) &&
|
121
|
+
!(col_comment = ActiveRecord::Base.column_comment(col_spec.name, model.table_name)).nil? &&
|
122
|
+
col_comment != comment
|
123
|
+
) ||
|
124
|
+
begin
|
125
|
+
native_type = native_types[type]
|
126
|
+
check_attributes = [:null, :default]
|
127
|
+
check_attributes += [:precision, :scale] if sql_type == :decimal && !col_spec.is_a?(SQLITE_COLUMN_CLASS) # remove when rails fixes https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2872
|
128
|
+
check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
|
129
|
+
check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
|
130
|
+
(sql_type == :text && self.class.mysql_text_limits?)
|
131
|
+
check_attributes.any? do |k|
|
132
|
+
if k == :default
|
133
|
+
case Rails::VERSION::MAJOR
|
134
|
+
when 4
|
135
|
+
col_spec.type_cast_from_database(col_spec.default) != col_spec.type_cast_from_database(default)
|
136
|
+
else
|
137
|
+
cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
|
138
|
+
cast_type.deserialize(col_spec.default) != cast_type.deserialize(default)
|
139
|
+
end
|
140
|
+
else
|
141
|
+
col_value = col_spec.send(k)
|
142
|
+
if col_value.nil? && native_type
|
143
|
+
col_value = native_type[k]
|
144
|
+
end
|
145
|
+
col_value != send(k)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def native_type?(type)
|
154
|
+
type.to_sym != :primary_key && native_types.has_key?(type)
|
155
|
+
end
|
156
|
+
|
157
|
+
def native_types
|
158
|
+
Generators::DeclareSchema::Migration::Migrator.native_types
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclareSchema
|
4
|
+
module Model
|
5
|
+
class IndexSpec
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :table, :fields, :explicit_name, :name, :unique, :where
|
9
|
+
|
10
|
+
class IndexNameTooLongError < RuntimeError; end
|
11
|
+
|
12
|
+
PRIMARY_KEY_NAME = "PRIMARY_KEY"
|
13
|
+
MYSQL_INDEX_NAME_MAX_LENGTH = 64
|
14
|
+
|
15
|
+
def initialize(model, fields, options = {})
|
16
|
+
@model = model
|
17
|
+
@table = options.delete(:table_name) || model.table_name
|
18
|
+
@fields = Array.wrap(fields).map(&:to_s)
|
19
|
+
@explicit_name = options[:name] unless options.delete(:allow_equivalent)
|
20
|
+
@name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
|
21
|
+
@unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
|
22
|
+
|
23
|
+
if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
|
24
|
+
raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
|
25
|
+
end
|
26
|
+
|
27
|
+
if (where = options[:where])
|
28
|
+
@where = where.start_with?('(') ? where : "(#{where})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
# extract IndexSpecs from an existing table
|
34
|
+
def for_model(model, old_table_name = nil)
|
35
|
+
t = old_table_name || model.table_name
|
36
|
+
connection = model.connection.dup
|
37
|
+
# TODO: Change below to use prepend
|
38
|
+
class << connection # defeat Rails code that skips the primary keys by changing their name to PRIMARY_KEY_NAME
|
39
|
+
def each_hash(result)
|
40
|
+
super do |hash|
|
41
|
+
if hash[:Key_name] == "PRIMARY"
|
42
|
+
hash[:Key_name] = PRIMARY_KEY_NAME
|
43
|
+
end
|
44
|
+
yield hash
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
connection.indexes(t).map do |i|
|
49
|
+
new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name) unless model.ignore_indexes.include?(i.name)
|
50
|
+
end.compact
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def primary_key?
|
55
|
+
name == PRIMARY_KEY_NAME
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_add_statement(new_table_name, existing_primary_key = nil)
|
59
|
+
if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
60
|
+
to_add_primary_key_statement(new_table_name, existing_primary_key)
|
61
|
+
else
|
62
|
+
r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
|
63
|
+
r += ", unique: true" if unique
|
64
|
+
r += ", where: '#{where}'" if where.present?
|
65
|
+
r += ", name: '#{name}'"
|
66
|
+
r
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_add_primary_key_statement(new_table_name, existing_primary_key)
|
71
|
+
drop = "DROP PRIMARY KEY, " if existing_primary_key
|
72
|
+
statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
|
73
|
+
"execute #{statement.inspect}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_key
|
77
|
+
@key ||= [table, fields, name, unique, where].map(&:to_s)
|
78
|
+
end
|
79
|
+
|
80
|
+
def settings
|
81
|
+
@settings ||= [table, fields, unique].map(&:to_s)
|
82
|
+
end
|
83
|
+
|
84
|
+
def hash
|
85
|
+
to_key.hash
|
86
|
+
end
|
87
|
+
|
88
|
+
def <=>(rhs)
|
89
|
+
to_key <=> rhs.to_key
|
90
|
+
end
|
91
|
+
|
92
|
+
def equivalent?(rhs)
|
93
|
+
settings == rhs.settings
|
94
|
+
end
|
95
|
+
|
96
|
+
def with_name(new_name)
|
97
|
+
self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
alias eql? ==
|
101
|
+
end
|
102
|
+
|
103
|
+
class ForeignKeySpec
|
104
|
+
include Comparable
|
105
|
+
|
106
|
+
attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
|
107
|
+
|
108
|
+
def initialize(model, foreign_key, options = {})
|
109
|
+
@model = model
|
110
|
+
@foreign_key = foreign_key.presence
|
111
|
+
@options = options
|
112
|
+
|
113
|
+
@child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
|
114
|
+
@parent_table_name = options[:parent_table]
|
115
|
+
@foreign_key_name = options[:foreign_key] || self.foreign_key
|
116
|
+
@index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
|
117
|
+
@constraint_name = options[:constraint_name] || @index_name || ''
|
118
|
+
@on_delete_cascade = options[:dependent] == :delete
|
119
|
+
|
120
|
+
# Empty constraint lets mysql generate the name
|
121
|
+
end
|
122
|
+
|
123
|
+
class << self
|
124
|
+
def for_model(model, old_table_name)
|
125
|
+
show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
|
126
|
+
constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
|
127
|
+
|
128
|
+
constraints.map do |fkc|
|
129
|
+
options = {}
|
130
|
+
name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
|
131
|
+
options[:constraint_name] = name
|
132
|
+
options[:parent_table] = parent_table
|
133
|
+
options[:foreign_key] = foreign_key
|
134
|
+
options[:dependent] = :delete if fkc['ON DELETE CASCADE']
|
135
|
+
|
136
|
+
new(model, foreign_key, options)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def parent_table_name
|
142
|
+
@parent_table_name ||=
|
143
|
+
options[:class_name]&.is_a?(Class) &&
|
144
|
+
options[:class_name].respond_to?(:table_name) &&
|
145
|
+
options[:class_name]&.table_name
|
146
|
+
@parent_table_name ||=
|
147
|
+
options[:class_name]&.constantize &&
|
148
|
+
options[:class_name].constantize.respond_to?(:table_name) &&
|
149
|
+
options[:class_name].constantize.table_name ||
|
150
|
+
foreign_key.gsub(/_id/, '').camelize.constantize.table_name
|
151
|
+
end
|
152
|
+
|
153
|
+
attr_writer :parent_table_name
|
154
|
+
|
155
|
+
def to_add_statement(_ = true)
|
156
|
+
statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
|
157
|
+
"execute #{statement.inspect}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_key
|
161
|
+
@key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
|
162
|
+
end
|
163
|
+
|
164
|
+
def hash
|
165
|
+
to_key.hash
|
166
|
+
end
|
167
|
+
|
168
|
+
def <=>(rhs)
|
169
|
+
to_key <=> rhs.to_key
|
170
|
+
end
|
171
|
+
|
172
|
+
alias eql? ==
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'declare_schema'
|
4
|
+
require 'rails'
|
5
|
+
|
6
|
+
module DeclareSchema
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
ActiveSupport.on_load(:active_record) do
|
9
|
+
require 'declare_schema/extensions/active_record/fields_declaration'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
Description:
|
2
|
+
|
3
|
+
This generator compares your existing schema against the
|
4
|
+
schema declared inside your fields declarations in your
|
5
|
+
models.
|
6
|
+
|
7
|
+
If the generator finds differences, it will display the
|
8
|
+
migration it has created, and ask you if you wish to
|
9
|
+
[g]enerate migration, generate and [m]igrate now or [c]ancel?
|
10
|
+
Enter "g" to just generate the migration but do not run it.
|
11
|
+
Enter "m" to generate the migration and run it, or press "c"
|
12
|
+
to do nothing.
|
13
|
+
|
14
|
+
The generator will then prompt you for the migration name,
|
15
|
+
supplying a numbered default name.
|
16
|
+
|
17
|
+
The generator is conservative and will prompt you to resolve
|
18
|
+
any ambiguities.
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
|
22
|
+
$ rails generate declare_schema:migration
|
23
|
+
|
24
|
+
---------- Up Migration ----------
|
25
|
+
create_table :foos do |t|
|
26
|
+
t.datetime :created_at
|
27
|
+
t.datetime :updated_at
|
28
|
+
end
|
29
|
+
----------------------------------
|
30
|
+
|
31
|
+
---------- Down Migration --------
|
32
|
+
drop_table :foos
|
33
|
+
----------------------------------
|
34
|
+
What now: [g]enerate migration, generate and [m]igrate now or [c]ancel? m
|
35
|
+
|
36
|
+
Migration filename:
|
37
|
+
(you can type spaces instead of '_' -- every little helps)
|
38
|
+
Filename [declare_schema_migration_2]: create_foo
|
39
|
+
exists db/migrate
|
40
|
+
create db/migrate/20091023183838_create_foo.rb
|
41
|
+
(in /work/foo)
|
42
|
+
== CreateFoo: migrating ======================================================
|
43
|
+
-- create_table(:yos)
|
44
|
+
-> 0.0856s
|
45
|
+
== CreateFoo: migrated (0.0858s) =============================================
|
46
|
+
|
47
|
+
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/migration'
|
4
|
+
require 'rails/generators/active_record'
|
5
|
+
require 'generators/declare_schema/support/thor_shell'
|
6
|
+
|
7
|
+
module DeclareSchema
|
8
|
+
class MigrationGenerator < Rails::Generators::Base
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
|
11
|
+
argument :name, type: :string, optional: true
|
12
|
+
|
13
|
+
include Rails::Generators::Migration
|
14
|
+
include DeclareSchema::Support::ThorShell
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# the Rails::Generators::Migration.next_migration_number gives a NotImplementedError
|
18
|
+
# in Rails 3.0.0.beta4, so we need to implement the logic of ActiveRecord.
|
19
|
+
# For other ORMs we will wait for the rails implementation
|
20
|
+
# see http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/a507ce419076cda2
|
21
|
+
def next_migration_number(dirname)
|
22
|
+
ActiveRecord::Generators::Base.next_migration_number(dirname)
|
23
|
+
end
|
24
|
+
|
25
|
+
def banner
|
26
|
+
"rails generate declare_schema:migration #{arguments.map(&:usage).join(' ')} [options]"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class_option :drop,
|
31
|
+
aliases: '-d',
|
32
|
+
type: :boolean,
|
33
|
+
desc: "Don't prompt with 'drop or rename' - just drop everything"
|
34
|
+
|
35
|
+
class_option :default_name,
|
36
|
+
aliases: '-n',
|
37
|
+
type: :boolean,
|
38
|
+
desc: "Don't prompt for a migration name - just pick one"
|
39
|
+
|
40
|
+
class_option :generate,
|
41
|
+
aliases: '-g',
|
42
|
+
type: :boolean,
|
43
|
+
desc: "Don't prompt for action - generate the migration"
|
44
|
+
|
45
|
+
class_option :migrate,
|
46
|
+
aliases: '-m',
|
47
|
+
type: :boolean,
|
48
|
+
desc: "Don't prompt for action - generate and migrate"
|
49
|
+
|
50
|
+
def migrate
|
51
|
+
return if migrations_pending?
|
52
|
+
|
53
|
+
generator = Generators::DeclareSchema::Migration::Migrator.new(->(c, d, k, p) { extract_renames!(c, d, k, p) })
|
54
|
+
up, down = generator.generate
|
55
|
+
|
56
|
+
if up.blank?
|
57
|
+
say "Database and models match -- nothing to change"
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
say "\n---------- Up Migration ----------"
|
62
|
+
say up
|
63
|
+
say "----------------------------------"
|
64
|
+
|
65
|
+
say "\n---------- Down Migration --------"
|
66
|
+
say down
|
67
|
+
say "----------------------------------"
|
68
|
+
|
69
|
+
action = options[:generate] && 'g' ||
|
70
|
+
options[:migrate] && 'm' ||
|
71
|
+
choose("\nWhat now: [g]enerate migration, generate and [m]igrate now or [c]ancel?", /^(g|m|c)$/)
|
72
|
+
|
73
|
+
if action != 'c'
|
74
|
+
if name.blank? && !options[:default_name]
|
75
|
+
final_migration_name = choose("\nMigration filename: [<enter>=#{migration_name}|<custom_name>]:", /^[a-z0-9_ ]*$/, migration_name).strip.gsub(' ', '_')
|
76
|
+
end
|
77
|
+
final_migration_name = migration_name if final_migration_name.blank?
|
78
|
+
|
79
|
+
up.gsub!("\n", "\n ")
|
80
|
+
up.gsub!(/ +\n/, "\n")
|
81
|
+
down.gsub!("\n", "\n ")
|
82
|
+
down.gsub!(/ +\n/, "\n")
|
83
|
+
|
84
|
+
@up = up
|
85
|
+
@down = down
|
86
|
+
@migration_class_name = final_migration_name.camelize
|
87
|
+
|
88
|
+
migration_template 'migration.rb.erb', "db/migrate/#{final_migration_name.underscore}.rb"
|
89
|
+
if action == 'm'
|
90
|
+
case Rails::VERSION::MAJOR
|
91
|
+
when 4
|
92
|
+
rake('db:migrate')
|
93
|
+
else
|
94
|
+
rails_command('db:migrate')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
rescue ::DeclareSchema::Model::FieldSpec::UnknownSqlTypeError => ex
|
99
|
+
say "Invalid field type: #{ex}"
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def migrations_pending?
|
105
|
+
migrations = case Rails::VERSION::MAJOR
|
106
|
+
when 4
|
107
|
+
ActiveRecord::Migrator.migrations('db/migrate')
|
108
|
+
when 5
|
109
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrations
|
110
|
+
else
|
111
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration).migrations
|
112
|
+
end
|
113
|
+
pending_migrations = case Rails::VERSION::MAJOR
|
114
|
+
when 4, 5
|
115
|
+
ActiveRecord::Migrator.new(:up, migrations).pending_migrations
|
116
|
+
else
|
117
|
+
ActiveRecord::Migrator.new(:up, migrations, ActiveRecord::SchemaMigration).pending_migrations
|
118
|
+
end
|
119
|
+
|
120
|
+
if pending_migrations.any?
|
121
|
+
say "You have #{pending_migrations.size} pending migration#{'s' if pending_migrations.size > 1}:"
|
122
|
+
pending_migrations.each do |pending_migration|
|
123
|
+
say format(' %4d %s', pending_migration.version, pending_migration.name)
|
124
|
+
end
|
125
|
+
true
|
126
|
+
else
|
127
|
+
false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def extract_renames!(to_create, to_drop, kind_str, name_prefix = "")
|
132
|
+
to_rename = {}
|
133
|
+
|
134
|
+
unless options[:drop]
|
135
|
+
|
136
|
+
rename_to_choices = to_create
|
137
|
+
to_drop.dup.each do |t|
|
138
|
+
loop do
|
139
|
+
if rename_to_choices.empty?
|
140
|
+
say "\nCONFIRM DROP! #{kind_str} #{name_prefix}#{t}"
|
141
|
+
resp = ask("Enter 'drop #{t}' to confirm or press enter to keep:")
|
142
|
+
if resp.strip == "drop #{t}"
|
143
|
+
break
|
144
|
+
elsif resp.strip.empty?
|
145
|
+
to_drop.delete(t)
|
146
|
+
break
|
147
|
+
else
|
148
|
+
next
|
149
|
+
end
|
150
|
+
else
|
151
|
+
say "\nDROP, RENAME or KEEP?: #{kind_str} #{name_prefix}#{t}"
|
152
|
+
say "Rename choices: #{to_create * ', '}"
|
153
|
+
resp = ask "Enter either 'drop #{t}' or one of the rename choices or press enter to keep:"
|
154
|
+
resp = resp.strip
|
155
|
+
|
156
|
+
if resp == "drop #{t}"
|
157
|
+
# Leave things as they are
|
158
|
+
break
|
159
|
+
else
|
160
|
+
resp.gsub!(' ', '_')
|
161
|
+
to_drop.delete(t)
|
162
|
+
if resp.in?(rename_to_choices)
|
163
|
+
to_rename[t] = resp
|
164
|
+
to_create.delete(resp)
|
165
|
+
rename_to_choices.delete(resp)
|
166
|
+
break
|
167
|
+
elsif resp.empty?
|
168
|
+
break
|
169
|
+
else
|
170
|
+
next
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
to_rename
|
178
|
+
end
|
179
|
+
|
180
|
+
def migration_name
|
181
|
+
name || Generators::DeclareSchema::Migration::Migrator.default_migration_name
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|