db_schema 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +17 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +522 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +10 -0
  11. data/bin/setup +8 -0
  12. data/db_schema.gemspec +35 -0
  13. data/lib/db_schema.rb +125 -0
  14. data/lib/db_schema/awesome_print.rb +246 -0
  15. data/lib/db_schema/changes.rb +396 -0
  16. data/lib/db_schema/configuration.rb +29 -0
  17. data/lib/db_schema/definitions.rb +122 -0
  18. data/lib/db_schema/definitions/field.rb +38 -0
  19. data/lib/db_schema/definitions/field/array.rb +19 -0
  20. data/lib/db_schema/definitions/field/base.rb +90 -0
  21. data/lib/db_schema/definitions/field/binary.rb +9 -0
  22. data/lib/db_schema/definitions/field/bit_string.rb +15 -0
  23. data/lib/db_schema/definitions/field/boolean.rb +9 -0
  24. data/lib/db_schema/definitions/field/character.rb +19 -0
  25. data/lib/db_schema/definitions/field/custom.rb +22 -0
  26. data/lib/db_schema/definitions/field/datetime.rb +30 -0
  27. data/lib/db_schema/definitions/field/geometric.rb +33 -0
  28. data/lib/db_schema/definitions/field/json.rb +13 -0
  29. data/lib/db_schema/definitions/field/monetary.rb +9 -0
  30. data/lib/db_schema/definitions/field/network.rb +17 -0
  31. data/lib/db_schema/definitions/field/numeric.rb +30 -0
  32. data/lib/db_schema/definitions/field/range.rb +29 -0
  33. data/lib/db_schema/definitions/field/text_search.rb +13 -0
  34. data/lib/db_schema/definitions/field/uuid.rb +9 -0
  35. data/lib/db_schema/dsl.rb +145 -0
  36. data/lib/db_schema/reader.rb +270 -0
  37. data/lib/db_schema/runner.rb +220 -0
  38. data/lib/db_schema/utils.rb +50 -0
  39. data/lib/db_schema/validator.rb +89 -0
  40. data/lib/db_schema/version.rb +3 -0
  41. metadata +239 -0
@@ -0,0 +1,13 @@
1
+ module DbSchema
2
+ module Definitions
3
+ module Field
4
+ class JSON < Base
5
+ register :json
6
+ end
7
+
8
+ class JSONB < Base
9
+ register :jsonb
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module DbSchema
2
+ module Definitions
3
+ module Field
4
+ class Money < Base
5
+ register :money
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module DbSchema
2
+ module Definitions
3
+ module Field
4
+ class Cidr < Base
5
+ register :cidr
6
+ end
7
+
8
+ class Inet < Base
9
+ register :inet
10
+ end
11
+
12
+ class MacAddr < Base
13
+ register :macaddr
14
+ end
15
+ end
16
+ end
17
+ end
@@ -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,13 @@
1
+ module DbSchema
2
+ module Definitions
3
+ module Field
4
+ class TsVector < Base
5
+ register :tsvector
6
+ end
7
+
8
+ class TsQuery < Base
9
+ register :tsquery
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module DbSchema
2
+ module Definitions
3
+ module Field
4
+ class UUID < Base
5
+ register :uuid
6
+ end
7
+ end
8
+ end
9
+ 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