db-evolve 0.1.4

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a7f3f14f0e03a5b37bc20363d266cfc1b4b01d14
4
+ data.tar.gz: b26ebd66e911fad82cfda7a7d0b808e64b5d58e2
5
+ SHA512:
6
+ metadata.gz: 158eef8c638f24938426a879f4ee721a8734b33debd8cbb653a8f3c279dff368455afcb4130b1d4a8d8ace3beecfae514ff4f772f3d4ce84acf29219ede63c6c
7
+ data.tar.gz: 251428b0ca100a1c8eaa1d97ecb4cfdc78c7fb64d775c815f72997fa2706b92cbcf8f9ce3ae0f5b0b8e01ff6979fcf5c026cc073e3905ce6031a08fa37a10821
@@ -0,0 +1,9 @@
1
+ require 'rails'
2
+ class RubyDBEvolve
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ require_relative 'tasks/db'
6
+ end
7
+ end
8
+ end
9
+
@@ -0,0 +1,482 @@
1
+ require 'set'
2
+ require 'active_record'
3
+ require 'active_support/all'
4
+
5
+ module ActiveRecord
6
+ class Migration
7
+ def iloaded()
8
+ end
9
+ end
10
+ class Schema
11
+ def self.define(x)
12
+ raise "\nTo use rails-db-evolve, please edit your schema.db file and change:\n\n ActiveRecord::Schema.define(...) do\n\nto:\n\n DB::Schema.define do\n\nAnd re-run this task.\n\n"
13
+ end
14
+ end
15
+ end
16
+
17
+
18
+ namespace :db do
19
+
20
+ desc "Diff your database against your schema.rb and offer SQL to bring your database up to date."
21
+ task :evolve, [:arg1,:arg2] => :environment do |t, args|
22
+
23
+ argv = [ args[:arg1], args[:arg2] ]
24
+ noop = argv.include? "noop"
25
+ nowait = argv.include? "nowait"
26
+ yes = argv.include? "yes"
27
+
28
+ # confirm our shim is in place before we load schema.rb
29
+ # lest we accidentally drop and reload their database!
30
+ ActiveRecord::Schema.iloaded
31
+
32
+ do_evolve(noop, yes, nowait)
33
+
34
+ end
35
+
36
+ # mock db:schema:load db:test:load so "rake spec" works
37
+ namespace :test do
38
+ task :load do
39
+ # do nothing
40
+ end
41
+ end
42
+ namespace :schema do
43
+ task :load do
44
+ do_evolve(false, true, true)
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+
51
+ def do_evolve(noop, yes, nowait)
52
+ existing_tables, existing_indexes = load_existing_tables()
53
+
54
+ require_relative 'db_mock'
55
+
56
+ require Rails.root + 'db/schema'
57
+
58
+ adds, deletes, renames = calc_table_changes(existing_tables.keys, $schema_tables.keys, $akas_tables)
59
+
60
+ to_run = []
61
+
62
+ to_run += sql_adds(adds)
63
+ to_run += sql_renames(renames)
64
+
65
+ rename_cols_by_table = {}
66
+
67
+ existing_tables.each do |etn, ecols|
68
+ next if deletes.include? etn
69
+ ntn = renames[etn] || etn
70
+ commands, rename_cols = calc_column_changes(ntn, existing_tables[etn], $schema_tables[ntn].columns)
71
+ to_run += commands
72
+ rename_cols_by_table[ntn] = rename_cols
73
+ end
74
+
75
+ to_run += calc_index_changes(existing_indexes, $schema_indexes, renames, rename_cols_by_table)
76
+
77
+ to_run += sql_drops(deletes)
78
+
79
+ # prompt and execute
80
+
81
+ if to_run.empty?
82
+ if !noop
83
+ puts "\nYour database is up to date!"
84
+ puts
85
+ end
86
+ else
87
+ to_run.unshift("\nBEGIN TRANSACTION")
88
+ to_run.append("\nCOMMIT")
89
+
90
+ require_relative 'sql_color'
91
+ to_run.each do |sql|
92
+ puts SQLColor.colorize(sql)
93
+ end
94
+ puts
95
+
96
+ if noop
97
+ return
98
+ end
99
+
100
+ config = ActiveRecord::Base.connection_config
101
+ puts "Connecting to database:"
102
+ config.each do |k,v|
103
+ next if k==:password
104
+ puts "\t#{k} => #{v}"
105
+ end
106
+
107
+ if !yes
108
+ print "Run this SQL? (type yes or no) "
109
+ end
110
+ if yes || STDIN.gets.strip=='yes'
111
+ require 'pg'
112
+ config = ActiveRecord::Base.connection_config
113
+ config.delete(:adapter)
114
+ config[:dbname] = config.delete(:database)
115
+ config[:user] = config.delete(:username)
116
+ if !nowait
117
+ print "\nExecuting in "
118
+ [3,2,1].each do |c|
119
+ print "#{c}..."
120
+ sleep(1)
121
+ end
122
+ end
123
+ puts
124
+ conn = PG::Connection.open(config)
125
+ to_run.each do |sql|
126
+ puts SQLColor.colorize(sql)
127
+ conn.exec(sql)
128
+ end
129
+ puts "\n--==[ COMPLETED ]==--"
130
+ else
131
+ puts "\n--==[ ABORTED ]==--"
132
+ end
133
+ puts
134
+ end
135
+
136
+
137
+ end
138
+
139
+
140
+ def calc_index_changes(existing_indexes, schema_indexes, table_renames, rename_cols_by_table)
141
+ # rename_cols_by_table is by the new table name
142
+ existing_indexes = Set.new existing_indexes
143
+ schema_indexes = Set.new schema_indexes
144
+
145
+ add_indexes = schema_indexes - existing_indexes
146
+ delete_indexes = existing_indexes - schema_indexes
147
+
148
+ $tmp_to_run = []
149
+
150
+ connection = ActiveRecord::Base.connection
151
+
152
+ add_indexes.each do |index|
153
+ table = index.delete(:table)
154
+ columns = index.delete(:columns)
155
+ connection.add_index table, columns, index
156
+ end
157
+
158
+ to_run = $tmp_to_run
159
+
160
+ delete_indexes.each do |index|
161
+ $tmp_to_run = []
162
+ table = index.delete(:table)
163
+ name = index[:name]
164
+ connection.remove_index table, :name => name
165
+ to_run.append($tmp_to_run[0].sub('DROP INDEX', 'DROP INDEX IF EXISTS'))
166
+ end
167
+
168
+ if !to_run.empty?
169
+ to_run.unshift("\n-- update indexes")
170
+ end
171
+
172
+ return to_run
173
+ end
174
+
175
+
176
+
177
+ IgnoreTables = Set.new ["schema_migrations"]
178
+
179
+ def load_existing_tables()
180
+ existing_tables = {}
181
+ existing_indexes = []
182
+ connection = ActiveRecord::Base.connection
183
+ connection.tables.sort.each do |tbl|
184
+ next if IgnoreTables.include? tbl
185
+ columns = connection.columns(tbl)
186
+ existing_tables[tbl] = columns
187
+ connection.indexes(tbl).each do |i|
188
+ index = {:table => i.table, :name => i.name, :columns => i.columns, :unique => i.unique}
189
+ existing_indexes.append(index)
190
+ end
191
+ end
192
+ return existing_tables, existing_indexes
193
+ end
194
+
195
+
196
+
197
+
198
+ class Table
199
+ attr_accessor :name, :opts, :id, :columns
200
+
201
+ def initialize()
202
+ @columns = []
203
+ end
204
+
205
+ def method_missing(method_sym, *arguments, &block)
206
+ c = Column.new
207
+ c.type = method_sym.to_s
208
+ c.name = arguments[0]
209
+ c.opts = arguments[1]
210
+ if c.opts
211
+ c.default = c.opts["default"]
212
+ c.default = c.opts["null"]
213
+ aka = c.opts[:aka]
214
+ if !aka.nil?
215
+ if aka.respond_to?('each')
216
+ c.akas = aka
217
+ else
218
+ c.akas = [aka]
219
+ end
220
+ end
221
+ else
222
+ c.opts = {}
223
+ end
224
+ @columns.append c
225
+ end
226
+
227
+ end
228
+
229
+ class Column
230
+ attr_accessor :name, :type, :opts, :null, :default, :akas
231
+ end
232
+
233
+ $schema_tables = {}
234
+ $akas_tables = Hash.new { |h, k| h[k] = Set.new }
235
+
236
+ def create_table(name, opts={})
237
+ tbl = Table.new
238
+ tbl.name = name
239
+ tbl.opts = opts
240
+ if opts
241
+ if opts.has_key? 'id'
242
+ tbl.id = opts['id']
243
+ else
244
+ tbl.id = true
245
+ end
246
+ end
247
+ if tbl.id
248
+ c = Column.new
249
+ c.type = "integer"
250
+ c.name = "id"
251
+ c.opts = { :null=>false }
252
+ tbl.columns.append c
253
+ end
254
+ yield tbl
255
+ $schema_tables[name] = tbl
256
+ aka = tbl.opts[:aka]
257
+ if !aka.nil?
258
+ if aka.respond_to?('each')
259
+ $akas_tables[tbl.name].merge(aka)
260
+ else
261
+ $akas_tables[tbl.name].add(aka)
262
+ end
263
+ end
264
+ end
265
+
266
+ $schema_indexes = []
267
+
268
+ def add_index(table, columns, opts)
269
+ opts[:table] = table
270
+ opts[:columns] = columns
271
+ if !opts.has_key? :unique
272
+ opts[:unique] = false
273
+ end
274
+ $schema_indexes.append(opts)
275
+ end
276
+
277
+ module DB
278
+ module Evolve
279
+ class Schema
280
+ def self.define()
281
+ yield
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ def calc_table_changes(existing_tables, schema_tables, akas_tables)
288
+ existing_tables = Set.new existing_tables
289
+ schema_tables = Set.new schema_tables
290
+ adds = schema_tables - existing_tables
291
+ deletes = existing_tables - schema_tables
292
+ renames = {}
293
+ adds.each do |newt|
294
+ akas = Set.new akas_tables[newt]
295
+ possibles = akas & deletes
296
+ if possibles.size > 1
297
+ raise "Too many possible table matches (#{possibles}) for #{newt}. Please trim your akas."
298
+ end
299
+ if possibles.size == 1
300
+ oldt = possibles.to_a()[0]
301
+ renames[oldt] = newt
302
+ adds.delete(newt)
303
+ deletes.delete(oldt)
304
+ end
305
+ end
306
+ return adds, deletes, renames
307
+ end
308
+
309
+ def escape_table(k)
310
+ return PG::Connection.quote_ident k
311
+ end
312
+
313
+ def gen_pg_adapter()
314
+ $tmp_to_run = []
315
+ a = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.allocate
316
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_method(:initialize).bind(a).call ActiveRecord::Base.connection
317
+ return a
318
+ end
319
+
320
+ def sql_renames(renames)
321
+ to_run = []
322
+ pg_a = gen_pg_adapter()
323
+ renames.each do |k,v|
324
+ pg_a.rename_table(k, v)
325
+ to_run += $tmp_to_run
326
+ end
327
+ if !to_run.empty?
328
+ to_run.unshift("\n-- rename tables")
329
+ end
330
+ return to_run
331
+ end
332
+
333
+ def sql_drops(tables)
334
+ to_run = []
335
+ tables.each do |tbl|
336
+ sql = "DROP TABLE #{escape_table(tbl)}"
337
+ to_run.append sql
338
+ end
339
+ if !to_run.empty?
340
+ to_run.unshift("\n-- remove tables")
341
+ end
342
+ return to_run
343
+ end
344
+
345
+ def sql_adds(tables)
346
+ a = gen_pg_adapter()
347
+ tables.each do |tn|
348
+ tbl = $schema_tables[tn]
349
+ a.create_table tbl.name, :force => true do |t|
350
+ tbl.columns.each do |c|
351
+ t.send(c.type.to_sym, *[c.name, c.opts])
352
+ end
353
+ end
354
+ end
355
+ if !$tmp_to_run.empty?
356
+ $tmp_to_run.unshift("\n-- add tables")
357
+ end
358
+ return $tmp_to_run
359
+ end
360
+
361
+ def can_convert(type1, type2)
362
+ if type1==type2
363
+ return true
364
+ end
365
+ if type1=='integer' and type2=='decimal'
366
+ return true
367
+ end
368
+ return false
369
+ end
370
+
371
+ # taken from comments in ActiveRecord::ConnectionAdapters::TableDefinition
372
+ NATIVE_DATABASE_PRECISION = {
373
+ :numeric => 19,
374
+ :decimal => 19, #38,
375
+ }
376
+ NATIVE_DATABASE_SCALE = {
377
+ }
378
+
379
+ def calc_column_changes(tbl, existing_cols, schema_cols)
380
+
381
+ existing_cols_by_name = Hash[existing_cols.collect { |c| [c.name, c] }]
382
+ schema_cols_by_name = Hash[schema_cols.collect { |c| [c.name, c] }]
383
+ existing_col_names = Set.new existing_cols_by_name.keys
384
+ schema_col_names = Set.new schema_cols_by_name.keys
385
+ new_cols = schema_col_names - existing_col_names
386
+ delete_cols = existing_col_names - schema_col_names
387
+ rename_cols = {}
388
+
389
+ new_cols.each do |cn|
390
+ sc = schema_cols_by_name[cn]
391
+ if sc.akas
392
+ sc.akas.each do |aka|
393
+ if delete_cols.include? aka
394
+ ec = existing_cols_by_name[aka]
395
+ if can_convert(sc.type.to_s, ec.type.to_s)
396
+ rename_cols[ec.name] = sc.name
397
+ new_cols.delete cn
398
+ delete_cols.delete aka
399
+ end
400
+ end
401
+ end
402
+ end
403
+ end
404
+
405
+ to_run = []
406
+
407
+ pg_a = gen_pg_adapter()
408
+
409
+ if new_cols.size > 0
410
+ # puts "tbl: #{tbl} new_cols: #{new_cols}"
411
+ new_cols.each do |cn|
412
+ sc = schema_cols_by_name[cn]
413
+ pg_a.add_column(tbl, cn, sc.type.to_sym, sc.opts)
414
+ end
415
+ to_run += $tmp_to_run
416
+ end
417
+
418
+ $tmp_to_run = []
419
+ rename_cols.each do |ecn, scn|
420
+ pg_a.rename_column(tbl, ecn, scn)
421
+ end
422
+ to_run += $tmp_to_run
423
+ delete_cols.each do |cn|
424
+ to_run.append("ALTER TABLE #{escape_table(tbl)} DROP COLUMN #{escape_table(cn)}")
425
+ end
426
+
427
+ same_names = existing_col_names - delete_cols
428
+ same_names.each do |ecn|
429
+ $tmp_to_run = []
430
+ ec = existing_cols_by_name[ecn]
431
+ if rename_cols.include? ecn
432
+ sc = schema_cols_by_name[rename_cols[ecn]]
433
+ else
434
+ sc = schema_cols_by_name[ecn]
435
+ end
436
+ type_changed = sc.type.to_s != ec.type.to_s
437
+ # numeric and decimal are equiv in postges, and the db always returns numeric
438
+ if type_changed and sc.type.to_s=="decimal" and ec.type.to_s=="numeric"
439
+ type_changed = false
440
+ end
441
+ # ruby turns decimal(x,0) into integer when reading meta-data
442
+ if type_changed and sc.type.to_s=="decimal" and ec.type.to_s=="integer" and sc.opts[:scale]==0
443
+ type_changed = false
444
+ end
445
+ sc_limit = sc.opts.has_key?(:limit) ? sc.opts[:limit] : ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[sc.type.to_sym][:limit]
446
+ limit_changed = (sc.type=="string" and sc_limit!=ec.limit) # numeric types in postgres report the precision as the limit - ignore non string types for now
447
+ sc_precision = sc.opts.has_key?(:precision) ? sc.opts[:precision] : NATIVE_DATABASE_PRECISION[sc.type]
448
+ precision_changed = (sc.type=="decimal" and sc_precision!=ec.precision) # by type_to_sql in schema_statements.rb, precision is only used on decimal types
449
+ sc_scale = sc.opts.has_key?(:scale) ? sc.opts[:scale] : NATIVE_DATABASE_SCALE[sc.type]
450
+ scale_changed = (sc.type=="decimal" and sc_scale!=ec.scale)
451
+ if type_changed or limit_changed or precision_changed or scale_changed
452
+ pg_a.change_column(tbl, sc.name, sc.type.to_sym, sc.opts)
453
+ end
454
+ if ec.default != sc.opts[:default]
455
+ pg_a.change_column_default(tbl, sc.name, sc.opts[:default])
456
+ end
457
+ sc_null = sc.opts.has_key?(:null) ? sc.opts[:null] : true
458
+ if ec.null != sc_null
459
+ if !sc_null and !sc.opts.has_key?(:default)
460
+ raise "\nERROR: In order to set #{tbl}.#{sc.name} as NOT NULL you need to add a :default value.\n\n"
461
+ end
462
+ pg_a.change_column_null(tbl, sc.name, sc_null, sc.opts[:default])
463
+ end
464
+ to_run += $tmp_to_run
465
+ end
466
+
467
+ if !to_run.empty?
468
+ to_run.unshift("\n-- column changes for table #{tbl}")
469
+ end
470
+
471
+ return to_run, rename_cols
472
+ end
473
+
474
+
475
+
476
+ $tmp_to_run = []
477
+
478
+
479
+
480
+
481
+
482
+
@@ -0,0 +1,24 @@
1
+ require 'pg'
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ class PostgreSQLAdapter
6
+ def execute(sql, name = nil)
7
+ $tmp_to_run.append sql
8
+ end
9
+ def table_exists?(name)
10
+ false
11
+ end
12
+ def clear_cache!
13
+ end
14
+ def quote_string s
15
+ # hack to prevent double-quoting when setting default
16
+ return s
17
+ end
18
+ def escape(s)
19
+ return PG::Connection.quote_ident s
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,32 @@
1
+
2
+
3
+ class SQLColor
4
+
5
+ BLACK = 30
6
+ RED = 31
7
+ GREEN = 32
8
+ YELLOW = 33
9
+ BLUE = 34
10
+ MAGENTA = 35
11
+ CYAN = 36
12
+ WHITE = 37
13
+
14
+ def self.colorize(sql)
15
+ if sql.strip.start_with?('--')
16
+ return apply(CYAN, sql)
17
+ end
18
+ sql = sql.gsub(/(CREATE|ALTER|TABLE|COLUMN|ADD|TYPE|BEGIN|TRANSACTION|COMMIT| ON |INDEX|UPDATE|SET|WHERE)/){|s|apply(GREEN, s)}
19
+ sql = sql.gsub(/(DROP)/){|s|apply(RED, s)}
20
+ sql = sql.gsub(/("[^"]*")/){|s|apply(WHITE, s, bold=true)}
21
+ return sql
22
+ end
23
+
24
+ def self.apply(color_code, text, bold=false)
25
+ bold = bold ? ";1" : ""
26
+ "\e[#{color_code}#{bold}m#{text}\e[0m"
27
+ end
28
+
29
+ end
30
+
31
+
32
+
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db-evolve
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Derek Anderson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A diff/patch between your schema.rb and what's in your database.
14
+ email: public@kered.org
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/db-evolve.rb
20
+ - lib/tasks/db.rb
21
+ - lib/tasks/db_mock.rb
22
+ - lib/tasks/sql_color.rb
23
+ homepage: https://github.com/keredson/ruby-db-evolve
24
+ licenses:
25
+ - GPLv2
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubyforge_project:
43
+ rubygems_version: 2.2.2
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Schema Evolution for Ruby
47
+ test_files: []
48
+ has_rdoc: