db_schema 0.3.rc1 → 0.3

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: 60b581e6c318ed7bf979417435dce0ec4960a978
4
- data.tar.gz: 0f296ac7525965a1bd3026e76cfa0cd2bb24b821
3
+ metadata.gz: 216fab4c9e20545d7653f46cdb5fe9ce5528a68a
4
+ data.tar.gz: a3115a63fd0a6da8289d1b7e6848e57620785563
5
5
  SHA512:
6
- metadata.gz: 19375ec9fac1576a3c8164de6584ceacc21019d111bb2e584850713d89936a3714c394c0338c16eb5873c2f86d319286136690fad8cb28b2748dc911c625f3e8
7
- data.tar.gz: b80cb8b4a448e5f0014763d89b1e2e2e376d48cd84f03380c55c823a71efe952d7187242888f021b27ccdbf3f85191b5286cefa11fd31d40ce7a3757f3d7dff4
6
+ metadata.gz: 2ef95f7dcf532b83a288423f0626943479a4353797e281e0df5aa0788508811b19de3382576a7ed78f40c0b664bcd7f455ce12c335cf8c7a59cbdf33eccf093c
7
+ data.tar.gz: 660d11cc3e03fb68b2227ebe4f6febb544d17454d842daa6ad6d8d5cc8ec8af550f3e6ef936c8a7cf18f177ec1edfc56c792e4c0f51407b509868acd39b16b0d
data/.travis.yml CHANGED
@@ -2,7 +2,9 @@ dist: trusty
2
2
  sudo: false
3
3
  language: ruby
4
4
  rvm:
5
- - 2.3.3
5
+ - 2.2.8
6
+ - 2.3.5
7
+ - 2.4.2
6
8
  services:
7
9
  - postgresql
8
10
  addons:
data/Guardfile CHANGED
@@ -13,5 +13,6 @@ guard :rspec, cmd: 'bundle exec rspec', all_on_start: true do
13
13
  dsl.watch_spec_files_for(ruby.lib_files)
14
14
  watch(%r{lib/db_schema/definitions\.rb}) { rspec.spec_dir }
15
15
  watch(%r{lib/db_schema/definitions/.*\.rb}) { rspec.spec_dir }
16
+ watch(%r{lib/db_schema/operations\.rb}) { rspec.spec_dir }
16
17
  watch('lib/db_schema/utils.rb') { rspec.spec_dir }
17
18
  end
data/README.md CHANGED
@@ -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.3.rc1'
56
+ gem 'db_schema', '~> 0.3.0'
57
57
  ```
58
58
 
59
59
  And then execute:
@@ -65,521 +65,164 @@ $ bundle
65
65
  Or install it yourself as:
66
66
 
67
67
  ``` sh
68
- $ gem install db_schema --prerelease
68
+ $ gem install db_schema
69
69
  ```
70
70
 
71
71
  ## Usage
72
72
 
73
- You define your schema with a special DSL; you can put it in `db/schema.rb` file or anywhere you want. Be sure to keep this file under version control.
73
+ First you need to configure DbSchema so it knows how to connect to your database. This should happen
74
+ in a file that is loaded during the application boot process - a Rails or Hanami initializer would do.
74
75
 
75
- DbSchema DSL looks like this:
76
-
77
- ``` ruby
78
- DbSchema.describe do |db|
79
- db.table :users do |t|
80
- t.primary_key :id
81
- t.varchar :email, null: false, unique: true
82
- t.varchar :password_digest, length: 40
83
- t.timestamptz :created_at
84
- t.timestamptz :updated_at
85
- end
86
- end
87
- ```
88
-
89
- Before DbSchema connects to the database you need to configure it:
76
+ DbSchema can be configured with a call to `DbSchema.configure`:
90
77
 
91
78
  ``` ruby
79
+ # config/initializers/db_schema.rb
92
80
  DbSchema.configure(
93
- adapter: 'postgresql',
94
- database: 'my_database',
95
- user: 'bob',
96
- password: 'secret'
97
- )
98
-
99
- # or in Rails
100
- DbSchema.configure_from_yaml(
101
- Rails.root.join('config', 'database.yml'),
102
- Rails.env
81
+ database: 'my_app_development'
103
82
  )
104
83
  ```
105
84
 
106
- Then you can load your schema definition (it is executable - it instantly applies itself to your database):
107
-
108
- ``` ruby
109
- load 'path/to/schema.rb'
110
- ```
111
-
112
- In order to get an always-up-to-date database schema in development and test environments you need to load the schema definition when your application is starting up. For instance, in Rails an initializer would be a good place to do that.
113
-
114
- On the other hand, in production environment this can cause race condition problems as your schema can be applied concurrently by different worker processes (this also applies to staging and any other environments where the application is being run by multi-worker servers); therefore it is wiser to disable schema auto loading in such environments and run it from a rake task on each deploy.
85
+ There is also a Rails' `database.yml`-compatible `configure_from_yaml` method. DbSchema configuration
86
+ is discussed in detail [here](https://github.com/7even/db_schema/wiki/Configuration).
115
87
 
116
- Here's an initializer example for a Rails app:
88
+ After DbSchema is configured you can load your schema definition file:
117
89
 
118
90
  ``` ruby
119
91
  # config/initializers/db_schema.rb
120
- DbSchema.configure_from_yaml(
121
- Rails.root.join('config', 'database.yml'),
122
- Rails.env
123
- )
124
92
 
125
- if Rails.env.development? || Rails.env.test?
126
- load Rails.root.join('db', 'schema.rb')
127
- end
93
+ # ...
94
+ load application_root.join('db/schema.rb')
128
95
  ```
129
96
 
130
- And the rake task:
131
-
132
- ``` ruby
133
- # lib/tasks/db_schema.rake
134
- namespace :db do
135
- namespace :schema do
136
- desc 'Apply database schema'
137
- task apply: :environment do
138
- load Rails.root.join('db', 'schema.rb')
139
- end
140
- end
141
- end
142
- ```
143
-
144
- Then you just call `rake db:schema:apply` from your deploy script before restarting the app.
145
-
146
- If your production setup doesn't include multiple workers starting simultaneously (for example if you run one Puma worker per docker container and restart containers one by one on deploy) you can go the simple way and just `load Rails.root.join('db', 'schema.rb')` in any environment without a separate rake task. The first DbSchema run will apply the schema while the subsequent ones will see there's nothing left to do.
147
-
148
- ## DSL
149
-
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
- `#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`.
97
+ This `db/schema.rb` file will contain a description of your database structure
98
+ (you can choose any filename you want). When you load this file it instantly
99
+ applies the described structure to your database. Be sure to keep this file
100
+ under version control as it will be a single source of truth about
101
+ the database structure.
154
102
 
155
103
  ``` ruby
104
+ # db/schema.rb
156
105
  DbSchema.describe do |db|
157
- db.extension :hstore
158
-
159
106
  db.table :users do |t|
160
107
  t.primary_key :id
161
- t.varchar :email, null: false, unique: true
162
- t.varchar :password, null: false
163
- t.varchar :name, null: false
164
- t.integer :age
165
- t.user_status :status, null: false, default: 'registered'
166
- t.hstore :tracking, null: false, default: ''
167
- end
168
-
169
- db.enum :user_status, [:registered, :confirmed_email, :subscriber]
170
-
171
- db.table :posts do |t|
172
- t.primary_key :id
173
- t.integer :user_id, null: false, index: true, references: :users
174
- t.varchar :title, null: false, length: 50
175
- t.text :content
176
- t.array :tags, of: :varchar
108
+ t.varchar :email, null: false, unique: true
109
+ t.varchar :password_digest, length: 40
110
+ t.timestamptz :created_at
111
+ t.timestamptz :updated_at
177
112
  end
178
113
  end
179
114
  ```
180
115
 
181
- ### Tables
182
-
183
- Tables are described with the `#table` method; you pass it the name of the table and describe the table structure in the block:
184
-
185
- ``` ruby
186
- db.table :users do |t|
187
- t.varchar :email
188
- t.varchar :password
189
- end
190
- ```
191
-
192
- #### Fields
193
-
194
- You can define a field of any type by calling the corresponding method inside the table block passing it the field name and it's attributes. Most of the attributes are optional.
195
-
196
- Here's an example table with various kinds of data:
197
-
198
- ``` ruby
199
- db.table :people do |t|
200
- t.varchar :first_name, length: 50, null: false
201
- t.varchar :last_name, length: 60, null: false
202
- t.integer :age
203
- t.numeric :salary, precision: 10, scale: 2
204
- t.text :about
205
- t.date :birthday
206
- t.boolean :developer
207
- t.inet :ip_address
208
- t.jsonb :preferences, default: '{}'
209
- t.array :interests, of: :varchar
210
- t.numrange :salary_expectations
211
-
212
- t.timestamptz :created_at
213
- t.timestamptz :updated_at
214
- end
215
- ```
216
-
217
- Passing `null: false` to the field definition makes it `NOT NULL`; passing some value under the `:default` key makes it the default value. You can use `String`s as SQL strings, `Fixnum`s as integers, `Float`s as floating point numbers, `true` & `false` as their SQL counterparts, `Date`s as SQL dates and `Time`s as timestamps. A symbol passed as a default is a special case: it is interpreted as an SQL expression so `t.timestamp :created_at, default: :'now()'` defines a field with a default value of `NOW()`.
218
-
219
- Other attributes are type specific, like `:length` for varchars; the following table lists them all (values in parentheses are default attribute values).
220
-
221
- | Type | Attributes |
222
- | ------------- | ---------------- |
223
- | `smallint` | |
224
- | `integer` | |
225
- | `bigint` | |
226
- | `numeric` | precision, scale |
227
- | `real` | |
228
- | `float` | |
229
- | `money` | |
230
- | `char` | length(1) |
231
- | `varchar` | length |
232
- | `text` | |
233
- | `bytea` | |
234
- | `timestamp` | |
235
- | `timestamptz` | |
236
- | `date` | |
237
- | `time` | |
238
- | `timetz` | |
239
- | `interval` | fields |
240
- | `boolean` | |
241
- | `point` | |
242
- | `line` | |
243
- | `lseg` | |
244
- | `box` | |
245
- | `path` | |
246
- | `polygon` | |
247
- | `circle` | |
248
- | `cidr` | |
249
- | `inet` | |
250
- | `macaddr` | |
251
- | `bit` | length(1) |
252
- | `varbit` | length |
253
- | `tsvector` | |
254
- | `tsquery` | |
255
- | `uuid` | |
256
- | `json` | |
257
- | `jsonb` | |
258
- | `array` | of |
259
- | `int4range` | |
260
- | `int8range` | |
261
- | `numrange` | |
262
- | `tsrange` | |
263
- | `tstzrange` | |
264
- | `daterange` | |
265
- | `chkpass` | |
266
- | `citext` | |
267
- | `cube` | |
268
- | `hstore` | |
269
- | `ean13` | |
270
- | `isbn13` | |
271
- | `ismn13` | |
272
- | `issn13` | |
273
- | `isbn` | |
274
- | `ismn` | |
275
- | `issn` | |
276
- | `upc` | |
277
- | `ltree` | |
278
- | `seg` | |
279
-
280
- The `of` attribute of the array type is the only required attribute (you need to specify the array element type here); other attributes either have default values or can be omitted at all.
281
-
282
- You can also use your custom types in the same way: `t.user_status :status` creates a field called `status` with `user_status` type. Custom types are explained in a later section of this document.
283
-
284
- Primary key is a special case; currently when you create a primary key with DbSchema you get a NOT NULL autoincrementing (by a sequence) integer field with a primary key constraint. There is no way to change the primary key field type or make a complex primary key at the moment; this is planned for future versions of DbSchema.
285
-
286
- Primary keys are created with the `#primary_key` method:
287
-
288
- ``` ruby
289
- db.table :posts do |t|
290
- t.primary_key :id
291
- t.varchar :title
292
- end
293
- ```
294
-
295
- **Important: you can't rename a table or a column just by changing it's name in the schema definition - this will result in a column with the old name being deleted and a column with the new name being added; all data in that table or column will be lost.**
296
-
297
- #### Indexes
298
-
299
- Indexes are created using the `#index` method: you pass it the field name you want to index:
300
-
301
- ``` ruby
302
- db.table :users do |t|
303
- t.varchar :email
304
- t.index :email
305
- end
306
- ```
307
-
308
- Unique indexes are created with `unique: true`:
309
-
310
- ``` ruby
311
- t.index :email, unique: true
312
- ```
313
-
314
- Simple one-field indexes can be created with `index: true` and `unique: true` options passed to the field definition method so
315
-
316
- ``` ruby
317
- db.table :users do |t|
318
- t.varchar :name, index: true
319
- t.varchar :email, unique: true
320
- end
321
- ```
322
-
323
- is essentially the same as
324
-
325
- ``` ruby
326
- db.table :users do |t|
327
- t.varchar :name
328
- t.varchar :email
329
-
330
- t.index :name
331
- t.index :email, unique: true
332
- end
333
- ```
334
-
335
- Passing several field names to `#index` makes a multiple index:
336
-
337
- ``` ruby
338
- db.table :users do |t|
339
- t.varchar :first_name
340
- t.varchar :last_name
341
-
342
- t.index :first_name, :last_name
343
- end
344
- ```
345
-
346
- If you want to specify a custom name for your index, you can pass it in the `:name` option:
347
-
348
- ``` ruby
349
- t.index :first_name, :last_name, name: :username_index
350
- ```
351
-
352
- Otherwise the index name will be generated as `"#{table_name}_#{field_names.join('_')}_index"` so the index above will be called `users_first_name_last_name_index`.
353
-
354
- You can specify the order of each field in your index - it's either `ASC` (`:asc`, the default), `DESC` (`:desc`), `ASC NULLS FIRST` (`:asc_nulls_first`), or `DESC NULLS LAST` (`:desc_nulls_last`). It looks like this:
355
-
356
- ``` ruby
357
- db.table :some_table do |t|
358
- t.integer :col1
359
- t.integer :col2
360
- t.integer :col3
361
- t.integer :col4
362
-
363
- t.index col1: :asc, col2: :desc, col3: :asc_nulls_first, col4: :desc_nulls_last
364
- end
365
- ```
366
-
367
- By default B-tree indexes are created; if you need an index of a different type you can pass it in the `:using` option:
116
+ Database schema definition DSL is documented [here](https://github.com/7even/db_schema/wiki/Schema-definition-DSL).
368
117
 
369
- ``` ruby
370
- db.table :users do |t|
371
- t.array :interests, of: :varchar
372
- t.index :interests, using: :gin
373
- end
374
- ```
375
-
376
- You can also create a partial index if you pass some condition as SQL string in the `:where` option:
377
-
378
- ``` ruby
379
- db.table :users do |t|
380
- t.varchar :email
381
- t.index :email, unique: true, where: 'email IS NOT NULL'
382
- end
383
- ```
384
-
385
- If you need an index on expression you can use the same syntax replacing column names with SQL strings containing the expressions:
386
-
387
- ``` ruby
388
- db.table :users do |t|
389
- t.timestamp :created_at
390
- t.index 'date(created_at)'
391
- end
392
- ```
393
-
394
- Expression indexes syntax allows specifying an order exactly like in a common index on table fields - just use a hash form like `t.index 'date(created_at)' => :desc`. You can also use an expression in a multiple index.
118
+ If you want to analyze your database structure in any way from your app (e.g. defining methods
119
+ with `#define_method` for each enum value) you can use `DbSchema.current_schema` - it returns
120
+ a cached copy of the database structure as a `DbSchema::Definitions::Schema` object which you
121
+ can query in different ways. It is available after the schema was applied by DbSchema
122
+ (`DbSchema.describe` remembers the current schema of the database and exposes it
123
+ at `.current_schema`). Documentation for schema analysis DSL can be found
124
+ [here](https://github.com/7even/db_schema/wiki/Schema-analysis-DSL).
395
125
 
396
- #### Foreign keys
126
+ ### Production setup
397
127
 
398
- The `#foreign_key` method defines a foreign key. In it's minimal form it takes a referencing field name and referenced table name:
128
+ In order to get an always-up-to-date database schema in development and test environments
129
+ you need to load the schema definition when your application is starting up. But if you use
130
+ an application server with multiple workers (puma in cluster mode, unicorn) in other environments
131
+ (production, staging) you may get yourself into situation when different workers simultaneously
132
+ run DbSchema code applying the same changes to your database. If this is the case you will need
133
+ to disable loading the schema definition in those environments and do that from a rake task called
134
+ from your deploy script:
399
135
 
400
136
  ``` ruby
401
- db.table :users do |t|
402
- t.primary_key :id
403
- t.varchar :name
404
- end
405
-
406
- db.table :posts do |t|
407
- t.integer :user_id
408
- t.varchar :title
409
-
410
- t.foreign_key :user_id, references: :users
411
- end
412
- ```
413
-
414
- The syntax above assumes that this foreign key references the primary key. If you need to reference another field you can pass a 2-element array in `:references` option, the first element being table name and the second being field name:
415
-
416
- ``` ruby
417
- db.table :users do |t|
418
- t.varchar :name
419
- t.index :name, unique: true # you can only reference either primary keys or unique columns
420
- end
421
-
422
- db.table :posts do |t|
423
- t.varchar :username
424
- t.foreign_key :username, references: [:users, :name]
425
- end
426
- ```
427
-
428
- DbSchema also provides a short syntax for simple one-column foreign keys - just pass the `:references` option to the field definition:
429
-
430
- ``` ruby
431
- db.table :posts do |t|
432
- t.integer :user_id, references: :users
433
- t.varchar :username, references: [:users, :name]
434
- end
435
- ```
436
-
437
- As with indexes, you can pass your custom name in the `:name` option; default foreign key name looks like `"#{table_name}_#{fkey_fields.first}_fkey"`.
438
-
439
- You can also define a composite foreign key consisting of (and referencing) multiple columns; just list them all:
440
-
441
- ``` ruby
442
- db.table :table_a do |t|
443
- t.integer :col1
444
- t.integer :col2
445
- t.index :col1, :col2, unique: true
446
- end
447
-
448
- db.table :table_b do |t|
449
- t.integer :a_col1
450
- t.integer :a_col2
451
- t.foreign_key :a_col1, :a_col2, references: [:table_a, :col1, :col2]
452
- end
453
- ```
454
-
455
- There are 3 more options to the `#foreign_key` method: `:on_update`, `:on_delete` and `:deferrable`. First two define an action that will be taken when a referenced column is changed or the whole referenced row is deleted, respectively; you can set these to one of `:no_action` (the default), `:restrict`, `:cascade`, `:set_null` or `:set_default`. See [PostgreSQL documentation](https://www.postgresql.org/docs/current/static/ddl-constraints.html#DDL-CONSTRAINTS-FK) for more information.
456
-
457
- Passing `deferrable: true` defines a foreign key that is checked at the end of transaction.
458
-
459
- #### Check constraints
460
-
461
- A check constraint is like a validation on the database side: it checks if the inserted/updated row has valid values.
462
-
463
- To define a check constraint you can use the `#check` method passing it the constraint name (no auto-generated names here, sorry) and the condition that must be satisfied, in a form of SQL string.
464
-
465
- ``` ruby
466
- db.table :users do |t|
467
- t.primary_key :id
468
- t.varchar :name
469
- t.integer :age, null: false
470
-
471
- t.check :valid_age, 'age >= 18'
472
- end
473
- ```
474
-
475
- As with indexes and foreign keys, DbSchema has a short syntax for simple check constraints - a `:check` option in the method definition:
476
-
477
- ``` ruby
478
- db.table :products do |t|
479
- t.primary_key :id
480
- t.text :name, null: false
481
- t.numeric :price, check: 'price > 0'
482
- end
483
- ```
484
-
485
- ### Enum types
486
-
487
- PostgreSQL allows developers to create custom enum types; value of enum type is one of a fixed set of values stored in the type definition.
488
-
489
- Enum types are declared with the `#enum` method (note that you must call it from the top level of your schema and not from within some table definition):
490
-
491
- ``` ruby
492
- db.enum :user_status, [:registered, :confirmed_email]
493
- ```
494
-
495
- Then you can create fields of that type exactly as you would create a field of any built-in type - just call the method with the same name as the type you defined:
137
+ # config/initializers/db_schema.rb
138
+ DbSchema.configure(url: ENV['DATABASE_URL'])
496
139
 
497
- ``` ruby
498
- db.table :users do |t|
499
- t.user_status :status, default: 'registered'
140
+ if ENV['APP_ENV'] == 'development' || ENV['APP_ENV'] == 'test'
141
+ load application_root.join('db/schema.rb')
500
142
  end
501
- ```
502
-
503
- Arrays of enums are also supported - they are described just like arrays of any other element type:
504
-
505
- ``` ruby
506
- db.enum :user_role, [:user, :manager, :admin]
507
143
 
508
- db.table :users do |t|
509
- t.array :roles, of: :user_role, default: '{user}'
144
+ # lib/tasks/db_schema.rake
145
+ namespace :db do
146
+ namespace :schema do
147
+ desc 'Apply database schema'
148
+ task apply: :environment do
149
+ load application_root.join('db/schema.rb')
150
+ end
151
+ end
510
152
  end
511
153
  ```
512
154
 
513
- ### Extensions
514
-
515
- PostgreSQL has a [wide variety](https://www.postgresql.org/docs/9.5/static/contrib.html) of extensions providing additional data types, functions and operators. You can use DbSchema to add and remove extensions in your database:
516
-
517
- ``` ruby
518
- db.extension :hstore
519
- ```
520
-
521
- *Note that adding and removing extensions in Postgres requires superuser privileges.*
522
-
523
- ## Configuration
524
-
525
- DbSchema must be configured prior to applying the schema. There are 2 methods you can use for that: `configure` and `configure_from_yaml`.
526
-
527
- ### DbSchema.configure
528
-
529
- `configure` is a generic method that receives a hash with all configuration options:
530
-
531
- ``` ruby
532
- DbSchema.configure(
533
- adapter: 'postgresql',
534
- host: ENV['db_host'],
535
- port: ENV['db_port'],
536
- database: ENV['db_name'],
537
- user: ENV['db_user'],
538
- password: ENV['db_password']
539
- )
540
- ```
541
-
542
- ### DbSchema.configure_from_yaml
543
-
544
- `configure_from_yaml` is designed to use with Rails so you don't have to duplicate database connection settings from your `database.yml` in DbSchema configuration. Pass it the full path to your `database.yml` file and your current application environment (`development`, `production` etc), and it will read the db connection settings from that file.
545
-
546
- ``` ruby
547
- DbSchema.configure_from_yaml(Rails.root.join('config', 'database.yml'), Rails.env)
548
- ```
549
-
550
- If you need to specify other options you can simply pass them as keyword arguments after the environment:
551
-
552
- ``` ruby
553
- DbSchema.configure_from_yaml(
554
- Rails.root.join('config', 'database.yml'),
555
- Rails.env,
556
- dry_run: true
557
- )
558
- ```
559
-
560
- ### Configuration options
561
-
562
- All configuration options are described in the following table:
563
-
564
- | Option | Default value | Description |
565
- | ----------- | ------------- | ------------------------------------------------ |
566
- | adapter | `'postgres'` | Database adapter |
567
- | host | `'localhost'` | Database host |
568
- | port | `5432` | Database port |
569
- | database | (no default) | Database name |
570
- | user | `nil` | Database user |
571
- | password | `''` | Database password |
572
- | log_changes | `true` | When true, schema changes are logged |
573
- | post_check | `true` | When true, database schema is checked afterwards |
574
- | dry_run | `false` | When true, no operations are actually made |
575
-
576
- By default DbSchema logs the changes it applies to your database; you can disable that by setting `log_changes` to false.
577
-
578
- DbSchema provides an opt-out post-run schema check; it ensures that the schema was applied correctly and there are no remaining differences between your `schema.rb` and the actual database schema. The corresponding `post_check` option is likely to become off by default when DbSchema becomes more stable and battle-tested.
579
-
580
- There is also a dry run mode which does not apply the changes to your database - it just logs the necessary changes (if you leave `log_changes` set to `true`). Post check is also skipped in that case.
155
+ Then you just call `rake db:schema:apply` from your deploy script before restarting the app.
581
156
 
582
- Dry run may be useful while you are building your schema definition for an existing app; adjust your `schema.rb` and apply it in dry run mode until it fits your database and next dry run doesn't report any changes. Don't forget to turn `dry_run` off afterwards!
157
+ If your production setup doesn't include multiple application processes starting simultaneously
158
+ (for example if you run one Puma process per docker container and replace containers
159
+ successively on deploy) you can go the simple way and just
160
+ `load application_root.join('db/schema.rb')` in any environment right from the initializer.
161
+ The first puma process will apply the schema while the subsequent ones will see there's nothing
162
+ left to do.
163
+
164
+ ### How it works
165
+
166
+ When you call `DbSchema.describe` with a block that describes the database structure for your
167
+ application DbSchema compares this *desired* structure with the *actual* structure your
168
+ database has at the moment.
169
+
170
+ The database structure is a tree; it's top-level node is a `Schema` object that has several
171
+ child nodes - tables, enums and extensions. `Table` objects in turn have child nodes describing
172
+ everything that belongs to a table - fields, indexes etc. The full tree structure looks like this:
173
+
174
+ * Schema
175
+ * Table
176
+ * Field
177
+ * Index
178
+ * Check constraint
179
+ * Foreign key
180
+ * Enum type
181
+ * Extension
182
+
183
+ DbSchema compares two structure trees by finding *objects with matching names* in both trees.
184
+ *Desired* objects that don't have a match in the *actual* schema produce a **create** operation,
185
+ while *actual* objects that don't have a counterpart in the *desired* schema generate a **drop**
186
+ operation.
187
+
188
+ Then each matching pair is compared by attributes and child objects:
189
+
190
+ * if the objects differ in their attributes they make an **alter** operation if it is supported
191
+ for that kind of object (that's tables, fields and enum types at the moment) or a pair of **drop**
192
+ and **create** operations if it's not
193
+ * if the objects differ in their child nodes then the process continues recursively for these
194
+ two sets of child objects
195
+ * if the objects are identical no operations take place on them
196
+
197
+ Then DbSchema runs all these operations inside a transaction.
198
+
199
+ For example if *desired* schema has tables `users`, `cities` and `posts`, and *actual* schema
200
+ only has `users` and `posts` (where `posts` lack a couple of fields compared to the *desired*
201
+ version), then the `cities` table will be created and new fields will be added to `posts`.
202
+
203
+ The fact that objects are compared by name implies a very important detail: **you can't rename
204
+ anything just by changing the name in the definition.**
205
+
206
+ Imagine that you have a `foo` table in your schema definition and an identical table in the database.
207
+ If you change it's name to `bar` in the definition and run your app DbSchema will see there
208
+ is a `bar` table in the *desired* schema but no match in the database so a new `bar` table will be created;
209
+ and since there is a `foo` table in the *actual* schema without a counterpart in the *desired*
210
+ schema DbSchema will drop this table. Of course all data in the `foo` table will be lost.
211
+
212
+ This can be solved with conditional migrations - a tool that allows you to make some changes to your database
213
+ *before* the schema comparison described earlier takes control. A migration describes all required operations
214
+ in an imperative manner (`rename_table`, `drop_index` etc) with a dedicated DSL. DbSchema doesn't store
215
+ anything about migrations in the database though (as opposed to ActiveRecord or Sequel migrations);
216
+ instead you have to provide some conditions required to run the migration (the goal here is to come up with
217
+ conditions that a) will only trigger if the migration wasn't applied yet and b) are necessary for the
218
+ migration to work) - like "rename the `users`
219
+ table to `people` only if the database has a `users` table" (DbSchema also provides
220
+ a [simple DSL](https://github.com/7even/db_schema/wiki/Schema-analysis-DSL) for schema analysis).
221
+ This way the migration won't be applied again and the whole DbSchema process stays idempotent.
222
+ Also you don't have to keep these migrations forever - once a migration is applied to databases
223
+ in all environments you can safely delete it (though you can give your teammates a week or two to keep up).
224
+
225
+ Conditional migrations are described [here](https://github.com/7even/db_schema/wiki/Conditional-Migrations).
583
226
 
584
227
  ## Known problems and limitations
585
228
 
@@ -587,7 +230,6 @@ Dry run may be useful while you are building your schema definition for an exist
587
230
  * array element type attributes are not supported
588
231
  * precision in all date/time types isn't supported
589
232
  * no support for databases other than PostgreSQL
590
- * no support for renaming tables & columns
591
233
 
592
234
  ## Development
593
235
 
@@ -15,8 +15,13 @@ 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
+ when ::DbSchema::Definitions::NullTable,
19
+ ::DbSchema::Definitions::NullField,
20
+ ::DbSchema::Definitions::NullIndex,
21
+ ::DbSchema::Definitions::NullCheckConstraint,
22
+ ::DbSchema::Definitions::NullForeignKey,
23
+ ::DbSchema::Definitions::NullEnum
24
+ :dbschema_null_object
20
25
  when ::DbSchema::Definitions::Table
21
26
  :dbschema_table
22
27
  when ::DbSchema::Definitions::Field::Custom
@@ -43,42 +48,28 @@ if defined?(AwesomePrint)
43
48
  :dbschema_alter_table
44
49
  when ::DbSchema::Operations::CreateColumn
45
50
  :dbschema_create_column
46
- when ::DbSchema::Operations::DropColumn
51
+ when ::DbSchema::Operations::ColumnOperation
47
52
  :dbschema_column_operation
48
- when ::DbSchema::Operations::RenameTable,
49
- ::DbSchema::Operations::RenameColumn
53
+ when ::DbSchema::Operations::RenameOperation
50
54
  :dbschema_rename
51
55
  when ::DbSchema::Operations::AlterColumnType
52
56
  :dbschema_alter_column_type
53
- when ::DbSchema::Operations::CreatePrimaryKey,
54
- ::DbSchema::Operations::DropPrimaryKey,
55
- ::DbSchema::Operations::AllowNull,
56
- ::DbSchema::Operations::DisallowNull
57
- :dbschema_column_operation
58
57
  when ::DbSchema::Operations::AlterColumnDefault
59
58
  :dbschema_alter_column_default
60
59
  when ::DbSchema::Operations::CreateIndex
61
60
  :dbschema_create_index
62
- when ::DbSchema::Operations::DropIndex
63
- :dbschema_column_operation
64
61
  when ::DbSchema::Operations::CreateCheckConstraint
65
62
  :dbschema_create_check_constraint
66
- when ::DbSchema::Operations::DropCheckConstraint
67
- :dbschema_column_operation
68
63
  when ::DbSchema::Operations::CreateForeignKey
69
64
  :dbschema_create_foreign_key
70
65
  when ::DbSchema::Operations::DropForeignKey
71
66
  :dbschema_drop_foreign_key
72
67
  when ::DbSchema::Operations::CreateEnum
73
68
  :dbschema_create_enum
74
- when ::DbSchema::Operations::DropEnum
75
- :dbschema_column_operation
76
69
  when ::DbSchema::Operations::AlterEnumValues
77
70
  :dbschema_alter_enum_values
78
71
  when ::DbSchema::Operations::CreateExtension
79
72
  :dbschema_create_extension
80
- when ::DbSchema::Operations::DropExtension
81
- :dbschema_column_operation
82
73
  else
83
74
  cast_without_dbschema(object, type)
84
75
  end
@@ -104,8 +95,8 @@ if defined?(AwesomePrint)
104
95
  "#<DbSchema::Definitions::Table #{object.name.ai} #{data_string}>"
105
96
  end
106
97
 
107
- def awesome_dbschema_null_table(object)
108
- '#<DbSchema::Definitions::NullTable>'
98
+ def awesome_dbschema_null_object(object)
99
+ "#<#{object.class}>"
109
100
  end
110
101
 
111
102
  def awesome_dbschema_field(object)
@@ -4,7 +4,7 @@ module DbSchema
4
4
  class Configuration
5
5
  include Dry::Equalizer(:params)
6
6
 
7
- DEFAULT_VALUES = {
7
+ DEFAULT_PARAMS = {
8
8
  adapter: 'postgres',
9
9
  host: 'localhost',
10
10
  port: 5432,
@@ -16,12 +16,18 @@ module DbSchema
16
16
  post_check: true
17
17
  }.freeze
18
18
 
19
- def initialize(params = {})
20
- @params = DEFAULT_VALUES.merge(params)
19
+ def initialize(params = DEFAULT_PARAMS)
20
+ @params = params
21
21
  end
22
22
 
23
23
  def merge(new_params)
24
- Configuration.new(@params.merge(new_params))
24
+ params = [
25
+ @params,
26
+ Configuration.params_from_url(new_params[:url]),
27
+ Utils.filter_by_keys(new_params, *DEFAULT_PARAMS.keys)
28
+ ].reduce(:merge)
29
+
30
+ Configuration.new(params)
25
31
  end
26
32
 
27
33
  [:adapter, :host, :port, :database, :user, :password].each do |param_name|
@@ -42,6 +48,22 @@ module DbSchema
42
48
  @params[:post_check]
43
49
  end
44
50
 
51
+ class << self
52
+ def params_from_url(url_string)
53
+ return {} if url_string.nil?
54
+ url = URI.parse(url_string)
55
+
56
+ Utils.remove_nil_values(
57
+ adapter: url.scheme,
58
+ host: url.host,
59
+ port: url.port,
60
+ database: url.path.sub(/\A\//, ''),
61
+ user: url.user,
62
+ password: url.password
63
+ )
64
+ end
65
+ end
66
+
45
67
  protected
46
68
  attr_reader :params
47
69
  end
@@ -15,7 +15,9 @@ module DbSchema
15
15
  end
16
16
 
17
17
  class NullEnum < Enum
18
- def initialize; end
18
+ def initialize
19
+ @values = []
20
+ end
19
21
  end
20
22
  end
21
23
  end
@@ -35,7 +35,10 @@ module DbSchema
35
35
  end
36
36
 
37
37
  class NullForeignKey < ForeignKey
38
- def initialize; end
38
+ def initialize
39
+ @fields = []
40
+ @keys = []
41
+ end
39
42
  end
40
43
  end
41
44
  end
@@ -109,7 +109,12 @@ module DbSchema
109
109
  end
110
110
 
111
111
  class NullTable < Table
112
- def initialize; end
112
+ def initialize
113
+ @fields = []
114
+ @indices = []
115
+ @checks = []
116
+ @foreign_keys = []
117
+ end
113
118
  end
114
119
  end
115
120
  end
@@ -15,7 +15,7 @@ module DbSchema
15
15
  end
16
16
 
17
17
  def run!(connection)
18
- migration.body.call(BodyYielder.new(connection)) unless migration.body.nil?
18
+ migration.body.call(BodyYielder.new(connection), connection) unless migration.body.nil?
19
19
  end
20
20
 
21
21
  class BodyYielder
@@ -155,6 +155,10 @@ module DbSchema
155
155
  run Operations::DropEnum.new(name)
156
156
  end
157
157
 
158
+ def rename_enum(from, to:)
159
+ run Operations::RenameEnum.new(old_name: from, new_name: to)
160
+ end
161
+
158
162
  def create_extension(name)
159
163
  run Operations::CreateExtension.new(Definitions::Extension.new(name))
160
164
  end
@@ -167,8 +171,7 @@ module DbSchema
167
171
  run Operations::ExecuteQuery.new(query)
168
172
  end
169
173
 
170
- private
171
-
174
+ private
172
175
  def run(operation)
173
176
  Runner.new(Array(operation), connection).run!
174
177
  end
@@ -1,5 +1,16 @@
1
1
  module DbSchema
2
2
  module Operations
3
+ # Abstract base class for rename operations.
4
+ class RenameOperation
5
+ include Dry::Equalizer(:old_name, :new_name)
6
+ attr_reader :old_name, :new_name
7
+
8
+ def initialize(old_name:, new_name:)
9
+ @old_name = old_name
10
+ @new_name = new_name
11
+ end
12
+ end
13
+
3
14
  class CreateTable
4
15
  include Dry::Equalizer(:table)
5
16
  attr_reader :table
@@ -18,14 +29,7 @@ module DbSchema
18
29
  end
19
30
  end
20
31
 
21
- class RenameTable
22
- include Dry::Equalizer(:old_name, :new_name)
23
- attr_reader :old_name, :new_name
24
-
25
- def initialize(old_name:, new_name:)
26
- @old_name = old_name
27
- @new_name = new_name
28
- end
32
+ class RenameTable < RenameOperation
29
33
  end
30
34
 
31
35
  class AlterTable
@@ -76,14 +80,7 @@ module DbSchema
76
80
  class DropColumn < ColumnOperation
77
81
  end
78
82
 
79
- class RenameColumn
80
- include Dry::Equalizer(:old_name, :new_name)
81
- attr_reader :old_name, :new_name
82
-
83
- def initialize(old_name:, new_name:)
84
- @old_name = old_name
85
- @new_name = new_name
86
- end
83
+ class RenameColumn < RenameOperation
87
84
  end
88
85
 
89
86
  class AlterColumnType
@@ -176,6 +173,9 @@ module DbSchema
176
173
  class DropEnum < ColumnOperation
177
174
  end
178
175
 
176
+ class RenameEnum < RenameOperation
177
+ end
178
+
179
179
  class AlterEnumValues
180
180
  include Dry::Equalizer(:enum_name, :new_values, :enum_fields)
181
181
  attr_reader :enum_name, :new_values, :enum_fields
@@ -26,6 +26,8 @@ module DbSchema
26
26
  create_enum(change)
27
27
  when Operations::DropEnum
28
28
  drop_enum(change)
29
+ when Operations::RenameEnum
30
+ rename_enum(change)
29
31
  when Operations::AlterEnumValues
30
32
  alter_enum_values(change)
31
33
  when Operations::CreateExtension
@@ -141,6 +143,13 @@ module DbSchema
141
143
  connection.drop_enum(change.name)
142
144
  end
143
145
 
146
+ def rename_enum(change)
147
+ old_name = connection.quote_identifier(change.old_name)
148
+ new_name = connection.quote_identifier(change.new_name)
149
+
150
+ connection.run(%Q(ALTER TYPE #{old_name} RENAME TO #{new_name}))
151
+ end
152
+
144
153
  def alter_enum_values(change)
145
154
  change.enum_fields.each do |field_data|
146
155
  connection.alter_table(field_data[:table_name]) do
@@ -172,11 +181,11 @@ module DbSchema
172
181
  end
173
182
 
174
183
  def create_extension(change)
175
- connection.run(%Q(CREATE EXTENSION "#{change.extension.name}"))
184
+ connection.run(%Q(CREATE EXTENSION #{connection.quote_identifier(change.extension.name)}))
176
185
  end
177
186
 
178
187
  def drop_extension(change)
179
- connection.run(%Q(DROP EXTENSION "#{change.name}"))
188
+ connection.run(%Q(DROP EXTENSION #{connection.quote_identifier(change.name)}))
180
189
  end
181
190
 
182
191
  def execute_query(change)
@@ -34,6 +34,16 @@ module DbSchema
34
34
  end
35
35
  end
36
36
 
37
+ def remove_nil_values(hash)
38
+ hash.reduce({}) do |new_hash, (key, value)|
39
+ if value.nil?
40
+ new_hash
41
+ else
42
+ new_hash.merge(key => value)
43
+ end
44
+ end
45
+ end
46
+
37
47
  def sort_by_class(array, sorted_classes)
38
48
  sorted_classes.flat_map do |klass|
39
49
  array.select { |object| object.is_a?(klass) }
@@ -1,3 +1,3 @@
1
1
  module DbSchema
2
- VERSION = '0.3.rc1'
2
+ VERSION = '0.3'
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.3.rc1
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vsevolod Romashov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-08-17 00:00:00.000000000 Z
11
+ date: 2017-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -249,12 +249,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
249
249
  version: '0'
250
250
  required_rubygems_version: !ruby/object:Gem::Requirement
251
251
  requirements:
252
- - - ">"
252
+ - - ">="
253
253
  - !ruby/object:Gem::Version
254
- version: 1.3.1
254
+ version: '0'
255
255
  requirements: []
256
256
  rubyforge_project:
257
- rubygems_version: 2.5.2
257
+ rubygems_version: 2.6.13
258
258
  signing_key:
259
259
  specification_version: 4
260
260
  summary: Declarative database schema definition.