db_schema 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +16 -3
- data/lib/db_schema/awesome_print.rb +5 -5
- data/lib/db_schema/changes.rb +2 -2
- data/lib/db_schema/definitions.rb +44 -8
- data/lib/db_schema/dsl.rb +21 -8
- data/lib/db_schema/reader.rb +48 -8
- data/lib/db_schema/runner.rb +2 -14
- data/lib/db_schema/validator.rb +1 -1
- data/lib/db_schema/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a6dfe3738279a4199344a79ce5bfbbb7e504d50
|
4
|
+
data.tar.gz: 035b90071d29f3447ac99d79cd324b403e4838d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a19fa80ca01c4a3fd95983a73e58cc09628a5f453ca49fc290cfa7238e9eb136bf8de25e8ae7f8f179d9c65ff24a3e18418c6d380e9db7f351f8903cfe8d839f
|
7
|
+
data.tar.gz: 1fe00c3cada2070f96d397920aa785b4ef70cc77856e0ac6818eae46f92281cfdf92d9153c3a7db978a2ad1b13919edf1f2b734331501379c133812437ba8242
|
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)
|
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)
|
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.1.
|
56
|
+
gem 'db_schema', '~> 0.1.2'
|
57
57
|
```
|
58
58
|
|
59
59
|
And then execute:
|
@@ -356,6 +356,19 @@ end
|
|
356
356
|
|
357
357
|
Be warned though that you have to specify the condition exactly as PostgreSQL outputs it in `psql` with `\d table_name` command; otherwise your index will be recreated on each DbSchema run. This will be fixed in a later DbSchema version.
|
358
358
|
|
359
|
+
If you need an index on expression you can use the same syntax replacing column names with SQL strings containing the expressions:
|
360
|
+
|
361
|
+
``` ruby
|
362
|
+
db.table :users do |t|
|
363
|
+
t.timestamp :created_at
|
364
|
+
t.index 'date(created_at)'
|
365
|
+
end
|
366
|
+
```
|
367
|
+
|
368
|
+
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.
|
369
|
+
|
370
|
+
As with partial index condition (and all other SQL segments in `db_schema`), you must write the expression in a way `psql` outputs it, so instead of `lower(email)` you should use `lower(email::text)` (assuming that `email` is a varchar field).
|
371
|
+
|
359
372
|
#### Foreign keys
|
360
373
|
|
361
374
|
The `#foreign_key` method defines a foreign key. In it's minimal form it takes a referencing field name and referenced table name:
|
@@ -519,7 +532,7 @@ All configuration options are described in the following table:
|
|
519
532
|
|
520
533
|
By default DbSchema logs the changes it applies to your database; you can disable that by setting `log_changes` to false.
|
521
534
|
|
522
|
-
DbSchema provides an opt-out post-run schema check; it ensures that there are no remaining differences between your `schema.rb` and the actual database schema. If DbSchema still sees any differences it will keep applying them on each run - usually this is harmless (because it does not really change your schema) but in the case of a partial index with a complex condition it may rebuild the index which is an expensive operation on a large table. You can set `post_check` to false if you are 100% sure that your persistent changes are not a problem for you but I strongly recommend that you turn it on from time to time just to make sure nothing dangerous appears in these persistent changes.
|
535
|
+
DbSchema provides an opt-out post-run schema check; it ensures that there are no remaining differences between your `schema.rb` and the actual database schema. If DbSchema still sees any differences it will keep applying them on each run - usually this is harmless (because it does not really change your schema) but in the case of a partial index with a complex condition or an index on some expression it may rebuild the index which is an expensive operation on a large table. You can set `post_check` to false if you are 100% sure that your persistent changes are not a problem for you but I strongly recommend that you turn it on from time to time just to make sure nothing dangerous appears in these persistent changes.
|
523
536
|
|
524
537
|
The `post_check` option is likely to become off by default when DbSchema becomes more stable and battle-tested, and when the partial index problem will be solved.
|
525
538
|
|
@@ -21,8 +21,8 @@ if defined?(AwesomePrint)
|
|
21
21
|
:dbschema_field
|
22
22
|
when ::DbSchema::Definitions::Index
|
23
23
|
:dbschema_index
|
24
|
-
when ::DbSchema::Definitions::Index::
|
25
|
-
:
|
24
|
+
when ::DbSchema::Definitions::Index::Column
|
25
|
+
:dbschema_index_column
|
26
26
|
when ::DbSchema::Definitions::CheckConstraint
|
27
27
|
:dbschema_check_constraint
|
28
28
|
when ::DbSchema::Definitions::ForeignKey
|
@@ -120,17 +120,17 @@ if defined?(AwesomePrint)
|
|
120
120
|
end
|
121
121
|
|
122
122
|
def awesome_dbschema_index(object)
|
123
|
-
|
123
|
+
columns = format_dbschema_fields(object.columns)
|
124
124
|
using = ' using ' + colorize(object.type.to_s, :symbol) unless object.btree?
|
125
125
|
|
126
126
|
data = [nil]
|
127
127
|
data << colorize('unique', :nilclass) if object.unique?
|
128
128
|
data << colorize('condition: ', :symbol) + object.condition.ai unless object.condition.nil?
|
129
129
|
|
130
|
-
"#<#{object.class} #{object.name.ai} on #{
|
130
|
+
"#<#{object.class} #{object.name.ai} on #{columns}#{using}#{data.join(', ')}>"
|
131
131
|
end
|
132
132
|
|
133
|
-
def
|
133
|
+
def awesome_dbschema_index_column(object)
|
134
134
|
data = [object.name.ai]
|
135
135
|
|
136
136
|
if object.desc?
|
data/lib/db_schema/changes.rb
CHANGED
@@ -155,7 +155,7 @@ module DbSchema
|
|
155
155
|
if desired && !actual
|
156
156
|
table_changes << CreateIndex.new(
|
157
157
|
name: index_name,
|
158
|
-
|
158
|
+
columns: desired.columns,
|
159
159
|
unique: desired.unique?,
|
160
160
|
type: desired.type,
|
161
161
|
condition: desired.condition
|
@@ -166,7 +166,7 @@ module DbSchema
|
|
166
166
|
table_changes << DropIndex.new(index_name)
|
167
167
|
table_changes << CreateIndex.new(
|
168
168
|
name: index_name,
|
169
|
-
|
169
|
+
columns: desired.columns,
|
170
170
|
unique: desired.unique?,
|
171
171
|
type: desired.type,
|
172
172
|
condition: desired.condition
|
@@ -3,12 +3,12 @@ require 'dry/equalizer'
|
|
3
3
|
module DbSchema
|
4
4
|
module Definitions
|
5
5
|
class Index
|
6
|
-
include Dry::Equalizer(:name, :
|
7
|
-
attr_reader :name, :
|
6
|
+
include Dry::Equalizer(:name, :columns, :unique?, :type, :condition)
|
7
|
+
attr_reader :name, :columns, :type, :condition
|
8
8
|
|
9
|
-
def initialize(name:,
|
9
|
+
def initialize(name:, columns:, unique: false, type: :btree, condition: nil)
|
10
10
|
@name = name.to_sym
|
11
|
-
@
|
11
|
+
@columns = columns
|
12
12
|
@unique = unique
|
13
13
|
@type = type
|
14
14
|
@condition = condition
|
@@ -22,7 +22,15 @@ module DbSchema
|
|
22
22
|
type == :btree
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
def columns_to_sequel
|
26
|
+
if btree?
|
27
|
+
columns.map(&:ordered_expression)
|
28
|
+
else
|
29
|
+
columns.map(&:to_sequel)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Column
|
26
34
|
include Dry::Equalizer(:name, :order, :nulls)
|
27
35
|
attr_reader :name, :order, :nulls
|
28
36
|
|
@@ -40,14 +48,42 @@ module DbSchema
|
|
40
48
|
@order == :desc
|
41
49
|
end
|
42
50
|
|
43
|
-
def
|
51
|
+
def ordered_expression
|
44
52
|
if asc?
|
45
|
-
Sequel.asc(
|
53
|
+
Sequel.asc(to_sequel, nulls: nulls)
|
46
54
|
else
|
47
|
-
Sequel.desc(
|
55
|
+
Sequel.desc(to_sequel, nulls: nulls)
|
48
56
|
end
|
49
57
|
end
|
50
58
|
end
|
59
|
+
|
60
|
+
class TableField < Column
|
61
|
+
def expression?
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
def index_name_segment
|
66
|
+
name
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_sequel
|
70
|
+
name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Expression < Column
|
75
|
+
def expression?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def index_name_segment
|
80
|
+
name.scan(/\b[A-Za-z0-9_]+\b/).join('_')
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_sequel
|
84
|
+
Sequel.lit("(#{name})")
|
85
|
+
end
|
86
|
+
end
|
51
87
|
end
|
52
88
|
|
53
89
|
class ForeignKey
|
data/lib/db_schema/dsl.rb
CHANGED
@@ -55,11 +55,20 @@ module DbSchema
|
|
55
55
|
fields << Definitions::Field.build(name, type, options)
|
56
56
|
end
|
57
57
|
|
58
|
-
def index(*
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
58
|
+
def index(*columns, name: nil, unique: false, using: :btree, where: nil, **ordered_fields)
|
59
|
+
if columns.last.is_a?(Hash)
|
60
|
+
*ascending_columns, ordered_expressions = columns
|
61
|
+
else
|
62
|
+
ascending_columns = columns
|
63
|
+
ordered_expressions = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
columns_data = ascending_columns.each_with_object({}) do |column_name, columns|
|
67
|
+
columns[column_name] = :asc
|
68
|
+
end.merge(ordered_fields).merge(ordered_expressions)
|
69
|
+
|
70
|
+
index_columns = columns_data.map do |column_name, column_order_options|
|
71
|
+
options = case column_order_options
|
63
72
|
when :asc
|
64
73
|
{}
|
65
74
|
when :desc
|
@@ -72,14 +81,18 @@ module DbSchema
|
|
72
81
|
raise ArgumentError, 'Only :asc, :desc, :asc_nulls_first and :desc_nulls_last options are supported.'
|
73
82
|
end
|
74
83
|
|
75
|
-
|
84
|
+
if column_name.is_a?(String)
|
85
|
+
Definitions::Index::Expression.new(column_name, **options)
|
86
|
+
else
|
87
|
+
Definitions::Index::TableField.new(column_name, **options)
|
88
|
+
end
|
76
89
|
end
|
77
90
|
|
78
|
-
index_name = name || "#{table_name}_#{
|
91
|
+
index_name = name || "#{table_name}_#{index_columns.map(&:index_name_segment).join('_')}_index"
|
79
92
|
|
80
93
|
indices << Definitions::Index.new(
|
81
94
|
name: index_name,
|
82
|
-
|
95
|
+
columns: index_columns,
|
83
96
|
unique: unique,
|
84
97
|
type: using,
|
85
98
|
condition: where
|
data/lib/db_schema/reader.rb
CHANGED
@@ -60,8 +60,8 @@ SELECT conname AS name,
|
|
60
60
|
indisunique AS unique,
|
61
61
|
indoption AS index_options,
|
62
62
|
pg_get_expr(indpred, indrelid, true) AS condition,
|
63
|
-
|
64
|
-
|
63
|
+
amname AS index_type,
|
64
|
+
indexrelid AS index_oid
|
65
65
|
FROM pg_class, pg_index
|
66
66
|
LEFT JOIN pg_opclass
|
67
67
|
ON pg_opclass.oid = ANY(pg_index.indclass::int[])
|
@@ -75,7 +75,16 @@ LEFT JOIN pg_am
|
|
75
75
|
AND pg_class.oid = pg_index.indrelid
|
76
76
|
AND indisprimary != 't'
|
77
77
|
)
|
78
|
-
GROUP BY name, column_positions, indisunique, index_options, condition,
|
78
|
+
GROUP BY name, column_positions, indisunique, index_options, condition, index_type, index_oid
|
79
|
+
SQL
|
80
|
+
|
81
|
+
EXPRESSION_INDICES_QUERY = <<-SQL.freeze
|
82
|
+
WITH index_ids AS (SELECT unnest(?) AS index_id),
|
83
|
+
elements AS (SELECT unnest(?) AS element)
|
84
|
+
SELECT index_id,
|
85
|
+
array_agg(pg_get_indexdef(index_id, element, 't')) AS definitions
|
86
|
+
FROM index_ids, elements
|
87
|
+
GROUP BY index_id;
|
79
88
|
SQL
|
80
89
|
|
81
90
|
ENUMS_QUERY = <<-SQL.freeze
|
@@ -139,14 +148,17 @@ SELECT extname
|
|
139
148
|
|
140
149
|
def indices_data_for(table_name)
|
141
150
|
column_names = DbSchema.connection[COLUMN_NAMES_QUERY, table_name.to_s].reduce({}) do |names, column|
|
142
|
-
names.merge(column[:pos] => column[:name])
|
151
|
+
names.merge(column[:pos] => column[:name].to_sym)
|
143
152
|
end
|
144
153
|
|
145
|
-
DbSchema.connection[INDICES_QUERY, table_name.to_s].
|
154
|
+
indices_data = DbSchema.connection[INDICES_QUERY, table_name.to_s].to_a
|
155
|
+
expressions_data = index_expressions_data(indices_data)
|
156
|
+
|
157
|
+
indices_data.map do |index|
|
146
158
|
positions = index[:column_positions].split(' ').map(&:to_i)
|
147
159
|
options = index[:index_options].split(' ').map(&:to_i)
|
148
160
|
|
149
|
-
|
161
|
+
columns = positions.zip(options).map do |column_position, column_order_options|
|
150
162
|
options = case column_order_options
|
151
163
|
when 0
|
152
164
|
{}
|
@@ -158,12 +170,17 @@ SELECT extname
|
|
158
170
|
{ order: :desc, nulls: :last }
|
159
171
|
end
|
160
172
|
|
161
|
-
|
173
|
+
if column_position.zero?
|
174
|
+
expression = expressions_data.fetch(index[:index_oid]).shift
|
175
|
+
DbSchema::Definitions::Index::Expression.new(expression, **options)
|
176
|
+
else
|
177
|
+
DbSchema::Definitions::Index::TableField.new(column_names.fetch(column_position), **options)
|
178
|
+
end
|
162
179
|
end
|
163
180
|
|
164
181
|
{
|
165
182
|
name: index[:name].to_sym,
|
166
|
-
|
183
|
+
columns: columns,
|
167
184
|
unique: index[:unique],
|
168
185
|
type: index[:index_type].to_sym,
|
169
186
|
condition: index[:condition]
|
@@ -172,6 +189,29 @@ SELECT extname
|
|
172
189
|
end
|
173
190
|
|
174
191
|
private
|
192
|
+
def index_expressions_data(indices_data)
|
193
|
+
expressions_stats = indices_data.each_with_object(ids: [], max: 0) do |index_data, stats|
|
194
|
+
expressions_count = index_data[:column_positions].split(' ').count('0')
|
195
|
+
|
196
|
+
if expressions_count > 0
|
197
|
+
stats[:ids] << index_data[:index_oid]
|
198
|
+
stats[:max] = [stats[:max], expressions_count].max
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
if expressions_stats[:max] > 0
|
203
|
+
DbSchema.connection[
|
204
|
+
EXPRESSION_INDICES_QUERY,
|
205
|
+
Sequel.pg_array(expressions_stats[:ids]),
|
206
|
+
Sequel.pg_array((1..expressions_stats[:max]).to_a)
|
207
|
+
].each_with_object({}) do |index_data, indices_data|
|
208
|
+
indices_data[index_data[:index_id]] = index_data[:definitions]
|
209
|
+
end
|
210
|
+
else
|
211
|
+
{}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
175
215
|
def build_field(data, primary_key: false)
|
176
216
|
type = data[:type].to_sym.downcase
|
177
217
|
if type == :'user-defined'
|
data/lib/db_schema/runner.rb
CHANGED
@@ -70,14 +70,8 @@ module DbSchema
|
|
70
70
|
end
|
71
71
|
|
72
72
|
change.indices.each do |index|
|
73
|
-
fields = if index.btree?
|
74
|
-
index.fields.map(&:to_sequel)
|
75
|
-
else
|
76
|
-
index.fields.map(&:name)
|
77
|
-
end
|
78
|
-
|
79
73
|
index(
|
80
|
-
|
74
|
+
index.columns_to_sequel,
|
81
75
|
name: index.name,
|
82
76
|
unique: index.unique?,
|
83
77
|
type: index.type,
|
@@ -141,14 +135,8 @@ module DbSchema
|
|
141
135
|
when Changes::AlterColumnDefault
|
142
136
|
set_column_default(element.name, element.new_default)
|
143
137
|
when Changes::CreateIndex
|
144
|
-
fields = if element.btree?
|
145
|
-
element.fields.map(&:to_sequel)
|
146
|
-
else
|
147
|
-
element.fields.map(&:name)
|
148
|
-
end
|
149
|
-
|
150
138
|
add_index(
|
151
|
-
|
139
|
+
element.columns_to_sequel,
|
152
140
|
name: element.name,
|
153
141
|
unique: element.unique?,
|
154
142
|
type: element.type,
|
data/lib/db_schema/validator.rb
CHANGED
@@ -24,7 +24,7 @@ module DbSchema
|
|
24
24
|
field_names = table.fields.map(&:name)
|
25
25
|
|
26
26
|
table.indices.each do |index|
|
27
|
-
index.
|
27
|
+
index.columns.reject(&:expression?).map(&:name).each do |field_name|
|
28
28
|
unless field_names.include?(field_name)
|
29
29
|
error_message = %(Index "#{index.name}" refers to a missing field "#{table.name}.#{field_name}")
|
30
30
|
errors << error_message
|
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.1.
|
4
|
+
version: 0.1.2
|
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-
|
11
|
+
date: 2016-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|