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.
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