db_schema 0.1
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/.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
|