db_schema 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +522 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/db_schema.gemspec +35 -0
- data/lib/db_schema.rb +125 -0
- data/lib/db_schema/awesome_print.rb +246 -0
- data/lib/db_schema/changes.rb +396 -0
- data/lib/db_schema/configuration.rb +29 -0
- data/lib/db_schema/definitions.rb +122 -0
- data/lib/db_schema/definitions/field.rb +38 -0
- data/lib/db_schema/definitions/field/array.rb +19 -0
- data/lib/db_schema/definitions/field/base.rb +90 -0
- data/lib/db_schema/definitions/field/binary.rb +9 -0
- data/lib/db_schema/definitions/field/bit_string.rb +15 -0
- data/lib/db_schema/definitions/field/boolean.rb +9 -0
- data/lib/db_schema/definitions/field/character.rb +19 -0
- data/lib/db_schema/definitions/field/custom.rb +22 -0
- data/lib/db_schema/definitions/field/datetime.rb +30 -0
- data/lib/db_schema/definitions/field/geometric.rb +33 -0
- data/lib/db_schema/definitions/field/json.rb +13 -0
- data/lib/db_schema/definitions/field/monetary.rb +9 -0
- data/lib/db_schema/definitions/field/network.rb +17 -0
- data/lib/db_schema/definitions/field/numeric.rb +30 -0
- data/lib/db_schema/definitions/field/range.rb +29 -0
- data/lib/db_schema/definitions/field/text_search.rb +13 -0
- data/lib/db_schema/definitions/field/uuid.rb +9 -0
- data/lib/db_schema/dsl.rb +145 -0
- data/lib/db_schema/reader.rb +270 -0
- data/lib/db_schema/runner.rb +220 -0
- data/lib/db_schema/utils.rb +50 -0
- data/lib/db_schema/validator.rb +89 -0
- data/lib/db_schema/version.rb +3 -0
- metadata +239 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module DbSchema
|
2
|
+
module Definitions
|
3
|
+
module Field
|
4
|
+
class SmallInt < Base
|
5
|
+
register :smallint
|
6
|
+
end
|
7
|
+
|
8
|
+
class Integer < Base
|
9
|
+
register :integer
|
10
|
+
end
|
11
|
+
|
12
|
+
class BigInt < Base
|
13
|
+
register :bigint
|
14
|
+
end
|
15
|
+
|
16
|
+
class Numeric < Base
|
17
|
+
register :numeric, :decimal
|
18
|
+
attributes :precision, :scale
|
19
|
+
end
|
20
|
+
|
21
|
+
class Real < Base
|
22
|
+
register :real
|
23
|
+
end
|
24
|
+
|
25
|
+
class DoublePrecision < Base
|
26
|
+
register :'double precision', :float
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module DbSchema
|
2
|
+
module Definitions
|
3
|
+
module Field
|
4
|
+
class Int4Range < Base
|
5
|
+
register :int4range
|
6
|
+
end
|
7
|
+
|
8
|
+
class Int8Range < Base
|
9
|
+
register :int8range
|
10
|
+
end
|
11
|
+
|
12
|
+
class NumRange < Base
|
13
|
+
register :numrange
|
14
|
+
end
|
15
|
+
|
16
|
+
class TsRange < Base
|
17
|
+
register :tsrange
|
18
|
+
end
|
19
|
+
|
20
|
+
class TsTzRange < Base
|
21
|
+
register :tstzrange
|
22
|
+
end
|
23
|
+
|
24
|
+
class DateRange < Base
|
25
|
+
register :daterange
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module DbSchema
|
2
|
+
class DSL
|
3
|
+
attr_reader :block
|
4
|
+
|
5
|
+
def initialize(block)
|
6
|
+
@block = block
|
7
|
+
@schema = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def schema
|
11
|
+
block.call(self)
|
12
|
+
|
13
|
+
@schema
|
14
|
+
end
|
15
|
+
|
16
|
+
def table(name, &block)
|
17
|
+
table_yielder = TableYielder.new(name, block)
|
18
|
+
|
19
|
+
@schema << Definitions::Table.new(
|
20
|
+
name,
|
21
|
+
fields: table_yielder.fields,
|
22
|
+
indices: table_yielder.indices,
|
23
|
+
checks: table_yielder.checks,
|
24
|
+
foreign_keys: table_yielder.foreign_keys
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def enum(name, values)
|
29
|
+
@schema << Definitions::Enum.new(name.to_sym, values.map(&:to_sym))
|
30
|
+
end
|
31
|
+
|
32
|
+
class TableYielder
|
33
|
+
attr_reader :table_name
|
34
|
+
|
35
|
+
def initialize(table_name, block)
|
36
|
+
@table_name = table_name
|
37
|
+
block.call(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
DbSchema::Definitions::Field.registry.keys.each do |type|
|
41
|
+
define_method(type) do |name, **options|
|
42
|
+
field(name, type, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def primary_key(name)
|
47
|
+
fields << Definitions::Field::Integer.new(name, primary_key: true)
|
48
|
+
end
|
49
|
+
|
50
|
+
def field(name, type, **options)
|
51
|
+
fields << Definitions::Field.build(name, type, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def index(*fields, name: nil, unique: false, using: :btree, where: nil, **ordered_fields)
|
55
|
+
index_fields = fields.map do |field_name|
|
56
|
+
Definitions::Index::Field.new(field_name.to_sym)
|
57
|
+
end + ordered_fields.map do |field_name, field_order_options|
|
58
|
+
options = case field_order_options
|
59
|
+
when :asc
|
60
|
+
{}
|
61
|
+
when :desc
|
62
|
+
{ order: :desc }
|
63
|
+
when :asc_nulls_first
|
64
|
+
{ nulls: :first }
|
65
|
+
when :desc_nulls_last
|
66
|
+
{ order: :desc, nulls: :last }
|
67
|
+
else
|
68
|
+
raise ArgumentError, 'Only :asc, :desc, :asc_nulls_first and :desc_nulls_last options are supported.'
|
69
|
+
end
|
70
|
+
|
71
|
+
Definitions::Index::Field.new(field_name.to_sym, **options)
|
72
|
+
end
|
73
|
+
|
74
|
+
index_name = name || "#{table_name}_#{index_fields.map(&:name).join('_')}_index"
|
75
|
+
|
76
|
+
indices << Definitions::Index.new(
|
77
|
+
name: index_name,
|
78
|
+
fields: index_fields,
|
79
|
+
unique: unique,
|
80
|
+
type: using,
|
81
|
+
condition: where
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def check(name, condition)
|
86
|
+
checks << Definitions::CheckConstraint.new(name: name, condition: condition)
|
87
|
+
end
|
88
|
+
|
89
|
+
def foreign_key(*fkey_fields, references:, name: nil, on_update: :no_action, on_delete: :no_action, deferrable: false)
|
90
|
+
fkey_name = name || :"#{table_name}_#{fkey_fields.first}_fkey"
|
91
|
+
|
92
|
+
if references.is_a?(Array)
|
93
|
+
# [:table, :field]
|
94
|
+
referenced_table, *referenced_keys = references
|
95
|
+
|
96
|
+
foreign_keys << Definitions::ForeignKey.new(
|
97
|
+
name: fkey_name,
|
98
|
+
fields: fkey_fields,
|
99
|
+
table: referenced_table,
|
100
|
+
keys: referenced_keys,
|
101
|
+
on_delete: on_delete,
|
102
|
+
on_update: on_update,
|
103
|
+
deferrable: deferrable
|
104
|
+
)
|
105
|
+
else
|
106
|
+
# :table
|
107
|
+
foreign_keys << Definitions::ForeignKey.new(
|
108
|
+
name: fkey_name,
|
109
|
+
fields: fkey_fields,
|
110
|
+
table: references,
|
111
|
+
on_delete: on_delete,
|
112
|
+
on_update: on_update,
|
113
|
+
deferrable: deferrable
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def method_missing(method_name, name, *args, &block)
|
119
|
+
options = args.first || {}
|
120
|
+
|
121
|
+
fields << Definitions::Field::Custom.new(
|
122
|
+
name,
|
123
|
+
type_name: method_name,
|
124
|
+
**options
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def fields
|
129
|
+
@fields ||= []
|
130
|
+
end
|
131
|
+
|
132
|
+
def indices
|
133
|
+
@indices ||= []
|
134
|
+
end
|
135
|
+
|
136
|
+
def checks
|
137
|
+
@checks ||= []
|
138
|
+
end
|
139
|
+
|
140
|
+
def foreign_keys
|
141
|
+
@foreign_keys ||= []
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
module DbSchema
|
2
|
+
module Reader
|
3
|
+
class << self
|
4
|
+
def read_schema
|
5
|
+
adapter.read_schema
|
6
|
+
end
|
7
|
+
|
8
|
+
def adapter
|
9
|
+
adapter_name = DbSchema.configuration.adapter
|
10
|
+
registry.fetch(adapter_name) do |adapter_name|
|
11
|
+
raise NotImplementedError, "DbSchema::Reader does not support #{adapter_name}."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def registry
|
17
|
+
@registry ||= {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Postgres
|
22
|
+
DEFAULT_VALUE = /\A(('(?<string>.*)')|(?<float>\d+\.\d+)|(?<integer>\d+)|(?<boolean>true|false)|((?<function>[A-Za-z_]+)\(\)))/
|
23
|
+
|
24
|
+
COLUMN_NAMES_QUERY = <<-SQL.freeze
|
25
|
+
SELECT c.column_name AS name,
|
26
|
+
c.ordinal_position AS pos,
|
27
|
+
c.column_default AS default,
|
28
|
+
c.is_nullable AS null,
|
29
|
+
c.data_type AS type,
|
30
|
+
c.udt_name AS custom_type_name,
|
31
|
+
c.character_maximum_length AS char_length,
|
32
|
+
c.numeric_precision AS num_precision,
|
33
|
+
c.numeric_scale AS num_scale,
|
34
|
+
c.datetime_precision AS dt_precision,
|
35
|
+
c.interval_type,
|
36
|
+
e.data_type AS element_type
|
37
|
+
FROM information_schema.columns AS c
|
38
|
+
LEFT JOIN information_schema.element_types AS e
|
39
|
+
ON e.object_catalog = c.table_catalog
|
40
|
+
AND e.object_schema = c.table_schema
|
41
|
+
AND e.object_name = c.table_name
|
42
|
+
AND e.object_type = 'TABLE'
|
43
|
+
AND e.collection_type_identifier = c.dtd_identifier
|
44
|
+
WHERE c.table_schema = 'public'
|
45
|
+
AND c.table_name = ?
|
46
|
+
SQL
|
47
|
+
|
48
|
+
CONSTRAINTS_QUERY = <<-SQL.freeze
|
49
|
+
SELECT conname AS name,
|
50
|
+
pg_get_expr(conbin, conrelid, true) AS condition
|
51
|
+
FROM pg_constraint, pg_class
|
52
|
+
WHERE conrelid = pg_class.oid
|
53
|
+
AND relname = ?
|
54
|
+
AND contype = 'c'
|
55
|
+
SQL
|
56
|
+
|
57
|
+
INDICES_QUERY = <<-SQL.freeze
|
58
|
+
SELECT relname AS name,
|
59
|
+
indkey AS column_positions,
|
60
|
+
indisunique AS unique,
|
61
|
+
indoption AS index_options,
|
62
|
+
pg_get_expr(indpred, indrelid, true) AS condition,
|
63
|
+
pg_get_expr(indexprs, indrelid, true) AS expression,
|
64
|
+
amname AS index_type
|
65
|
+
FROM pg_class, pg_index
|
66
|
+
LEFT JOIN pg_opclass
|
67
|
+
ON pg_opclass.oid = ANY(pg_index.indclass::int[])
|
68
|
+
LEFT JOIN pg_am
|
69
|
+
ON pg_am.oid = pg_opclass.opcmethod
|
70
|
+
WHERE pg_class.oid = pg_index.indexrelid
|
71
|
+
AND pg_class.oid IN (
|
72
|
+
SELECT indexrelid
|
73
|
+
FROM pg_index, pg_class
|
74
|
+
WHERE pg_class.relname = ?
|
75
|
+
AND pg_class.oid = pg_index.indrelid
|
76
|
+
AND indisprimary != 't'
|
77
|
+
)
|
78
|
+
GROUP BY name, column_positions, indisunique, index_options, condition, expression, index_type
|
79
|
+
SQL
|
80
|
+
|
81
|
+
ENUMS_QUERY = <<-SQL.freeze
|
82
|
+
SELECT t.typname AS name,
|
83
|
+
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
|
84
|
+
FROM pg_enum AS e
|
85
|
+
JOIN pg_type AS t
|
86
|
+
ON t.oid = e.enumtypid
|
87
|
+
GROUP BY name
|
88
|
+
SQL
|
89
|
+
|
90
|
+
class << self
|
91
|
+
def read_schema
|
92
|
+
enums = DbSchema.connection[ENUMS_QUERY].map do |enum_data|
|
93
|
+
Definitions::Enum.new(enum_data[:name].to_sym, enum_data[:values].map(&:to_sym))
|
94
|
+
end
|
95
|
+
|
96
|
+
tables = DbSchema.connection.tables.map do |table_name|
|
97
|
+
primary_key_name = DbSchema.connection.primary_key(table_name)
|
98
|
+
|
99
|
+
fields = DbSchema.connection[COLUMN_NAMES_QUERY, table_name.to_s].map do |column_data|
|
100
|
+
build_field(column_data, primary_key: column_data[:name] == primary_key_name)
|
101
|
+
end
|
102
|
+
|
103
|
+
indices = indices_data_for(table_name).map do |index_data|
|
104
|
+
Definitions::Index.new(index_data)
|
105
|
+
end.sort_by(&:name)
|
106
|
+
|
107
|
+
foreign_keys = DbSchema.connection.foreign_key_list(table_name).map do |foreign_key_data|
|
108
|
+
build_foreign_key(foreign_key_data)
|
109
|
+
end
|
110
|
+
|
111
|
+
checks = DbSchema.connection[CONSTRAINTS_QUERY, table_name.to_s].map do |check_data|
|
112
|
+
Definitions::CheckConstraint.new(
|
113
|
+
name: check_data[:name].to_sym,
|
114
|
+
condition: check_data[:condition]
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
Definitions::Table.new(
|
119
|
+
table_name,
|
120
|
+
fields: fields,
|
121
|
+
indices: indices,
|
122
|
+
checks: checks,
|
123
|
+
foreign_keys: foreign_keys
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
enums + tables
|
128
|
+
end
|
129
|
+
|
130
|
+
def indices_data_for(table_name)
|
131
|
+
column_names = DbSchema.connection[COLUMN_NAMES_QUERY, table_name.to_s].reduce({}) do |names, column|
|
132
|
+
names.merge(column[:pos] => column[:name])
|
133
|
+
end
|
134
|
+
|
135
|
+
DbSchema.connection[INDICES_QUERY, table_name.to_s].map do |index|
|
136
|
+
positions = index[:column_positions].split(' ').map(&:to_i)
|
137
|
+
options = index[:index_options].split(' ').map(&:to_i)
|
138
|
+
|
139
|
+
fields = column_names.values_at(*positions).zip(options).map do |column_name, column_order_options|
|
140
|
+
options = case column_order_options
|
141
|
+
when 0
|
142
|
+
{}
|
143
|
+
when 3
|
144
|
+
{ order: :desc }
|
145
|
+
when 2
|
146
|
+
{ nulls: :first }
|
147
|
+
when 1
|
148
|
+
{ order: :desc, nulls: :last }
|
149
|
+
end
|
150
|
+
|
151
|
+
DbSchema::Definitions::Index::Field.new(column_name.to_sym, **options)
|
152
|
+
end
|
153
|
+
|
154
|
+
{
|
155
|
+
name: index[:name].to_sym,
|
156
|
+
fields: fields,
|
157
|
+
unique: index[:unique],
|
158
|
+
type: index[:index_type].to_sym,
|
159
|
+
condition: index[:condition]
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
def build_field(data, primary_key: false)
|
166
|
+
type = data[:type].to_sym.downcase
|
167
|
+
|
168
|
+
nullable = (data[:null] != 'NO')
|
169
|
+
|
170
|
+
unless primary_key || data[:default].nil?
|
171
|
+
if match = DEFAULT_VALUE.match(data[:default])
|
172
|
+
default = if match[:string]
|
173
|
+
match[:string]
|
174
|
+
elsif match[:integer]
|
175
|
+
match[:integer].to_i
|
176
|
+
elsif match[:float]
|
177
|
+
match[:float].to_f
|
178
|
+
elsif match[:boolean]
|
179
|
+
match[:boolean] == 'true'
|
180
|
+
elsif match[:function]
|
181
|
+
match[:function].to_sym
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
options = case type
|
187
|
+
when :character, :'character varying', :bit, :'bit varying'
|
188
|
+
Utils.rename_keys(
|
189
|
+
Utils.filter_by_keys(data, :char_length),
|
190
|
+
char_length: :length
|
191
|
+
)
|
192
|
+
when :numeric
|
193
|
+
Utils.rename_keys(
|
194
|
+
Utils.filter_by_keys(data, :num_precision, :num_scale),
|
195
|
+
num_precision: :precision,
|
196
|
+
num_scale: :scale
|
197
|
+
)
|
198
|
+
when :interval
|
199
|
+
Utils.rename_keys(
|
200
|
+
Utils.filter_by_keys(data, :dt_precision, :interval_type),
|
201
|
+
dt_precision: :precision
|
202
|
+
) do |attributes|
|
203
|
+
if interval_type = attributes.delete(:interval_type)
|
204
|
+
attributes[:fields] = interval_type.gsub(/\(\d\)/, '').downcase.to_sym
|
205
|
+
end
|
206
|
+
end
|
207
|
+
when :array
|
208
|
+
Utils.rename_keys(Utils.filter_by_keys(data, :element_type)) do |attributes|
|
209
|
+
attributes[:of] = attributes[:element_type].to_sym
|
210
|
+
end
|
211
|
+
else
|
212
|
+
{}
|
213
|
+
end
|
214
|
+
|
215
|
+
if data[:type] == 'USER-DEFINED'
|
216
|
+
Definitions::Field::Custom.new(
|
217
|
+
data[:name].to_sym,
|
218
|
+
type_name: data[:custom_type_name].to_sym,
|
219
|
+
primary_key: primary_key,
|
220
|
+
null: nullable,
|
221
|
+
default: default
|
222
|
+
)
|
223
|
+
else
|
224
|
+
Definitions::Field.build(
|
225
|
+
data[:name].to_sym,
|
226
|
+
type,
|
227
|
+
primary_key: primary_key,
|
228
|
+
null: nullable,
|
229
|
+
default: default,
|
230
|
+
**options
|
231
|
+
)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def build_foreign_key(data)
|
236
|
+
keys = if data[:key] == [primary_key_for(data[:table])]
|
237
|
+
[] # this foreign key references a primary key
|
238
|
+
else
|
239
|
+
data[:key]
|
240
|
+
end
|
241
|
+
|
242
|
+
Definitions::ForeignKey.new(
|
243
|
+
name: data[:name],
|
244
|
+
fields: data[:columns],
|
245
|
+
table: data[:table],
|
246
|
+
keys: keys,
|
247
|
+
on_delete: data[:on_delete],
|
248
|
+
on_update: data[:on_update],
|
249
|
+
deferrable: data[:deferrable]
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
def primary_key_for(table_name)
|
254
|
+
if pkey = primary_keys[table_name]
|
255
|
+
pkey.to_sym
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def primary_keys
|
260
|
+
@primary_keys ||= DbSchema.connection.tables.reduce({}) do |primary_keys, table_name|
|
261
|
+
primary_keys.merge(table_name => DbSchema.connection.primary_key(table_name))
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
registry['postgres'] = Postgres
|
268
|
+
registry['postgresql'] = Postgres
|
269
|
+
end
|
270
|
+
end
|