db_schema 0.2.4 → 0.2.5

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