declare_schema 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.
- 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
|