db_schema 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +522 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/db_schema.gemspec +35 -0
- data/lib/db_schema.rb +125 -0
- data/lib/db_schema/awesome_print.rb +246 -0
- data/lib/db_schema/changes.rb +396 -0
- data/lib/db_schema/configuration.rb +29 -0
- data/lib/db_schema/definitions.rb +122 -0
- data/lib/db_schema/definitions/field.rb +38 -0
- data/lib/db_schema/definitions/field/array.rb +19 -0
- data/lib/db_schema/definitions/field/base.rb +90 -0
- data/lib/db_schema/definitions/field/binary.rb +9 -0
- data/lib/db_schema/definitions/field/bit_string.rb +15 -0
- data/lib/db_schema/definitions/field/boolean.rb +9 -0
- data/lib/db_schema/definitions/field/character.rb +19 -0
- data/lib/db_schema/definitions/field/custom.rb +22 -0
- data/lib/db_schema/definitions/field/datetime.rb +30 -0
- data/lib/db_schema/definitions/field/geometric.rb +33 -0
- data/lib/db_schema/definitions/field/json.rb +13 -0
- data/lib/db_schema/definitions/field/monetary.rb +9 -0
- data/lib/db_schema/definitions/field/network.rb +17 -0
- data/lib/db_schema/definitions/field/numeric.rb +30 -0
- data/lib/db_schema/definitions/field/range.rb +29 -0
- data/lib/db_schema/definitions/field/text_search.rb +13 -0
- data/lib/db_schema/definitions/field/uuid.rb +9 -0
- data/lib/db_schema/dsl.rb +145 -0
- data/lib/db_schema/reader.rb +270 -0
- data/lib/db_schema/runner.rb +220 -0
- data/lib/db_schema/utils.rb +50 -0
- data/lib/db_schema/validator.rb +89 -0
- data/lib/db_schema/version.rb +3 -0
- metadata +239 -0
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/db_schema.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'db_schema/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'db_schema'
|
7
|
+
spec.version = DbSchema::VERSION
|
8
|
+
spec.authors = ['Vsevolod Romashov']
|
9
|
+
spec.email = ['7@7vn.ru']
|
10
|
+
|
11
|
+
spec.summary = 'Declarative database schema definition.'
|
12
|
+
spec.description = 'A database schema management tool that reads a "single-source-of-truth" schema definition from a ruby file and auto-migrates the database to conform to it.'
|
13
|
+
spec.homepage = 'https://github.com/7even/db_schema'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r(^spec/)) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r(^exe/)) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'sequel'
|
22
|
+
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'pry'
|
27
|
+
spec.add_development_dependency 'awesome_print', '~> 1.7'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
30
|
+
spec.add_development_dependency 'guard-rspec'
|
31
|
+
spec.add_development_dependency 'terminal-notifier'
|
32
|
+
spec.add_development_dependency 'terminal-notifier-guard'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'pg'
|
35
|
+
end
|
data/lib/db_schema.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require 'db_schema/configuration'
|
5
|
+
require 'db_schema/utils'
|
6
|
+
require 'db_schema/definitions'
|
7
|
+
require 'db_schema/awesome_print'
|
8
|
+
require 'db_schema/dsl'
|
9
|
+
require 'db_schema/validator'
|
10
|
+
require 'db_schema/reader'
|
11
|
+
require 'db_schema/changes'
|
12
|
+
require 'db_schema/runner'
|
13
|
+
require 'db_schema/version'
|
14
|
+
|
15
|
+
module DbSchema
|
16
|
+
class << self
|
17
|
+
def describe(&block)
|
18
|
+
desired_schema = DSL.new(block).schema
|
19
|
+
validate(desired_schema)
|
20
|
+
|
21
|
+
actual_schema = Reader.read_schema
|
22
|
+
changes = Changes.between(desired_schema, actual_schema)
|
23
|
+
return if changes.empty?
|
24
|
+
|
25
|
+
log_changes(changes) if configuration.log_changes?
|
26
|
+
return if configuration.dry_run?
|
27
|
+
|
28
|
+
Runner.new(changes).run!
|
29
|
+
|
30
|
+
if configuration.post_check_enabled?
|
31
|
+
perform_post_check(desired_schema)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def connection
|
36
|
+
@connection ||= Sequel.connect(
|
37
|
+
adapter: configuration.adapter,
|
38
|
+
host: configuration.host,
|
39
|
+
port: configuration.port,
|
40
|
+
database: configuration.database,
|
41
|
+
user: configuration.user,
|
42
|
+
password: configuration.password
|
43
|
+
).tap do |db|
|
44
|
+
db.extension :pg_enum
|
45
|
+
db.extension :pg_array
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def configure(connection_parameters)
|
50
|
+
@configuration = Configuration.new(connection_parameters)
|
51
|
+
@connection = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def configure_from_yaml(yaml_path, environment, **other_options)
|
55
|
+
data = Utils.symbolize_keys(YAML.load_file(yaml_path))
|
56
|
+
filtered_data = Utils.filter_by_keys(
|
57
|
+
data[environment.to_sym],
|
58
|
+
*%i(adapter host port database username password)
|
59
|
+
)
|
60
|
+
renamed_data = Utils.rename_keys(filtered_data, username: :user)
|
61
|
+
|
62
|
+
configure(renamed_data.merge(other_options))
|
63
|
+
end
|
64
|
+
|
65
|
+
def configuration
|
66
|
+
raise 'You must call DbSchema.configure in order to connect to the database.' if @configuration.nil?
|
67
|
+
|
68
|
+
@configuration
|
69
|
+
end
|
70
|
+
|
71
|
+
def reset!
|
72
|
+
@configuration = nil
|
73
|
+
@connection = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def validate(schema)
|
78
|
+
validation_result = Validator.validate(schema)
|
79
|
+
|
80
|
+
unless validation_result.valid?
|
81
|
+
message = "Requested schema is invalid:\n\n"
|
82
|
+
|
83
|
+
validation_result.errors.each do |error|
|
84
|
+
message << "* #{error}\n"
|
85
|
+
end
|
86
|
+
|
87
|
+
raise InvalidSchemaError, message
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def log_changes(changes)
|
92
|
+
return if changes.empty?
|
93
|
+
|
94
|
+
puts 'DbSchema is applying these changes to the database:'
|
95
|
+
if changes.respond_to?(:ai)
|
96
|
+
puts changes.ai
|
97
|
+
else
|
98
|
+
puts changes.inspect
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def perform_post_check(desired_schema)
|
103
|
+
unapplied_changes = Changes.between(desired_schema, Reader.read_schema)
|
104
|
+
return if unapplied_changes.empty?
|
105
|
+
|
106
|
+
readable_changes = if unapplied_changes.respond_to?(:ai)
|
107
|
+
unapplied_changes.ai
|
108
|
+
else
|
109
|
+
unapplied_changes.inspect
|
110
|
+
end
|
111
|
+
|
112
|
+
message = <<-ERROR
|
113
|
+
Your database still differs from the expected schema after applying it; if you are 100% sure this is ok you can turn these checks off with DbSchema.configure(post_check: false). Here are the differences:
|
114
|
+
|
115
|
+
#{readable_changes}
|
116
|
+
ERROR
|
117
|
+
|
118
|
+
raise SchemaMismatch, message
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class InvalidSchemaError < ArgumentError; end
|
123
|
+
class SchemaMismatch < RuntimeError; end
|
124
|
+
class UnsupportedOperation < ArgumentError; end
|
125
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
begin
|
2
|
+
require 'awesome_print'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
if defined?(AwesomePrint)
|
7
|
+
module AwesomePrint
|
8
|
+
module DbSchema
|
9
|
+
def self.included(base)
|
10
|
+
base.send :alias_method, :cast_without_dbschema, :cast
|
11
|
+
base.send :alias_method, :cast, :cast_with_dbschema
|
12
|
+
end
|
13
|
+
|
14
|
+
def cast_with_dbschema(object, type)
|
15
|
+
case object
|
16
|
+
when ::DbSchema::Definitions::Table
|
17
|
+
:dbschema_table
|
18
|
+
when ::DbSchema::Definitions::Field::Custom
|
19
|
+
:dbschema_custom_field
|
20
|
+
when ::DbSchema::Definitions::Field::Base
|
21
|
+
:dbschema_field
|
22
|
+
when ::DbSchema::Definitions::Index
|
23
|
+
:dbschema_index
|
24
|
+
when ::DbSchema::Definitions::Index::Field
|
25
|
+
:dbschema_index_field
|
26
|
+
when ::DbSchema::Definitions::CheckConstraint
|
27
|
+
:dbschema_check_constraint
|
28
|
+
when ::DbSchema::Definitions::ForeignKey
|
29
|
+
:dbschema_foreign_key
|
30
|
+
when ::DbSchema::Definitions::Enum
|
31
|
+
:dbschema_enum
|
32
|
+
when ::DbSchema::Changes::CreateTable
|
33
|
+
:dbschema_create_table
|
34
|
+
when ::DbSchema::Changes::DropTable
|
35
|
+
:dbschema_drop_table
|
36
|
+
when ::DbSchema::Changes::AlterTable
|
37
|
+
:dbschema_alter_table
|
38
|
+
when ::DbSchema::Changes::CreateColumn
|
39
|
+
:dbschema_create_column
|
40
|
+
when ::DbSchema::Changes::DropColumn
|
41
|
+
:dbschema_column_operation
|
42
|
+
when ::DbSchema::Changes::RenameColumn
|
43
|
+
:dbschema_rename_column
|
44
|
+
when ::DbSchema::Changes::AlterColumnType
|
45
|
+
:dbschema_alter_column_type
|
46
|
+
when ::DbSchema::Changes::CreatePrimaryKey,
|
47
|
+
::DbSchema::Changes::DropPrimaryKey,
|
48
|
+
::DbSchema::Changes::AllowNull,
|
49
|
+
::DbSchema::Changes::DisallowNull
|
50
|
+
:dbschema_column_operation
|
51
|
+
when ::DbSchema::Changes::AlterColumnDefault
|
52
|
+
:dbschema_alter_column_default
|
53
|
+
when ::DbSchema::Changes::CreateIndex
|
54
|
+
:dbschema_index
|
55
|
+
when ::DbSchema::Changes::DropIndex
|
56
|
+
:dbschema_column_operation
|
57
|
+
when ::DbSchema::Changes::CreateCheckConstraint
|
58
|
+
:dbschema_check_constraint
|
59
|
+
when ::DbSchema::Changes::DropCheckConstraint
|
60
|
+
:dbschema_column_operation
|
61
|
+
when ::DbSchema::Changes::CreateForeignKey
|
62
|
+
:dbschema_create_foreign_key
|
63
|
+
when ::DbSchema::Changes::DropForeignKey
|
64
|
+
:dbschema_drop_foreign_key
|
65
|
+
when ::DbSchema::Changes::CreateEnum
|
66
|
+
:dbschema_enum
|
67
|
+
when ::DbSchema::Changes::DropEnum
|
68
|
+
:dbschema_column_operation
|
69
|
+
when ::DbSchema::Changes::AddValueToEnum
|
70
|
+
:dbschema_add_value_to_enum
|
71
|
+
else
|
72
|
+
cast_without_dbschema(object, type)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def awesome_dbschema_table(object)
|
78
|
+
data = ["fields: #{object.fields.ai}"]
|
79
|
+
data << "indices: #{object.indices.ai}" if object.indices.any?
|
80
|
+
data << "checks: #{object.checks.ai}" if object.checks.any?
|
81
|
+
data << "foreign_keys: #{object.foreign_keys.ai}" if object.foreign_keys.any?
|
82
|
+
|
83
|
+
data_string = indent_lines(data.join(', '))
|
84
|
+
"#<DbSchema::Definitions::Table #{object.name.ai} #{data_string}>"
|
85
|
+
end
|
86
|
+
|
87
|
+
def awesome_dbschema_field(object)
|
88
|
+
options = object.options.map do |k, v|
|
89
|
+
key = colorize("#{k}:", :symbol)
|
90
|
+
"#{key} #{v.ai}"
|
91
|
+
end.unshift(nil).join(', ')
|
92
|
+
|
93
|
+
primary_key = if object.primary_key?
|
94
|
+
', ' + colorize('primary key', :nilclass)
|
95
|
+
else
|
96
|
+
''
|
97
|
+
end
|
98
|
+
|
99
|
+
"#<#{object.class} #{object.name.ai}#{options}#{primary_key}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
def awesome_dbschema_custom_field(object)
|
103
|
+
options = object.options.map do |k, v|
|
104
|
+
key = colorize("#{k}:", :symbol)
|
105
|
+
"#{key} #{v.ai}"
|
106
|
+
end.unshift(nil).join(', ')
|
107
|
+
|
108
|
+
primary_key = if object.primary_key?
|
109
|
+
', ' + colorize('primary key', :nilclass)
|
110
|
+
else
|
111
|
+
''
|
112
|
+
end
|
113
|
+
|
114
|
+
"#<#{object.class} (#{object.type.ai}) #{object.name.ai}#{options}#{primary_key}>"
|
115
|
+
end
|
116
|
+
|
117
|
+
def awesome_dbschema_index(object)
|
118
|
+
fields = format_dbschema_fields(object.fields)
|
119
|
+
using = ' using ' + colorize(object.type.to_s, :symbol) unless object.btree?
|
120
|
+
|
121
|
+
data = [nil]
|
122
|
+
data << colorize('unique', :nilclass) if object.unique?
|
123
|
+
data << colorize('condition: ', :symbol) + object.condition.ai unless object.condition.nil?
|
124
|
+
|
125
|
+
"#<#{object.class} #{object.name.ai} on #{fields}#{using}#{data.join(', ')}>"
|
126
|
+
end
|
127
|
+
|
128
|
+
def awesome_dbschema_index_field(object)
|
129
|
+
data = [object.name.ai]
|
130
|
+
|
131
|
+
if object.desc?
|
132
|
+
data << colorize('desc', :nilclass)
|
133
|
+
data << colorize('nulls last', :symbol) if object.nulls == :last
|
134
|
+
else
|
135
|
+
data << colorize('nulls first', :symbol) if object.nulls == :first
|
136
|
+
end
|
137
|
+
|
138
|
+
data.join(' ')
|
139
|
+
end
|
140
|
+
|
141
|
+
def awesome_dbschema_check_constraint(object)
|
142
|
+
"#<#{object.class} #{object.name.ai} #{object.condition.ai}>"
|
143
|
+
end
|
144
|
+
|
145
|
+
def awesome_dbschema_foreign_key(object)
|
146
|
+
fields = format_dbschema_fields(object.fields)
|
147
|
+
references = "#{colorize('references', :class)} #{object.table.ai}"
|
148
|
+
references << ' ' + format_dbschema_fields(object.keys) unless object.references_primary_key?
|
149
|
+
|
150
|
+
data = [nil]
|
151
|
+
data << colorize("on_update:", :symbol) + " #{object.on_update.ai}" unless object.on_update == :no_action
|
152
|
+
data << colorize("on_delete:", :symbol) + " #{object.on_delete.ai}" unless object.on_delete == :no_action
|
153
|
+
data << colorize('deferrable', :nilclass) if object.deferrable?
|
154
|
+
|
155
|
+
"#<#{object.class} #{object.name.ai} on #{fields} #{references}#{data.join(', ')}>"
|
156
|
+
end
|
157
|
+
|
158
|
+
def awesome_dbschema_enum(object)
|
159
|
+
values = object.values.map do |value|
|
160
|
+
colorize(value.to_s, :string)
|
161
|
+
end.join(', ')
|
162
|
+
|
163
|
+
"#<#{object.class} #{object.name.ai} (#{values})>"
|
164
|
+
end
|
165
|
+
|
166
|
+
def awesome_dbschema_create_table(object)
|
167
|
+
data = ["fields: #{object.fields.ai}"]
|
168
|
+
data << "indices: #{object.indices.ai}" if object.indices.any?
|
169
|
+
data << "checks: #{object.checks.ai}" if object.checks.any?
|
170
|
+
|
171
|
+
data_string = indent_lines(data.join(', '))
|
172
|
+
"#<DbSchema::Changes::CreateTable #{object.name.ai} #{data_string}>"
|
173
|
+
end
|
174
|
+
|
175
|
+
def awesome_dbschema_drop_table(object)
|
176
|
+
"#<DbSchema::Changes::DropTable #{object.name.ai}>"
|
177
|
+
end
|
178
|
+
|
179
|
+
def awesome_dbschema_alter_table(object)
|
180
|
+
data = ["fields: #{object.fields.ai}"]
|
181
|
+
data << "indices: #{object.indices.ai}" if object.indices.any?
|
182
|
+
data << "checks: #{object.checks.ai}" if object.checks.any?
|
183
|
+
|
184
|
+
data_string = indent_lines(data.join(', '))
|
185
|
+
"#<DbSchema::Changes::AlterTable #{object.name.ai} #{data_string}>"
|
186
|
+
end
|
187
|
+
|
188
|
+
def awesome_dbschema_create_column(object)
|
189
|
+
"#<DbSchema::Changes::CreateColumn #{object.field.ai}>"
|
190
|
+
end
|
191
|
+
|
192
|
+
def awesome_dbschema_drop_column(object)
|
193
|
+
"#<DbSchema::Changes::DropColumn #{object.name.ai}>"
|
194
|
+
end
|
195
|
+
|
196
|
+
def awesome_dbschema_rename_column(object)
|
197
|
+
"#<DbSchema::Changes::RenameColumn #{object.old_name.ai} => #{object.new_name.ai}>"
|
198
|
+
end
|
199
|
+
|
200
|
+
def awesome_dbschema_alter_column_type(object)
|
201
|
+
attributes = object.new_attributes.map do |k, v|
|
202
|
+
key = colorize("#{k}:", :symbol)
|
203
|
+
"#{key} #{v.ai}"
|
204
|
+
end.unshift(nil).join(', ')
|
205
|
+
|
206
|
+
"#<DbSchema::Changes::AlterColumnType #{object.name.ai}, #{object.new_type.ai}#{attributes}>"
|
207
|
+
end
|
208
|
+
|
209
|
+
def awesome_dbschema_alter_column_default(object)
|
210
|
+
"#<DbSchema::Changes::AlterColumnDefault #{object.name.ai}, #{object.new_default.ai}>"
|
211
|
+
end
|
212
|
+
|
213
|
+
def awesome_dbschema_create_foreign_key(object)
|
214
|
+
"#<DbSchema::Changes::CreateForeignKey #{object.foreign_key.ai} on #{object.table_name.ai}>"
|
215
|
+
end
|
216
|
+
|
217
|
+
def awesome_dbschema_drop_foreign_key(object)
|
218
|
+
"#<DbSchema::Changes::DropForeignKey #{object.fkey_name.ai} on #{object.table_name.ai}>"
|
219
|
+
end
|
220
|
+
|
221
|
+
def awesome_dbschema_column_operation(object)
|
222
|
+
"#<#{object.class} #{object.name.ai}>"
|
223
|
+
end
|
224
|
+
|
225
|
+
def awesome_dbschema_add_value_to_enum(object)
|
226
|
+
before = " before #{object.before.ai}" unless object.add_to_the_end?
|
227
|
+
|
228
|
+
"#<DbSchema::Changes::AddValueToEnum #{object.new_value.ai} to #{object.enum_name.ai}#{before}>"
|
229
|
+
end
|
230
|
+
|
231
|
+
def format_dbschema_fields(fields)
|
232
|
+
if fields.one?
|
233
|
+
fields.first.ai
|
234
|
+
else
|
235
|
+
'[' + fields.map(&:ai).join(', ') + ']'
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def indent_lines(text, indent_level = 4)
|
240
|
+
text.gsub(/(?<!\A)^/, ' ' * indent_level)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
AwesomePrint::Formatter.send(:include, AwesomePrint::DbSchema)
|
246
|
+
end
|
@@ -0,0 +1,396 @@
|
|
1
|
+
require 'db_schema/definitions'
|
2
|
+
require 'dry/equalizer'
|
3
|
+
|
4
|
+
module DbSchema
|
5
|
+
module Changes
|
6
|
+
class << self
|
7
|
+
def between(desired_schema, actual_schema)
|
8
|
+
desired_tables = extract_tables(desired_schema)
|
9
|
+
actual_tables = extract_tables(actual_schema)
|
10
|
+
|
11
|
+
table_names = [desired_tables, actual_tables].flatten.map(&:name).uniq
|
12
|
+
|
13
|
+
table_changes = table_names.each.with_object([]) do |table_name, changes|
|
14
|
+
desired = desired_tables.find { |table| table.name == table_name }
|
15
|
+
actual = actual_tables.find { |table| table.name == table_name }
|
16
|
+
|
17
|
+
if desired && !actual
|
18
|
+
changes << CreateTable.new(
|
19
|
+
table_name,
|
20
|
+
fields: desired.fields,
|
21
|
+
indices: desired.indices,
|
22
|
+
checks: desired.checks
|
23
|
+
)
|
24
|
+
|
25
|
+
fkey_operations = desired.foreign_keys.map do |fkey|
|
26
|
+
CreateForeignKey.new(table_name, fkey)
|
27
|
+
end
|
28
|
+
changes.concat(fkey_operations)
|
29
|
+
elsif actual && !desired
|
30
|
+
changes << DropTable.new(table_name)
|
31
|
+
|
32
|
+
actual.foreign_keys.each do |fkey|
|
33
|
+
changes << DropForeignKey.new(table_name, fkey.name)
|
34
|
+
end
|
35
|
+
elsif actual != desired
|
36
|
+
field_operations = field_changes(desired.fields, actual.fields)
|
37
|
+
index_operations = index_changes(desired.indices, actual.indices)
|
38
|
+
check_operations = check_changes(desired.checks, actual.checks)
|
39
|
+
fkey_operations = foreign_key_changes(table_name, desired.foreign_keys, actual.foreign_keys)
|
40
|
+
|
41
|
+
if field_operations.any? || index_operations.any? || check_operations.any?
|
42
|
+
changes << AlterTable.new(
|
43
|
+
table_name,
|
44
|
+
fields: field_operations,
|
45
|
+
indices: index_operations,
|
46
|
+
checks: check_operations
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
changes.concat(fkey_operations)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desired_enums = extract_enums(desired_schema)
|
55
|
+
actual_enums = extract_enums(actual_schema)
|
56
|
+
|
57
|
+
enum_names = [desired_enums, actual_enums].flatten.map(&:name).uniq
|
58
|
+
|
59
|
+
enum_changes = enum_names.each_with_object([]) do |enum_name, changes|
|
60
|
+
desired = desired_enums.find { |enum| enum.name == enum_name }
|
61
|
+
actual = actual_enums.find { |enum| enum.name == enum_name }
|
62
|
+
|
63
|
+
if desired && !actual
|
64
|
+
changes << CreateEnum.new(enum_name, desired.values)
|
65
|
+
elsif actual && !desired
|
66
|
+
changes << DropEnum.new(enum_name)
|
67
|
+
elsif actual != desired
|
68
|
+
new_values = desired.values - actual.values
|
69
|
+
dropped_values = actual.values - desired.values
|
70
|
+
|
71
|
+
if dropped_values.any?
|
72
|
+
raise UnsupportedOperation, "Enum #{enum_name.inspect} doesn't describe values #{dropped_values.inspect} that are present in the database; dropping values from enums is not supported."
|
73
|
+
end
|
74
|
+
|
75
|
+
if desired.values - new_values != actual.values
|
76
|
+
raise UnsupportedOperation, "Enum #{enum_name.inspect} describes values #{(desired.values - new_values).inspect} that are present in the database in a different order (#{actual.values.inspect}); reordering values in enums is not supported."
|
77
|
+
end
|
78
|
+
|
79
|
+
new_values.reverse.each do |value|
|
80
|
+
value_index = desired.values.index(value)
|
81
|
+
|
82
|
+
if value_index == desired.values.count - 1
|
83
|
+
changes << AddValueToEnum.new(enum_name, value)
|
84
|
+
else
|
85
|
+
next_value = desired.values[value_index + 1]
|
86
|
+
changes << AddValueToEnum.new(enum_name, value, before: next_value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
table_changes + enum_changes
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
def field_changes(desired_fields, actual_fields)
|
97
|
+
field_names = [desired_fields, actual_fields].flatten.map(&:name).uniq
|
98
|
+
|
99
|
+
field_names.each.with_object([]) do |field_name, table_changes|
|
100
|
+
desired = desired_fields.find { |field| field.name == field_name }
|
101
|
+
actual = actual_fields.find { |field| field.name == field_name }
|
102
|
+
|
103
|
+
if desired && !actual
|
104
|
+
table_changes << CreateColumn.new(desired)
|
105
|
+
elsif actual && !desired
|
106
|
+
table_changes << DropColumn.new(field_name)
|
107
|
+
elsif actual != desired
|
108
|
+
if (actual.type != desired.type) || (actual.attributes != desired.attributes)
|
109
|
+
table_changes << AlterColumnType.new(
|
110
|
+
field_name,
|
111
|
+
new_type: desired.type,
|
112
|
+
**desired.attributes
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
if desired.primary_key? && !actual.primary_key?
|
117
|
+
table_changes << CreatePrimaryKey.new(field_name)
|
118
|
+
end
|
119
|
+
|
120
|
+
if actual.primary_key? && !desired.primary_key?
|
121
|
+
table_changes << DropPrimaryKey.new(field_name)
|
122
|
+
end
|
123
|
+
|
124
|
+
if desired.null? && !actual.null?
|
125
|
+
table_changes << AllowNull.new(field_name)
|
126
|
+
end
|
127
|
+
|
128
|
+
if actual.null? && !desired.null?
|
129
|
+
table_changes << DisallowNull.new(field_name)
|
130
|
+
end
|
131
|
+
|
132
|
+
if actual.default != desired.default
|
133
|
+
table_changes << AlterColumnDefault.new(field_name, new_default: desired.default)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def index_changes(desired_indices, actual_indices)
|
140
|
+
index_names = [desired_indices, actual_indices].flatten.map(&:name).uniq
|
141
|
+
|
142
|
+
index_names.each.with_object([]) do |index_name, table_changes|
|
143
|
+
desired = desired_indices.find { |index| index.name == index_name }
|
144
|
+
actual = actual_indices.find { |index| index.name == index_name }
|
145
|
+
|
146
|
+
if desired && !actual
|
147
|
+
table_changes << CreateIndex.new(
|
148
|
+
name: index_name,
|
149
|
+
fields: desired.fields,
|
150
|
+
unique: desired.unique?,
|
151
|
+
type: desired.type,
|
152
|
+
condition: desired.condition
|
153
|
+
)
|
154
|
+
elsif actual && !desired
|
155
|
+
table_changes << DropIndex.new(index_name)
|
156
|
+
elsif actual != desired
|
157
|
+
table_changes << DropIndex.new(index_name)
|
158
|
+
table_changes << CreateIndex.new(
|
159
|
+
name: index_name,
|
160
|
+
fields: desired.fields,
|
161
|
+
unique: desired.unique?,
|
162
|
+
type: desired.type,
|
163
|
+
condition: desired.condition
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def check_changes(desired_checks, actual_checks)
|
170
|
+
check_names = [desired_checks, actual_checks].flatten.map(&:name).uniq
|
171
|
+
|
172
|
+
check_names.each.with_object([]) do |check_name, table_changes|
|
173
|
+
desired = desired_checks.find { |check| check.name == check_name }
|
174
|
+
actual = actual_checks.find { |check| check.name == check_name }
|
175
|
+
|
176
|
+
if desired && !actual
|
177
|
+
table_changes << CreateCheckConstraint.new(
|
178
|
+
name: check_name,
|
179
|
+
condition: desired.condition
|
180
|
+
)
|
181
|
+
elsif actual && !desired
|
182
|
+
table_changes << DropCheckConstraint.new(check_name)
|
183
|
+
elsif actual != desired
|
184
|
+
table_changes << DropCheckConstraint.new(check_name)
|
185
|
+
table_changes << CreateCheckConstraint.new(
|
186
|
+
name: check_name,
|
187
|
+
condition: desired.condition
|
188
|
+
)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def foreign_key_changes(table_name, desired_foreign_keys, actual_foreign_keys)
|
194
|
+
key_names = [desired_foreign_keys, actual_foreign_keys].flatten.map(&:name).uniq
|
195
|
+
|
196
|
+
key_names.each.with_object([]) do |key_name, table_changes|
|
197
|
+
desired = desired_foreign_keys.find { |key| key.name == key_name }
|
198
|
+
actual = actual_foreign_keys.find { |key| key.name == key_name }
|
199
|
+
|
200
|
+
foreign_key = Definitions::ForeignKey.new(
|
201
|
+
name: key_name,
|
202
|
+
fields: desired.fields,
|
203
|
+
table: desired.table,
|
204
|
+
keys: desired.keys,
|
205
|
+
on_delete: desired.on_delete,
|
206
|
+
on_update: desired.on_update,
|
207
|
+
deferrable: desired.deferrable?
|
208
|
+
) if desired
|
209
|
+
|
210
|
+
if desired && !actual
|
211
|
+
table_changes << CreateForeignKey.new(table_name, foreign_key)
|
212
|
+
elsif actual && !desired
|
213
|
+
table_changes << DropForeignKey.new(table_name, key_name)
|
214
|
+
elsif actual != desired
|
215
|
+
table_changes << DropForeignKey.new(table_name, key_name)
|
216
|
+
table_changes << CreateForeignKey.new(table_name, foreign_key)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def extract_tables(schema)
|
222
|
+
Utils.filter_by_class(schema, Definitions::Table)
|
223
|
+
end
|
224
|
+
|
225
|
+
def extract_enums(schema)
|
226
|
+
Utils.filter_by_class(schema, Definitions::Enum)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
class CreateTable
|
231
|
+
include Dry::Equalizer(:name, :fields, :indices, :checks)
|
232
|
+
attr_reader :name, :fields, :indices, :checks
|
233
|
+
|
234
|
+
def initialize(name, fields: [], indices: [], checks: [])
|
235
|
+
@name = name
|
236
|
+
@fields = fields
|
237
|
+
@indices = indices
|
238
|
+
@checks = checks
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class DropTable
|
243
|
+
include Dry::Equalizer(:name)
|
244
|
+
attr_reader :name
|
245
|
+
|
246
|
+
def initialize(name)
|
247
|
+
@name = name
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class AlterTable
|
252
|
+
include Dry::Equalizer(:name, :fields, :indices, :checks)
|
253
|
+
attr_reader :name, :fields, :indices, :checks
|
254
|
+
|
255
|
+
def initialize(name, fields: [], indices: [], checks: [])
|
256
|
+
@name = name
|
257
|
+
@fields = fields
|
258
|
+
@indices = indices
|
259
|
+
@checks = checks
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Abstract base class for single-column toggle operations.
|
264
|
+
class ColumnOperation
|
265
|
+
include Dry::Equalizer(:name)
|
266
|
+
attr_reader :name
|
267
|
+
|
268
|
+
def initialize(name)
|
269
|
+
@name = name
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
class CreateColumn
|
274
|
+
include Dry::Equalizer(:field)
|
275
|
+
attr_reader :field
|
276
|
+
|
277
|
+
def initialize(field)
|
278
|
+
@field = field
|
279
|
+
end
|
280
|
+
|
281
|
+
def name
|
282
|
+
field.name
|
283
|
+
end
|
284
|
+
|
285
|
+
def type
|
286
|
+
field.type
|
287
|
+
end
|
288
|
+
|
289
|
+
def primary_key?
|
290
|
+
field.primary_key?
|
291
|
+
end
|
292
|
+
|
293
|
+
def options
|
294
|
+
field.options
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
class DropColumn < ColumnOperation
|
299
|
+
end
|
300
|
+
|
301
|
+
class RenameColumn
|
302
|
+
attr_reader :old_name, :new_name
|
303
|
+
|
304
|
+
def initialize(old_name:, new_name:)
|
305
|
+
@old_name = old_name
|
306
|
+
@new_name = new_name
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
class AlterColumnType
|
311
|
+
include Dry::Equalizer(:name, :new_type, :new_attributes)
|
312
|
+
attr_reader :name, :new_type, :new_attributes
|
313
|
+
|
314
|
+
def initialize(name, new_type:, **new_attributes)
|
315
|
+
@name = name
|
316
|
+
@new_type = new_type
|
317
|
+
@new_attributes = new_attributes
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class CreatePrimaryKey < ColumnOperation
|
322
|
+
end
|
323
|
+
|
324
|
+
class DropPrimaryKey < ColumnOperation
|
325
|
+
end
|
326
|
+
|
327
|
+
class AllowNull < ColumnOperation
|
328
|
+
end
|
329
|
+
|
330
|
+
class DisallowNull < ColumnOperation
|
331
|
+
end
|
332
|
+
|
333
|
+
class AlterColumnDefault
|
334
|
+
include Dry::Equalizer(:name, :new_default)
|
335
|
+
attr_reader :name, :new_default
|
336
|
+
|
337
|
+
def initialize(name, new_default:)
|
338
|
+
@name = name
|
339
|
+
@new_default = new_default
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class CreateIndex < Definitions::Index
|
344
|
+
end
|
345
|
+
|
346
|
+
class DropIndex < ColumnOperation
|
347
|
+
end
|
348
|
+
|
349
|
+
class CreateCheckConstraint < Definitions::CheckConstraint
|
350
|
+
end
|
351
|
+
|
352
|
+
class DropCheckConstraint < ColumnOperation
|
353
|
+
end
|
354
|
+
|
355
|
+
class CreateForeignKey
|
356
|
+
include Dry::Equalizer(:table_name, :foreign_key)
|
357
|
+
attr_reader :table_name, :foreign_key
|
358
|
+
|
359
|
+
def initialize(table_name, foreign_key)
|
360
|
+
@table_name = table_name
|
361
|
+
@foreign_key = foreign_key
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
class DropForeignKey
|
366
|
+
include Dry::Equalizer(:table_name, :fkey_name)
|
367
|
+
attr_reader :table_name, :fkey_name
|
368
|
+
|
369
|
+
def initialize(table_name, fkey_name)
|
370
|
+
@table_name = table_name
|
371
|
+
@fkey_name = fkey_name
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
class CreateEnum < Definitions::Enum
|
376
|
+
end
|
377
|
+
|
378
|
+
class DropEnum < ColumnOperation
|
379
|
+
end
|
380
|
+
|
381
|
+
class AddValueToEnum
|
382
|
+
include Dry::Equalizer(:enum_name, :new_value, :before)
|
383
|
+
attr_reader :enum_name, :new_value, :before
|
384
|
+
|
385
|
+
def initialize(enum_name, new_value, before: nil)
|
386
|
+
@enum_name = enum_name
|
387
|
+
@new_value = new_value
|
388
|
+
@before = before
|
389
|
+
end
|
390
|
+
|
391
|
+
def add_to_the_end?
|
392
|
+
before.nil?
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|