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,220 @@
1
+ module DbSchema
2
+ class Runner
3
+ attr_reader :changes
4
+
5
+ def initialize(changes)
6
+ @changes = preprocess_changes(changes)
7
+ end
8
+
9
+ def run!
10
+ DbSchema.connection.transaction do
11
+ changes.each do |change|
12
+ case change
13
+ when Changes::CreateTable
14
+ self.class.create_table(change)
15
+ when Changes::DropTable
16
+ self.class.drop_table(change)
17
+ when Changes::AlterTable
18
+ self.class.alter_table(change)
19
+ when Changes::CreateForeignKey
20
+ self.class.create_foreign_key(change)
21
+ when Changes::DropForeignKey
22
+ self.class.drop_foreign_key(change)
23
+ when Changes::CreateEnum
24
+ self.class.create_enum(change)
25
+ when Changes::DropEnum
26
+ self.class.drop_enum(change)
27
+ end
28
+ end
29
+ end
30
+
31
+ # Postgres doesn't allow modifying enums inside a transaction
32
+ Utils.filter_by_class(changes, Changes::AddValueToEnum).each do |change|
33
+ self.class.add_value_to_enum(change)
34
+ end
35
+ end
36
+
37
+ private
38
+ def preprocess_changes(changes)
39
+ Utils.sort_by_class(
40
+ changes,
41
+ [
42
+ Changes::AddValueToEnum,
43
+ Changes::DropForeignKey,
44
+ Changes::CreateEnum,
45
+ Changes::CreateTable,
46
+ Changes::AlterTable,
47
+ Changes::DropTable,
48
+ Changes::DropEnum,
49
+ Changes::CreateForeignKey
50
+ ]
51
+ )
52
+ end
53
+
54
+ class << self
55
+ def create_table(change)
56
+ DbSchema.connection.create_table(change.name) do
57
+ change.fields.each do |field|
58
+ if field.primary_key?
59
+ primary_key(field.name)
60
+ else
61
+ options = Runner.map_options(field.class.type, field.options)
62
+ column(field.name, field.type.capitalize, options)
63
+ end
64
+ end
65
+
66
+ change.indices.each do |index|
67
+ fields = if index.btree?
68
+ index.fields.map(&:to_sequel)
69
+ else
70
+ index.fields.map(&:name)
71
+ end
72
+
73
+ index(
74
+ fields,
75
+ name: index.name,
76
+ unique: index.unique?,
77
+ type: index.type,
78
+ where: index.condition
79
+ )
80
+ end
81
+
82
+ change.checks.each do |check|
83
+ constraint(check.name, check.condition)
84
+ end
85
+ end
86
+ end
87
+
88
+ def drop_table(change)
89
+ DbSchema.connection.drop_table(change.name)
90
+ end
91
+
92
+ def alter_table(change)
93
+ DbSchema.connection.alter_table(change.name) do
94
+ Utils.sort_by_class(
95
+ change.fields + change.indices + change.checks,
96
+ [
97
+ DbSchema::Changes::DropPrimaryKey,
98
+ DbSchema::Changes::DropCheckConstraint,
99
+ DbSchema::Changes::DropIndex,
100
+ DbSchema::Changes::DropColumn,
101
+ DbSchema::Changes::RenameColumn,
102
+ DbSchema::Changes::AlterColumnType,
103
+ DbSchema::Changes::AllowNull,
104
+ DbSchema::Changes::DisallowNull,
105
+ DbSchema::Changes::AlterColumnDefault,
106
+ DbSchema::Changes::CreateColumn,
107
+ DbSchema::Changes::CreateIndex,
108
+ DbSchema::Changes::CreateCheckConstraint,
109
+ DbSchema::Changes::CreatePrimaryKey
110
+ ]
111
+ ).each do |element|
112
+ case element
113
+ when Changes::CreateColumn
114
+ if element.primary_key?
115
+ add_primary_key(element.name)
116
+ else
117
+ options = Runner.map_options(element.type, element.options)
118
+ add_column(element.name, element.type.capitalize, options)
119
+ end
120
+ when Changes::DropColumn
121
+ drop_column(element.name)
122
+ when Changes::RenameColumn
123
+ rename_column(element.old_name, element.new_name)
124
+ when Changes::AlterColumnType
125
+ attributes = Runner.map_options(element.new_type, element.new_attributes)
126
+ set_column_type(element.name, element.new_type.capitalize, attributes)
127
+ when Changes::CreatePrimaryKey
128
+ raise NotImplementedError, 'Converting an existing column to primary key is currently unsupported'
129
+ when Changes::DropPrimaryKey
130
+ raise NotImplementedError, 'Removing a primary key while leaving the column is currently unsupported'
131
+ when Changes::AllowNull
132
+ set_column_allow_null(element.name)
133
+ when Changes::DisallowNull
134
+ set_column_not_null(element.name)
135
+ when Changes::AlterColumnDefault
136
+ set_column_default(element.name, element.new_default)
137
+ when Changes::CreateIndex
138
+ fields = if element.btree?
139
+ element.fields.map(&:to_sequel)
140
+ else
141
+ element.fields.map(&:name)
142
+ end
143
+
144
+ add_index(
145
+ fields,
146
+ name: element.name,
147
+ unique: element.unique?,
148
+ type: element.type,
149
+ where: element.condition
150
+ )
151
+ when Changes::DropIndex
152
+ drop_index([], name: element.name)
153
+ when Changes::CreateCheckConstraint
154
+ add_constraint(element.name, element.condition)
155
+ when Changes::DropCheckConstraint
156
+ drop_constraint(element.name)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def create_foreign_key(change)
163
+ DbSchema.connection.alter_table(change.table_name) do
164
+ add_foreign_key(change.foreign_key.fields, change.foreign_key.table, change.foreign_key.options)
165
+ end
166
+ end
167
+
168
+ def drop_foreign_key(change)
169
+ DbSchema.connection.alter_table(change.table_name) do
170
+ drop_foreign_key([], name: change.fkey_name)
171
+ end
172
+ end
173
+
174
+ def create_enum(change)
175
+ DbSchema.connection.create_enum(change.name, change.values)
176
+ end
177
+
178
+ def drop_enum(change)
179
+ DbSchema.connection.drop_enum(change.name)
180
+ end
181
+
182
+ def add_value_to_enum(change)
183
+ if change.add_to_the_end?
184
+ DbSchema.connection.add_enum_value(change.enum_name, change.new_value)
185
+ else
186
+ DbSchema.connection.add_enum_value(change.enum_name, change.new_value, before: change.before)
187
+ end
188
+ end
189
+
190
+ def map_options(type, options)
191
+ mapping = case type
192
+ when :char, :varchar, :bit, :varbit
193
+ Utils.rename_keys(options, length: :size)
194
+ when :numeric
195
+ Utils.rename_keys(options) do |new_options|
196
+ precision, scale = Utils.delete_at(new_options, :precision, :scale)
197
+
198
+ if precision
199
+ if scale
200
+ new_options[:size] = [precision, scale]
201
+ else
202
+ new_options[:size] = precision
203
+ end
204
+ end
205
+ end
206
+ when :interval
207
+ Utils.rename_keys(options, precision: :size) do |new_options|
208
+ new_options[:type] = "INTERVAL #{new_options.delete(:fields).upcase}"
209
+ end
210
+ when :array
211
+ Utils.rename_keys(options) do |new_options|
212
+ new_options[:type] = "#{new_options.delete(:element_type)}[]"
213
+ end
214
+ else
215
+ options
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,50 @@
1
+ module DbSchema
2
+ module Utils
3
+ class << self
4
+ def rename_keys(hash, mapping = {})
5
+ hash.reduce({}) do |final_hash, (key, value)|
6
+ new_key = mapping.fetch(key, key)
7
+ final_hash.merge(new_key => value)
8
+ end.tap do |final_hash|
9
+ yield(final_hash) if block_given?
10
+ end
11
+ end
12
+
13
+ def filter_by_keys(hash, *needed_keys)
14
+ hash.reduce({}) do |final_hash, (key, value)|
15
+ if needed_keys.include?(key)
16
+ final_hash.merge(key => value)
17
+ else
18
+ final_hash
19
+ end
20
+ end
21
+ end
22
+
23
+ def delete_at(hash, *keys)
24
+ keys.map do |key|
25
+ hash.delete(key)
26
+ end
27
+ end
28
+
29
+ def symbolize_keys(hash)
30
+ return hash unless hash.is_a?(Hash)
31
+
32
+ hash.reduce({}) do |new_hash, (key, value)|
33
+ new_hash.merge(key.to_sym => symbolize_keys(value))
34
+ end
35
+ end
36
+
37
+ def sort_by_class(array, sorted_classes)
38
+ sorted_classes.flat_map do |klass|
39
+ array.select { |object| object.is_a?(klass) }
40
+ end
41
+ end
42
+
43
+ def filter_by_class(array, klass)
44
+ array.select do |element|
45
+ element.is_a?(klass)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,89 @@
1
+ module DbSchema
2
+ module Validator
3
+ class << self
4
+ def validate(schema)
5
+ tables = Utils.filter_by_class(schema, Definitions::Table)
6
+ enums = Utils.filter_by_class(schema, Definitions::Enum)
7
+
8
+ table_errors = tables.each_with_object([]) do |table, errors|
9
+ primary_keys_count = table.fields.select(&:primary_key?).count
10
+ if primary_keys_count > 1
11
+ error_message = %(Table "#{table.name}" has #{primary_keys_count} primary keys)
12
+ errors << error_message
13
+ end
14
+
15
+ table.fields.each do |field|
16
+ if field.is_a?(Definitions::Field::Custom)
17
+ unless enums.map(&:name).include?(field.type_name)
18
+ error_message = %(Field "#{table.name}.#{field.name}" has unknown type "#{field.type_name}")
19
+ errors << error_message
20
+ end
21
+ end
22
+ end
23
+
24
+ field_names = table.fields.map(&:name)
25
+
26
+ table.indices.each do |index|
27
+ index.fields.map(&:name).each do |field_name|
28
+ unless field_names.include?(field_name)
29
+ error_message = %(Index "#{index.name}" refers to a missing field "#{table.name}.#{field_name}")
30
+ errors << error_message
31
+ end
32
+ end
33
+ end
34
+
35
+ table.foreign_keys.each do |fkey|
36
+ fkey.fields.each do |field_name|
37
+ unless field_names.include?(field_name)
38
+ error_message = %(Foreign key "#{fkey.name}" constrains a missing field "#{table.name}.#{field_name}")
39
+ errors << error_message
40
+ end
41
+ end
42
+
43
+ if referenced_table = schema.find { |table| table.name == fkey.table }
44
+ if fkey.references_primary_key?
45
+ unless referenced_table.fields.any?(&:primary_key?)
46
+ error_message = %(Foreign key "#{fkey.name}" refers to primary key of table "#{fkey.table}" which does not have a primary key)
47
+ errors << error_message
48
+ end
49
+ else
50
+ referenced_table_field_names = referenced_table.fields.map(&:name)
51
+
52
+ fkey.keys.each do |key|
53
+ unless referenced_table_field_names.include?(key)
54
+ error_message = %(Foreign key "#{fkey.name}" refers to a missing field "#{fkey.table}.#{key}")
55
+ errors << error_message
56
+ end
57
+ end
58
+ end
59
+ else
60
+ error_message = %(Foreign key "#{fkey.name}" refers to a missing table "#{fkey.table}")
61
+ errors << error_message
62
+ end
63
+ end
64
+ end
65
+
66
+ enum_errors = enums.each_with_object([]) do |enum, errors|
67
+ if enum.values.empty?
68
+ error_message = %(Enum "#{enum.name}" contains no values)
69
+ errors << error_message
70
+ end
71
+ end
72
+
73
+ Result.new(table_errors + enum_errors)
74
+ end
75
+ end
76
+
77
+ class Result
78
+ attr_reader :errors
79
+
80
+ def initialize(errors)
81
+ @errors = errors
82
+ end
83
+
84
+ def valid?
85
+ errors.empty?
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,3 @@
1
+ module DbSchema
2
+ VERSION = '0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,239 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db_schema
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Vsevolod Romashov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-07-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-equalizer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: awesome_print
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: guard-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: terminal-notifier
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: terminal-notifier-guard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: pg
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: A database schema management tool that reads a "single-source-of-truth"
168
+ schema definition from a ruby file and auto-migrates the database to conform to
169
+ it.
170
+ email:
171
+ - 7@7vn.ru
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files: []
175
+ files:
176
+ - ".gitignore"
177
+ - ".rspec"
178
+ - ".travis.yml"
179
+ - Gemfile
180
+ - Guardfile
181
+ - LICENSE.txt
182
+ - README.md
183
+ - Rakefile
184
+ - bin/console
185
+ - bin/setup
186
+ - db_schema.gemspec
187
+ - lib/db_schema.rb
188
+ - lib/db_schema/awesome_print.rb
189
+ - lib/db_schema/changes.rb
190
+ - lib/db_schema/configuration.rb
191
+ - lib/db_schema/definitions.rb
192
+ - lib/db_schema/definitions/field.rb
193
+ - lib/db_schema/definitions/field/array.rb
194
+ - lib/db_schema/definitions/field/base.rb
195
+ - lib/db_schema/definitions/field/binary.rb
196
+ - lib/db_schema/definitions/field/bit_string.rb
197
+ - lib/db_schema/definitions/field/boolean.rb
198
+ - lib/db_schema/definitions/field/character.rb
199
+ - lib/db_schema/definitions/field/custom.rb
200
+ - lib/db_schema/definitions/field/datetime.rb
201
+ - lib/db_schema/definitions/field/geometric.rb
202
+ - lib/db_schema/definitions/field/json.rb
203
+ - lib/db_schema/definitions/field/monetary.rb
204
+ - lib/db_schema/definitions/field/network.rb
205
+ - lib/db_schema/definitions/field/numeric.rb
206
+ - lib/db_schema/definitions/field/range.rb
207
+ - lib/db_schema/definitions/field/text_search.rb
208
+ - lib/db_schema/definitions/field/uuid.rb
209
+ - lib/db_schema/dsl.rb
210
+ - lib/db_schema/reader.rb
211
+ - lib/db_schema/runner.rb
212
+ - lib/db_schema/utils.rb
213
+ - lib/db_schema/validator.rb
214
+ - lib/db_schema/version.rb
215
+ homepage: https://github.com/7even/db_schema
216
+ licenses:
217
+ - MIT
218
+ metadata: {}
219
+ post_install_message:
220
+ rdoc_options: []
221
+ require_paths:
222
+ - lib
223
+ required_ruby_version: !ruby/object:Gem::Requirement
224
+ requirements:
225
+ - - ">="
226
+ - !ruby/object:Gem::Version
227
+ version: '0'
228
+ required_rubygems_version: !ruby/object:Gem::Requirement
229
+ requirements:
230
+ - - ">="
231
+ - !ruby/object:Gem::Version
232
+ version: '0'
233
+ requirements: []
234
+ rubyforge_project:
235
+ rubygems_version: 2.5.1
236
+ signing_key:
237
+ specification_version: 4
238
+ summary: Declarative database schema definition.
239
+ test_files: []