db_schema 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a09ea036b2a5cd5dbfe5de0a285d2f88d085cc1
4
- data.tar.gz: 3aa56463f84cdeedd5ec878d49fab886d4f12acd
3
+ metadata.gz: fd3abbf119d7700e83eedf09b10b100cfc744f9e
4
+ data.tar.gz: d3e8cfd07c1fdcd902e705b337eed52f2bdf8829
5
5
  SHA512:
6
- metadata.gz: dd2c597cb6e6c768dda725934bad84b70044bb5a44fcb7a4f4c4106d3bb37accc729fabec9683c53830598b17c04051b7e1d0081eadf597346ea19ce295013fb
7
- data.tar.gz: 36738c9b037c4526683ee03ca796fe5afb10013a276117308948d30a0c5a485c2da921c33c03b43d0b9638d9b90a8bdfdee392e38e7163fca0c8072c128c8021
6
+ metadata.gz: 251c126151289b49ea663c2d57c46334d9ba98c9419110121acbb436aee006871eec48aeabbe3ea1a6e6369b1a76fd4ff07e5e9d3cbb0c88e12e13ba609269b2
7
+ data.tar.gz: a6163f3a147938bad96b7d891f26990fc9c86068f716ec236e4647dda13019c1af9bf1a2ba566fe3c4c8f0b46d5edd6db29ed002eb3212d56590e62fc6baf84f
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # DbSchema [![Build Status](https://travis-ci.org/7even/db_schema.svg?branch=master)](https://travis-ci.org/7even/db_schema) [![Gem Version](https://badge.fury.io/rb/db_schema.svg)](https://badge.fury.io/rb/db_schema)
1
+ # DbSchema [![Build Status](https://travis-ci.org/7even/db_schema.svg?branch=master)](https://travis-ci.org/7even/db_schema) [![Gem Version](https://badge.fury.io/rb/db_schema.svg)](https://badge.fury.io/rb/db_schema) [![Join the chat at https://gitter.im/7even/db_schema](https://badges.gitter.im/7even/db_schema.svg)](https://gitter.im/7even/db_schema?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2
2
 
3
3
  DbSchema is an opinionated database schema management tool that lets you maintain your DB schema with a single ruby file.
4
4
 
@@ -53,7 +53,7 @@ But you would lose it even with manual migrations.
53
53
  Add this line to your application's Gemfile:
54
54
 
55
55
  ``` ruby
56
- gem 'db_schema', '~> 0.2.4'
56
+ gem 'db_schema', '~> 0.2.5'
57
57
  ```
58
58
 
59
59
  And then execute:
@@ -148,9 +148,9 @@ If your production setup doesn't include multiple workers starting simultaneousl
148
148
  ## DSL
149
149
 
150
150
  Database schema is defined with a block passed to `DbSchema.describe` method.
151
- This block receives a `db` object on which you can call `#table` to define a table
152
- and `#enum` to define a custom enum type. Everything that belongs to a specific table
153
- is described in a block passed to `#table`.
151
+ This block receives a `db` object on which you can call `#table` to define a table,
152
+ `#enum` to define a custom enum type and `#extension` to plug a Postgres extension into your database.
153
+ Everything that belongs to a specific table is described in a block passed to `#table`.
154
154
 
155
155
  ``` ruby
156
156
  DbSchema.describe do |db|
@@ -500,13 +500,15 @@ db.table :users do |t|
500
500
  end
501
501
  ```
502
502
 
503
- After the enum type is created, you can add more values to it. They don't have to appear in the end of the values list - you can add new values to the middle or even to the beginning of the list:
503
+ Arrays of enums are also supported - they are described just like arrays of any other element type:
504
504
 
505
505
  ``` ruby
506
- db.enum :user_status, [:guest, :registered, :sent_confirmation_email, :confirmed_email, :subscriber]
507
- ```
506
+ db.enum :user_role, [:user, :manager, :admin]
508
507
 
509
- Reordering and deleting values from enum types is not supported.
508
+ db.table :users do |t|
509
+ t.array :roles, of: :user_role, default: '{user}'
510
+ end
511
+ ```
510
512
 
511
513
  ### Extensions
512
514
 
@@ -18,7 +18,7 @@ module DbSchema
18
18
  def describe(&block)
19
19
  desired_schema = DSL.new(block).schema
20
20
  validate(desired_schema)
21
- Normalizer.normalize_tables(desired_schema)
21
+ Normalizer.new(desired_schema).normalize_tables
22
22
 
23
23
  actual_schema = Reader.read_schema
24
24
  changes = Changes.between(desired_schema, actual_schema)
@@ -15,6 +15,8 @@ if defined?(AwesomePrint)
15
15
  case object
16
16
  when ::DbSchema::Definitions::Schema
17
17
  :dbschema_schema
18
+ when ::DbSchema::Definitions::NullTable
19
+ :dbschema_null_table
18
20
  when ::DbSchema::Definitions::Table
19
21
  :dbschema_table
20
22
  when ::DbSchema::Definitions::Field::Custom
@@ -55,11 +57,11 @@ if defined?(AwesomePrint)
55
57
  when ::DbSchema::Changes::AlterColumnDefault
56
58
  :dbschema_alter_column_default
57
59
  when ::DbSchema::Changes::CreateIndex
58
- :dbschema_index
60
+ :dbschema_create_index
59
61
  when ::DbSchema::Changes::DropIndex
60
62
  :dbschema_column_operation
61
63
  when ::DbSchema::Changes::CreateCheckConstraint
62
- :dbschema_check_constraint
64
+ :dbschema_create_check_constraint
63
65
  when ::DbSchema::Changes::DropCheckConstraint
64
66
  :dbschema_column_operation
65
67
  when ::DbSchema::Changes::CreateForeignKey
@@ -67,13 +69,14 @@ if defined?(AwesomePrint)
67
69
  when ::DbSchema::Changes::DropForeignKey
68
70
  :dbschema_drop_foreign_key
69
71
  when ::DbSchema::Changes::CreateEnum
70
- :dbschema_enum
72
+ :dbschema_create_enum
71
73
  when ::DbSchema::Changes::DropEnum
72
74
  :dbschema_column_operation
73
- when ::DbSchema::Changes::AddValueToEnum
74
- :dbschema_add_value_to_enum
75
- when ::DbSchema::Changes::CreateExtension,
76
- ::DbSchema::Changes::DropExtension
75
+ when ::DbSchema::Changes::AlterEnumValues
76
+ :dbschema_alter_enum_values
77
+ when ::DbSchema::Changes::CreateExtension
78
+ :dbschema_create_extension
79
+ when ::DbSchema::Changes::DropExtension
77
80
  :dbschema_column_operation
78
81
  else
79
82
  cast_without_dbschema(object, type)
@@ -100,6 +103,10 @@ if defined?(AwesomePrint)
100
103
  "#<DbSchema::Definitions::Table #{object.name.ai} #{data_string}>"
101
104
  end
102
105
 
106
+ def awesome_dbschema_null_table(object)
107
+ '#<DbSchema::Definitions::NullTable>'
108
+ end
109
+
103
110
  def awesome_dbschema_field(object)
104
111
  options = object.options.map do |k, v|
105
112
  key = colorize("#{k}:", :symbol)
@@ -190,12 +197,12 @@ if defined?(AwesomePrint)
190
197
  end
191
198
 
192
199
  def awesome_dbschema_create_table(object)
193
- data = ["fields: #{object.fields.ai}"]
194
- data << "indices: #{object.indices.ai}" if object.indices.any?
195
- data << "checks: #{object.checks.ai}" if object.checks.any?
200
+ data = ["fields: #{object.table.fields.ai}"]
201
+ data << "indices: #{object.table.indices.ai}" if object.table.indices.any?
202
+ data << "checks: #{object.table.checks.ai}" if object.table.checks.any?
196
203
 
197
204
  data_string = indent_lines(data.join(', '))
198
- "#<DbSchema::Changes::CreateTable #{object.name.ai} #{data_string}>"
205
+ "#<DbSchema::Changes::CreateTable #{object.table.name.ai} #{data_string}>"
199
206
  end
200
207
 
201
208
  def awesome_dbschema_drop_table(object)
@@ -203,12 +210,7 @@ if defined?(AwesomePrint)
203
210
  end
204
211
 
205
212
  def awesome_dbschema_alter_table(object)
206
- data = ["fields: #{object.fields.ai}"]
207
- data << "indices: #{object.indices.ai}" if object.indices.any?
208
- data << "checks: #{object.checks.ai}" if object.checks.any?
209
-
210
- data_string = indent_lines(data.join(', '))
211
- "#<DbSchema::Changes::AlterTable #{object.name.ai} #{data_string}>"
213
+ "#<DbSchema::Changes::AlterTable #{object.table_name.ai} #{indent_lines(object.changes.ai)}>"
212
214
  end
213
215
 
214
216
  def awesome_dbschema_create_column(object)
@@ -242,6 +244,21 @@ if defined?(AwesomePrint)
242
244
  "#<DbSchema::Changes::AlterColumnDefault #{object.name.ai}, #{new_default}>"
243
245
  end
244
246
 
247
+ def awesome_dbschema_create_index(object)
248
+ columns = format_dbschema_fields(object.index.columns)
249
+ using = ' using ' + colorize(object.index.type.to_s, :symbol) unless object.index.btree?
250
+
251
+ data = [nil]
252
+ data << colorize('unique', :nilclass) if object.index.unique?
253
+ data << colorize('condition: ', :symbol) + object.index.condition.ai unless object.index.condition.nil?
254
+
255
+ "#<#{object.class} #{object.index.name.ai} on #{columns}#{using}#{data.join(', ')}>"
256
+ end
257
+
258
+ def awesome_dbschema_create_check_constraint(object)
259
+ "#<#{object.class} #{object.check.name.ai} #{object.check.condition.ai}>"
260
+ end
261
+
245
262
  def awesome_dbschema_create_foreign_key(object)
246
263
  "#<DbSchema::Changes::CreateForeignKey #{object.foreign_key.ai} on #{object.table_name.ai}>"
247
264
  end
@@ -250,14 +267,28 @@ if defined?(AwesomePrint)
250
267
  "#<DbSchema::Changes::DropForeignKey #{object.fkey_name.ai} on #{object.table_name.ai}>"
251
268
  end
252
269
 
270
+ def awesome_dbschema_create_enum(object)
271
+ values = object.enum.values.map do |value|
272
+ colorize(value.to_s, :string)
273
+ end.join(', ')
274
+
275
+ "#<#{object.class} #{object.enum.name.ai} (#{values})>"
276
+ end
277
+
253
278
  def awesome_dbschema_column_operation(object)
254
279
  "#<#{object.class} #{object.name.ai}>"
255
280
  end
256
281
 
257
- def awesome_dbschema_add_value_to_enum(object)
258
- before = " before #{object.before.ai}" unless object.add_to_the_end?
282
+ def awesome_dbschema_alter_enum_values(object)
283
+ values = object.new_values.map do |value|
284
+ colorize(value.to_s, :string)
285
+ end.join(', ')
286
+
287
+ "#<DbSchema::Changes::AlterEnumValues #{object.enum_name.ai} to (#{values})>"
288
+ end
259
289
 
260
- "#<DbSchema::Changes::AddValueToEnum #{object.new_value.ai} to #{object.enum_name.ai}#{before}>"
290
+ def awesome_dbschema_create_extension(object)
291
+ "#<#{object.class} #{object.extension.name.ai}>"
261
292
  end
262
293
 
263
294
  def format_dbschema_fields(fields)
@@ -12,12 +12,7 @@ module DbSchema
12
12
  actual = actual_schema.tables.find { |table| table.name == table_name }
13
13
 
14
14
  if desired && !actual
15
- changes << CreateTable.new(
16
- table_name,
17
- fields: desired.fields,
18
- indices: desired.indices,
19
- checks: desired.checks
20
- )
15
+ changes << CreateTable.new(desired)
21
16
 
22
17
  fkey_operations = desired.foreign_keys.map do |fkey|
23
18
  CreateForeignKey.new(table_name, fkey)
@@ -38,9 +33,7 @@ module DbSchema
38
33
  if field_operations.any? || index_operations.any? || check_operations.any?
39
34
  changes << AlterTable.new(
40
35
  table_name,
41
- fields: field_operations,
42
- indices: index_operations,
43
- checks: check_operations
36
+ field_operations + index_operations + check_operations
44
37
  )
45
38
  end
46
39
 
@@ -55,36 +48,37 @@ module DbSchema
55
48
  actual = actual_schema.enums.find { |enum| enum.name == enum_name }
56
49
 
57
50
  if desired && !actual
58
- changes << CreateEnum.new(enum_name, desired.values)
51
+ changes << CreateEnum.new(desired)
59
52
  elsif actual && !desired
60
53
  changes << DropEnum.new(enum_name)
61
54
  elsif actual != desired
62
- new_values = desired.values - actual.values
63
- dropped_values = actual.values - desired.values
64
-
65
- if dropped_values.any?
66
- 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."
67
- end
68
-
69
- if desired.values - new_values != actual.values
70
- 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."
71
- end
72
-
73
- new_values.reverse.each do |value|
74
- value_index = desired.values.index(value)
75
-
76
- if value_index == desired.values.count - 1
77
- changes << AddValueToEnum.new(enum_name, value)
78
- else
79
- next_value = desired.values[value_index + 1]
80
- changes << AddValueToEnum.new(enum_name, value, before: next_value)
55
+ fields = actual_schema.tables.flat_map do |table|
56
+ table.fields.select do |field|
57
+ if field.array?
58
+ field.attributes[:element_type] == enum_name
59
+ else
60
+ field.type == enum_name
61
+ end
62
+ end.map do |field|
63
+ if desired_field = desired_schema[table.name][field.name]
64
+ new_default = desired_field.default
65
+ end
66
+
67
+ {
68
+ table_name: table.name,
69
+ field_name: field.name,
70
+ new_default: new_default,
71
+ array: field.array?
72
+ }
81
73
  end
82
74
  end
75
+
76
+ changes << AlterEnumValues.new(enum_name, desired.values, fields)
83
77
  end
84
78
  end
85
79
 
86
80
  extension_changes = (desired_schema.extensions - actual_schema.extensions).map do |extension|
87
- CreateExtension.new(extension.name)
81
+ CreateExtension.new(extension)
88
82
  end + (actual_schema.extensions - desired_schema.extensions).map do |extension|
89
83
  DropExtension.new(extension.name)
90
84
  end
@@ -144,24 +138,12 @@ module DbSchema
144
138
  actual = actual_indices.find { |index| index.name == index_name }
145
139
 
146
140
  if desired && !actual
147
- table_changes << CreateIndex.new(
148
- name: index_name,
149
- columns: desired.columns,
150
- unique: desired.unique?,
151
- type: desired.type,
152
- condition: desired.condition
153
- )
141
+ table_changes << CreateIndex.new(desired)
154
142
  elsif actual && !desired
155
143
  table_changes << DropIndex.new(index_name)
156
144
  elsif actual != desired
157
145
  table_changes << DropIndex.new(index_name)
158
- table_changes << CreateIndex.new(
159
- name: index_name,
160
- columns: desired.columns,
161
- unique: desired.unique?,
162
- type: desired.type,
163
- condition: desired.condition
164
- )
146
+ table_changes << CreateIndex.new(desired)
165
147
  end
166
148
  end
167
149
  end
@@ -174,18 +156,12 @@ module DbSchema
174
156
  actual = actual_checks.find { |check| check.name == check_name }
175
157
 
176
158
  if desired && !actual
177
- table_changes << CreateCheckConstraint.new(
178
- name: check_name,
179
- condition: desired.condition
180
- )
159
+ table_changes << CreateCheckConstraint.new(desired)
181
160
  elsif actual && !desired
182
161
  table_changes << DropCheckConstraint.new(check_name)
183
162
  elsif actual != desired
184
163
  table_changes << DropCheckConstraint.new(check_name)
185
- table_changes << CreateCheckConstraint.new(
186
- name: check_name,
187
- condition: desired.condition
188
- )
164
+ table_changes << CreateCheckConstraint.new(desired)
189
165
  end
190
166
  end
191
167
  end
@@ -197,37 +173,24 @@ module DbSchema
197
173
  desired = desired_foreign_keys.find { |key| key.name == key_name }
198
174
  actual = actual_foreign_keys.find { |key| key.name == key_name }
199
175
 
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
176
  if desired && !actual
211
- table_changes << CreateForeignKey.new(table_name, foreign_key)
177
+ table_changes << CreateForeignKey.new(table_name, desired)
212
178
  elsif actual && !desired
213
179
  table_changes << DropForeignKey.new(table_name, key_name)
214
180
  elsif actual != desired
215
181
  table_changes << DropForeignKey.new(table_name, key_name)
216
- table_changes << CreateForeignKey.new(table_name, foreign_key)
182
+ table_changes << CreateForeignKey.new(table_name, desired)
217
183
  end
218
184
  end
219
185
  end
220
186
  end
221
187
 
222
188
  class CreateTable
223
- include Dry::Equalizer(:name, :fields, :indices, :checks)
224
- attr_reader :name, :fields, :indices, :checks
225
-
226
- def initialize(name, fields: [], indices: [], checks: [])
227
- @name = name
228
- @fields = fields
229
- @indices = indices
230
- @checks = checks
189
+ include Dry::Equalizer(:table)
190
+ attr_reader :table
191
+
192
+ def initialize(table)
193
+ @table = table
231
194
  end
232
195
  end
233
196
 
@@ -241,14 +204,12 @@ module DbSchema
241
204
  end
242
205
 
243
206
  class AlterTable
244
- include Dry::Equalizer(:name, :fields, :indices, :checks)
245
- attr_reader :name, :fields, :indices, :checks
246
-
247
- def initialize(name, fields: [], indices: [], checks: [])
248
- @name = name
249
- @fields = fields
250
- @indices = indices
251
- @checks = checks
207
+ include Dry::Equalizer(:table_name, :changes)
208
+ attr_reader :table_name, :changes
209
+
210
+ def initialize(table_name, changes)
211
+ @table_name = table_name
212
+ @changes = changes
252
213
  end
253
214
  end
254
215
 
@@ -332,13 +293,25 @@ module DbSchema
332
293
  end
333
294
  end
334
295
 
335
- class CreateIndex < Definitions::Index
296
+ class CreateIndex
297
+ include Dry::Equalizer(:index)
298
+ attr_reader :index
299
+
300
+ def initialize(index)
301
+ @index = index
302
+ end
336
303
  end
337
304
 
338
305
  class DropIndex < ColumnOperation
339
306
  end
340
307
 
341
- class CreateCheckConstraint < Definitions::CheckConstraint
308
+ class CreateCheckConstraint
309
+ include Dry::Equalizer(:check)
310
+ attr_reader :check
311
+
312
+ def initialize(check)
313
+ @check = check
314
+ end
342
315
  end
343
316
 
344
317
  class DropCheckConstraint < ColumnOperation
@@ -364,28 +337,36 @@ module DbSchema
364
337
  end
365
338
  end
366
339
 
367
- class CreateEnum < Definitions::Enum
340
+ class CreateEnum
341
+ include Dry::Equalizer(:enum)
342
+ attr_reader :enum
343
+
344
+ def initialize(enum)
345
+ @enum = enum
346
+ end
368
347
  end
369
348
 
370
349
  class DropEnum < ColumnOperation
371
350
  end
372
351
 
373
- class AddValueToEnum
374
- include Dry::Equalizer(:enum_name, :new_value, :before)
375
- attr_reader :enum_name, :new_value, :before
376
-
377
- def initialize(enum_name, new_value, before: nil)
378
- @enum_name = enum_name
379
- @new_value = new_value
380
- @before = before
381
- end
352
+ class AlterEnumValues
353
+ include Dry::Equalizer(:enum_name, :new_values, :enum_fields)
354
+ attr_reader :enum_name, :new_values, :enum_fields
382
355
 
383
- def add_to_the_end?
384
- before.nil?
356
+ def initialize(enum_name, new_values, enum_fields)
357
+ @enum_name = enum_name
358
+ @new_values = new_values
359
+ @enum_fields = enum_fields
385
360
  end
386
361
  end
387
362
 
388
- class CreateExtension < Definitions::Extension
363
+ class CreateExtension
364
+ include Dry::Equalizer(:extension)
365
+ attr_reader :extension
366
+
367
+ def initialize(extension)
368
+ @extension = extension
369
+ end
389
370
  end
390
371
 
391
372
  class DropExtension < ColumnOperation
@@ -12,6 +12,10 @@ module DbSchema
12
12
  @enums = enums
13
13
  @extensions = extensions
14
14
  end
15
+
16
+ def [](table_name)
17
+ tables.find { |table| table.name == table_name } || NullTable.new
18
+ end
15
19
  end
16
20
 
17
21
  class Table
@@ -31,6 +35,54 @@ module DbSchema
31
35
  indices.any?(&:has_expressions?) ||
32
36
  checks.any?
33
37
  end
38
+
39
+ def [](field_name)
40
+ fields.find { |field| field.name == field_name }
41
+ end
42
+
43
+ def with_name(new_name)
44
+ Table.new(
45
+ new_name,
46
+ fields: fields,
47
+ indices: indices,
48
+ checks: checks,
49
+ foreign_keys: foreign_keys
50
+ )
51
+ end
52
+
53
+ def with_fields(new_fields)
54
+ Table.new(
55
+ name,
56
+ fields: new_fields,
57
+ indices: indices,
58
+ checks: checks,
59
+ foreign_keys: foreign_keys
60
+ )
61
+ end
62
+
63
+ def with_indices(new_indices)
64
+ Table.new(
65
+ name,
66
+ fields: fields,
67
+ indices: new_indices,
68
+ checks: checks,
69
+ foreign_keys: foreign_keys
70
+ )
71
+ end
72
+
73
+ def with_foreign_keys(new_foreign_keys)
74
+ Table.new(
75
+ name,
76
+ fields: fields,
77
+ indices: indices,
78
+ checks: checks,
79
+ foreign_keys: new_foreign_keys
80
+ )
81
+ end
82
+ end
83
+
84
+ class NullTable < Table
85
+ def initialize; end
34
86
  end
35
87
 
36
88
  class Index
@@ -65,6 +117,16 @@ module DbSchema
65
117
  !condition.nil? || columns.any?(&:expression?)
66
118
  end
67
119
 
120
+ def with_name(new_name)
121
+ Index.new(
122
+ name: new_name,
123
+ columns: columns,
124
+ unique: unique?,
125
+ type: type,
126
+ condition: condition
127
+ )
128
+ end
129
+
68
130
  class Column
69
131
  include Dry::Equalizer(:name, :order, :nulls)
70
132
  attr_reader :name, :order, :nulls
@@ -173,6 +235,10 @@ module DbSchema
173
235
  @name = name
174
236
  @values = values
175
237
  end
238
+
239
+ def with_name(new_name)
240
+ Enum.new(new_name, values)
241
+ end
176
242
  end
177
243
 
178
244
  class Extension
@@ -3,16 +3,27 @@ module DbSchema
3
3
  module Field
4
4
  class Array < Base
5
5
  register :array
6
- attr_reader :element_type
7
6
 
8
- def initialize(name, of:, **options)
9
- super(name, **options)
10
- @element_type = Field.type_class_for(of)
7
+ def initialize(name, **options)
8
+ type_class = Field.type_class_for(options[:element_type])
9
+ super(name, **options.merge(element_type: type_class))
11
10
  end
12
11
 
13
12
  def attributes
14
13
  super.merge(element_type: element_type.type)
15
14
  end
15
+
16
+ def array?
17
+ true
18
+ end
19
+
20
+ def element_type
21
+ @attributes[:element_type]
22
+ end
23
+
24
+ def custom_element_type?
25
+ element_type.superclass == Custom
26
+ end
16
27
  end
17
28
  end
18
29
  end
@@ -25,6 +25,14 @@ module DbSchema
25
25
  default.is_a?(Symbol)
26
26
  end
27
27
 
28
+ def array?
29
+ false
30
+ end
31
+
32
+ def custom?
33
+ false
34
+ end
35
+
28
36
  def options
29
37
  attributes.tap do |options|
30
38
  options[:null] = false unless null?
@@ -48,6 +56,14 @@ module DbSchema
48
56
  self.class.type
49
57
  end
50
58
 
59
+ def with_type(new_type)
60
+ Field.build(name, new_type, **options, primary_key: primary_key?)
61
+ end
62
+
63
+ def with_attribute(attr_name, attr_value)
64
+ Field.build(name, type, **options, primary_key: primary_key?, attr_name => attr_value)
65
+ end
66
+
51
67
  class << self
52
68
  def register(*types)
53
69
  types.each do |type|
@@ -6,7 +6,7 @@ module DbSchema
6
6
  def class_for(type_name)
7
7
  raise ArgumentError if type_name.nil?
8
8
 
9
- Class.new(self) do
9
+ custom_types[type_name] ||= Class.new(self) do
10
10
  define_method :type do
11
11
  type_name
12
12
  end
@@ -14,8 +14,17 @@ module DbSchema
14
14
  define_singleton_method :type do
15
15
  type_name
16
16
  end
17
+
18
+ define_method :custom? do
19
+ true
20
+ end
17
21
  end
18
22
  end
23
+
24
+ private
25
+ def custom_types
26
+ @custom_types ||= {}
27
+ end
19
28
  end
20
29
  end
21
30
  end
@@ -42,11 +42,17 @@ module DbSchema
42
42
  end
43
43
 
44
44
  DbSchema::Definitions::Field.registry.keys.each do |type|
45
+ next if type == :array
46
+
45
47
  define_method(type) do |name, **options|
46
48
  field(name, type, options)
47
49
  end
48
50
  end
49
51
 
52
+ def array(name, of:, **options)
53
+ field(name, :array, element_type: of, **options)
54
+ end
55
+
50
56
  def method_missing(method_name, name, *args, &block)
51
57
  field(name, method_name, args.first || {})
52
58
  end
@@ -2,118 +2,139 @@ require 'digest/md5'
2
2
 
3
3
  module DbSchema
4
4
  class Normalizer
5
- attr_reader :table
6
-
7
- class << self
8
- def normalize_tables(schema)
9
- DbSchema.connection.transaction do
10
- create_extensions!(schema.extensions)
11
- create_enums!(schema.enums)
12
-
13
- schema.tables = schema.tables.map do |table|
14
- if table.has_expressions?
15
- new(table).normalized_table
16
- else
17
- table
18
- end
19
- end
5
+ attr_reader :schema
6
+
7
+ def initialize(schema)
8
+ @schema = schema
9
+ end
10
+
11
+ def normalize_tables
12
+ DbSchema.connection.transaction do
13
+ create_extensions!
14
+ create_enums!
20
15
 
21
- raise Sequel::Rollback
16
+ schema.tables = schema.tables.map do |table|
17
+ if table.has_expressions?
18
+ Table.new(table, hash).normalized_table
19
+ else
20
+ table
21
+ end
22
22
  end
23
+
24
+ raise Sequel::Rollback
23
25
  end
26
+ end
24
27
 
25
- private
26
- def create_extensions!(extensions)
27
- (extensions - DbSchema::Reader.read_extensions).each do |extension|
28
- operation = DbSchema::Changes::CreateExtension.new(extension.name)
29
- DbSchema::Runner.new([operation]).run!
30
- end
28
+ private
29
+ def create_extensions!
30
+ operations = (schema.extensions - Reader.read_extensions).map do |extension|
31
+ Changes::CreateExtension.new(extension)
31
32
  end
32
33
 
33
- def create_enums!(enums)
34
- existing_enums_names = DbSchema::Reader.read_enums.map(&:name)
35
- enums.each do |enum|
36
- next if existing_enums_names.include?(enum.name)
34
+ Runner.new(operations).run!
35
+ end
37
36
 
38
- operation = DbSchema::Changes::CreateEnum.new(enum.name, enum.values)
39
- DbSchema::Runner.new([operation]).run!
40
- end
37
+ def create_enums!
38
+ operations = schema.enums.map do |enum|
39
+ Changes::CreateEnum.new(enum.with_name(append_hash(enum.name)))
41
40
  end
42
- end
43
41
 
44
- def initialize(table)
45
- @table = table
42
+ Runner.new(operations).run!
46
43
  end
47
44
 
48
- def normalized_table
49
- create_temporary_table!
50
- read_temporary_table
45
+ def append_hash(name)
46
+ "#{name}_#{hash}"
51
47
  end
52
48
 
53
- private
54
- def create_temporary_table!
55
- operation = Changes::CreateTable.new(
56
- temporary_table_name,
57
- fields: table.fields,
58
- indices: rename_indices(table.indices),
59
- checks: table.checks
60
- )
61
-
62
- Runner.new([operation]).run!
49
+ def hash
50
+ @hash ||= begin
51
+ names = schema.tables.flat_map do |table|
52
+ [table.name] + table.fields.map(&:name) + table.indices.map(&:name) + table.checks.map(&:name)
53
+ end
54
+
55
+ Digest::MD5.hexdigest(names.join(','))[0..9]
56
+ end
63
57
  end
64
58
 
65
- def read_temporary_table
66
- temporary_table = Reader.read_table(temporary_table_name)
59
+ class Table
60
+ attr_reader :table, :hash
67
61
 
68
- Definitions::Table.new(
69
- remove_hash(temporary_table.name),
70
- fields: temporary_table.fields,
71
- indices: rename_indices_back(temporary_table.indices),
72
- checks: temporary_table.checks,
73
- foreign_keys: table.foreign_keys
74
- )
75
- end
62
+ def initialize(table, hash)
63
+ @table = table
64
+ @hash = hash
65
+ end
76
66
 
77
- def rename_indices(indices)
78
- indices.map do |index|
79
- Definitions::Index.new(
80
- name: append_hash(index.name),
81
- columns: index.columns,
82
- unique: index.unique?,
83
- type: index.type,
84
- condition: index.condition
85
- )
67
+ def normalized_table
68
+ create_temporary_table!
69
+ read_temporary_table
86
70
  end
87
- end
88
71
 
89
- def rename_indices_back(indices)
90
- indices.map do |index|
91
- Definitions::Index.new(
92
- name: remove_hash(index.name),
93
- columns: index.columns,
94
- unique: index.unique?,
95
- type: index.type,
96
- condition: index.condition
72
+ private
73
+ def create_temporary_table!
74
+ operation = Changes::CreateTable.new(
75
+ table.with_name(temporary_table_name)
76
+ .with_fields(rename_types(table.fields))
77
+ .with_indices(rename_indices(table.indices))
97
78
  )
79
+
80
+ Runner.new([operation]).run!
98
81
  end
99
- end
100
82
 
101
- def temporary_table_name
102
- append_hash(table.name)
103
- end
83
+ def read_temporary_table
84
+ temporary_table = Reader.read_table(temporary_table_name)
104
85
 
105
- def append_hash(name)
106
- "#{name}_#{hash}"
107
- end
86
+ temporary_table.with_name(table.name)
87
+ .with_fields(rename_types_back(temporary_table.fields))
88
+ .with_indices(rename_indices_back(temporary_table.indices))
89
+ .with_foreign_keys(table.foreign_keys)
90
+ end
108
91
 
109
- def remove_hash(name)
110
- name.to_s.sub(/_#{Regexp.escape(hash)}$/, '').to_sym
111
- end
92
+ def rename_types(fields)
93
+ fields.map do |field|
94
+ if field.custom?
95
+ field.with_type(append_hash(field.type))
96
+ elsif field.array? && field.custom_element_type?
97
+ field.with_attribute(:element_type, append_hash(field.element_type.type).to_sym)
98
+ else
99
+ field
100
+ end
101
+ end
102
+ end
112
103
 
113
- def hash
114
- @hash ||= begin
115
- names = [table.name] + table.fields.map(&:name) + table.indices.map(&:name) + table.checks.map(&:name)
116
- Digest::MD5.hexdigest(names.join(','))[0..9]
104
+ def rename_types_back(fields)
105
+ fields.map do |field|
106
+ if field.custom?
107
+ field.with_type(remove_hash(field.type))
108
+ elsif field.array? && field.custom_element_type?
109
+ field.with_attribute(:element_type, remove_hash(field.element_type.type).to_sym)
110
+ else
111
+ field
112
+ end
113
+ end
114
+ end
115
+
116
+ def rename_indices(indices)
117
+ indices.map do |index|
118
+ index.with_name(append_hash(index.name))
119
+ end
120
+ end
121
+
122
+ def rename_indices_back(indices)
123
+ indices.map do |index|
124
+ index.with_name(remove_hash(index.name))
125
+ end
126
+ end
127
+
128
+ def temporary_table_name
129
+ append_hash(table.name)
130
+ end
131
+
132
+ def append_hash(name)
133
+ "#{name}_#{hash}"
134
+ end
135
+
136
+ def remove_hash(name)
137
+ name.to_s.sub(/_#{Regexp.escape(hash)}$/, '').to_sym
117
138
  end
118
139
  end
119
140
  end
@@ -298,7 +298,7 @@ SELECT extname
298
298
  Utils.rename_keys(
299
299
  Utils.filter_by_keys(data, :element_type, :element_custom_type_name)
300
300
  ) do |attributes|
301
- attributes[:of] = if attributes[:element_type] == 'USER-DEFINED'
301
+ attributes[:element_type] = if attributes[:element_type] == 'USER-DEFINED'
302
302
  attributes[:element_custom_type_name]
303
303
  else
304
304
  attributes[:element_type]
@@ -24,6 +24,8 @@ module DbSchema
24
24
  self.class.create_enum(change)
25
25
  when Changes::DropEnum
26
26
  self.class.drop_enum(change)
27
+ when Changes::AlterEnumValues
28
+ self.class.alter_enum_values(change)
27
29
  when Changes::CreateExtension
28
30
  self.class.create_extension(change)
29
31
  when Changes::DropExtension
@@ -31,11 +33,6 @@ module DbSchema
31
33
  end
32
34
  end
33
35
  end
34
-
35
- # Postgres doesn't allow modifying enums inside a transaction
36
- Utils.filter_by_class(changes, Changes::AddValueToEnum).each do |change|
37
- self.class.add_value_to_enum(change)
38
- end
39
36
  end
40
37
 
41
38
  private
@@ -44,8 +41,8 @@ module DbSchema
44
41
  changes,
45
42
  [
46
43
  Changes::CreateExtension,
47
- Changes::AddValueToEnum,
48
44
  Changes::DropForeignKey,
45
+ Changes::AlterEnumValues,
49
46
  Changes::CreateEnum,
50
47
  Changes::CreateTable,
51
48
  Changes::AlterTable,
@@ -59,8 +56,8 @@ module DbSchema
59
56
 
60
57
  class << self
61
58
  def create_table(change)
62
- DbSchema.connection.create_table(change.name) do
63
- change.fields.each do |field|
59
+ DbSchema.connection.create_table(change.table.name) do
60
+ change.table.fields.each do |field|
64
61
  if field.primary_key?
65
62
  primary_key(field.name)
66
63
  else
@@ -69,7 +66,7 @@ module DbSchema
69
66
  end
70
67
  end
71
68
 
72
- change.indices.each do |index|
69
+ change.table.indices.each do |index|
73
70
  index(
74
71
  index.columns_to_sequel,
75
72
  name: index.name,
@@ -79,7 +76,7 @@ module DbSchema
79
76
  )
80
77
  end
81
78
 
82
- change.checks.each do |check|
79
+ change.table.checks.each do |check|
83
80
  constraint(check.name, check.condition)
84
81
  end
85
82
  end
@@ -90,9 +87,9 @@ module DbSchema
90
87
  end
91
88
 
92
89
  def alter_table(change)
93
- DbSchema.connection.alter_table(change.name) do
90
+ DbSchema.connection.alter_table(change.table_name) do
94
91
  Utils.sort_by_class(
95
- change.fields + change.indices + change.checks,
92
+ change.changes,
96
93
  [
97
94
  DbSchema::Changes::DropPrimaryKey,
98
95
  DbSchema::Changes::DropCheckConstraint,
@@ -136,16 +133,16 @@ module DbSchema
136
133
  set_column_default(element.name, Runner.default_to_sequel(element.new_default))
137
134
  when Changes::CreateIndex
138
135
  add_index(
139
- element.columns_to_sequel,
140
- name: element.name,
141
- unique: element.unique?,
142
- type: element.type,
143
- where: element.condition
136
+ element.index.columns_to_sequel,
137
+ name: element.index.name,
138
+ unique: element.index.unique?,
139
+ type: element.index.type,
140
+ where: element.index.condition
144
141
  )
145
142
  when Changes::DropIndex
146
143
  drop_index([], name: element.name)
147
144
  when Changes::CreateCheckConstraint
148
- add_constraint(element.name, element.condition)
145
+ add_constraint(element.check.name, element.check.condition)
149
146
  when Changes::DropCheckConstraint
150
147
  drop_constraint(element.name)
151
148
  end
@@ -166,23 +163,45 @@ module DbSchema
166
163
  end
167
164
 
168
165
  def create_enum(change)
169
- DbSchema.connection.create_enum(change.name, change.values)
166
+ DbSchema.connection.create_enum(change.enum.name, change.enum.values)
170
167
  end
171
168
 
172
169
  def drop_enum(change)
173
170
  DbSchema.connection.drop_enum(change.name)
174
171
  end
175
172
 
176
- def add_value_to_enum(change)
177
- if change.add_to_the_end?
178
- DbSchema.connection.add_enum_value(change.enum_name, change.new_value)
179
- else
180
- DbSchema.connection.add_enum_value(change.enum_name, change.new_value, before: change.before)
173
+ def alter_enum_values(change)
174
+ change.enum_fields.each do |field_data|
175
+ DbSchema.connection.alter_table(field_data[:table_name]) do
176
+ set_column_type(field_data[:field_name], :VARCHAR)
177
+ set_column_default(field_data[:field_name], nil)
178
+ end
179
+ end
180
+
181
+ DbSchema.connection.drop_enum(change.enum_name)
182
+ DbSchema.connection.create_enum(change.enum_name, change.new_values)
183
+
184
+ change.enum_fields.each do |field_data|
185
+ DbSchema.connection.alter_table(field_data[:table_name]) do
186
+ field_type = if field_data[:array]
187
+ "#{change.enum_name}[]"
188
+ else
189
+ change.enum_name
190
+ end
191
+
192
+ set_column_type(
193
+ field_data[:field_name],
194
+ field_type,
195
+ using: "#{field_data[:field_name]}::#{field_type}"
196
+ )
197
+
198
+ set_column_default(field_data[:field_name], field_data[:new_default]) unless field_data[:new_default].nil?
199
+ end
181
200
  end
182
201
  end
183
202
 
184
203
  def create_extension(change)
185
- DbSchema.connection.run(%Q(CREATE EXTENSION "#{change.name}"))
204
+ DbSchema.connection.run(%Q(CREATE EXTENSION "#{change.extension.name}"))
186
205
  end
187
206
 
188
207
  def drop_extension(change)
@@ -1,3 +1,5 @@
1
+ require 'sequel/extensions/pg_array'
2
+
1
3
  module DbSchema
2
4
  module Validator
3
5
  class << self
@@ -10,10 +12,29 @@ module DbSchema
10
12
  end
11
13
 
12
14
  table.fields.each do |field|
13
- if field.is_a?(Definitions::Field::Custom)
14
- unless schema.enums.map(&:name).include?(field.type)
15
+ if field.custom?
16
+ type = schema.enums.find { |enum| enum.name == field.type }
17
+
18
+ if type.nil?
15
19
  error_message = %(Field "#{table.name}.#{field.name}" has unknown type "#{field.type}")
16
20
  errors << error_message
21
+ elsif !field.default.nil? && !type.values.include?(field.default.to_sym)
22
+ errors << %(Field "#{table.name}.#{field.name}" has invalid default value "#{field.default}" (valid values are #{type.values.map(&:to_s)}))
23
+ end
24
+ elsif field.array? && field.custom_element_type?
25
+ type = schema.enums.find { |enum| enum.name == field.element_type.type }
26
+
27
+ if type.nil?
28
+ error_message = %(Array field "#{table.name}.#{field.name}" has unknown element type "#{field.element_type.type}")
29
+ errors << error_message
30
+ elsif !field.default.nil?
31
+ default_array = Sequel::Postgres::PGArray::Parser.new(field.default).parse.map(&:to_sym)
32
+ invalid_values = default_array - type.values
33
+
34
+ if invalid_values.any?
35
+ error_message = %(Array field "#{table.name}.#{field.name}" has invalid default value #{default_array.map(&:to_s)} (valid values are #{type.values.map(&:to_s)}))
36
+ errors << error_message
37
+ end
17
38
  end
18
39
  end
19
40
  end
@@ -1,3 +1,3 @@
1
1
  module DbSchema
2
- VERSION = '0.2.4'
2
+ VERSION = '0.2.5'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vsevolod Romashov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-12-11 00:00:00.000000000 Z
11
+ date: 2017-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -240,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
240
240
  version: '0'
241
241
  requirements: []
242
242
  rubyforge_project:
243
- rubygems_version: 2.5.1
243
+ rubygems_version: 2.5.2
244
244
  signing_key:
245
245
  specification_version: 4
246
246
  summary: Declarative database schema definition.