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 +4 -4
- data/.travis.yml +3 -1
- data/Guardfile +1 -0
- data/README.md +120 -478
- data/lib/db_schema/awesome_print.rb +11 -20
- data/lib/db_schema/configuration.rb +26 -4
- data/lib/db_schema/definitions/enum.rb +3 -1
- data/lib/db_schema/definitions/foreign_key.rb +4 -1
- data/lib/db_schema/definitions/table.rb +6 -1
- data/lib/db_schema/migrator.rb +6 -3
- data/lib/db_schema/operations.rb +16 -16
- data/lib/db_schema/runner.rb +11 -2
- data/lib/db_schema/utils.rb +10 -0
- data/lib/db_schema/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 216fab4c9e20545d7653f46cdb5fe9ce5528a68a
|
4
|
+
data.tar.gz: a3115a63fd0a6da8289d1b7e6848e57620785563
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ef95f7dcf532b83a288423f0626943479a4353797e281e0df5aa0788508811b19de3382576a7ed78f40c0b664bcd7f455ce12c335cf8c7a59cbdf33eccf093c
|
7
|
+
data.tar.gz: 660d11cc3e03fb68b2227ebe4f6febb544d17454d842daa6ad6d8d5cc8ec8af550f3e6ef936c8a7cf18f177ec1edfc56c792e4c0f51407b509868acd39b16b0d
|
data/.travis.yml
CHANGED
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.
|
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
|
68
|
+
$ gem install db_schema
|
69
69
|
```
|
70
70
|
|
71
71
|
## Usage
|
72
72
|
|
73
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
126
|
-
|
127
|
-
end
|
93
|
+
# ...
|
94
|
+
load application_root.join('db/schema.rb')
|
128
95
|
```
|
129
96
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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,
|
162
|
-
t.varchar :
|
163
|
-
t.
|
164
|
-
t.
|
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
|
-
|
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
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
126
|
+
### Production setup
|
397
127
|
|
398
|
-
|
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
|
-
|
402
|
-
|
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
|
-
|
498
|
-
db.
|
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
|
-
|
509
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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::
|
51
|
+
when ::DbSchema::Operations::ColumnOperation
|
47
52
|
:dbschema_column_operation
|
48
|
-
when ::DbSchema::Operations::
|
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
|
108
|
-
|
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
|
-
|
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 =
|
19
|
+
def initialize(params = DEFAULT_PARAMS)
|
20
|
+
@params = params
|
21
21
|
end
|
22
22
|
|
23
23
|
def merge(new_params)
|
24
|
-
|
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
|
data/lib/db_schema/migrator.rb
CHANGED
@@ -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
|
-
|
171
|
-
|
174
|
+
private
|
172
175
|
def run(operation)
|
173
176
|
Runner.new(Array(operation), connection).run!
|
174
177
|
end
|
data/lib/db_schema/operations.rb
CHANGED
@@ -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
|
data/lib/db_schema/runner.rb
CHANGED
@@ -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
|
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
|
188
|
+
connection.run(%Q(DROP EXTENSION #{connection.quote_identifier(change.name)}))
|
180
189
|
end
|
181
190
|
|
182
191
|
def execute_query(change)
|
data/lib/db_schema/utils.rb
CHANGED
@@ -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) }
|
data/lib/db_schema/version.rb
CHANGED
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
|
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-
|
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:
|
254
|
+
version: '0'
|
255
255
|
requirements: []
|
256
256
|
rubyforge_project:
|
257
|
-
rubygems_version: 2.
|
257
|
+
rubygems_version: 2.6.13
|
258
258
|
signing_key:
|
259
259
|
specification_version: 4
|
260
260
|
summary: Declarative database schema definition.
|