dbdiff 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/CHANGELOG +6 -0
  2. data/LICENSE +58 -0
  3. data/README +29 -0
  4. data/lib/dbdiff/column.rb +69 -0
  5. data/lib/dbdiff/database.rb +169 -0
  6. data/lib/dbdiff/delta.rb +266 -0
  7. data/lib/dbdiff/foreign_key.rb +68 -0
  8. data/lib/dbdiff/key.rb +69 -0
  9. data/lib/dbdiff/row.rb +32 -0
  10. data/lib/dbdiff/table.rb +50 -0
  11. data/lib/dbdiff/table_element.rb +16 -0
  12. data/lib/dbdiff.rb +207 -0
  13. data/test/ai_column/source.sql +10 -0
  14. data/test/ai_column/target.sql +7 -0
  15. data/test/change_pk/source.sql +11 -0
  16. data/test/change_pk/target.sql +8 -0
  17. data/test/column/source.sql +10 -0
  18. data/test/column/target.sql +9 -0
  19. data/test/fk/source.sql +19 -0
  20. data/test/fk/target.sql +20 -0
  21. data/test/key/source.sql +11 -0
  22. data/test/key/target.sql +10 -0
  23. data/test/modify_column/source.sql +9 -0
  24. data/test/modify_column/target.sql +10 -0
  25. data/test/modify_fk/source.sql +20 -0
  26. data/test/modify_fk/target.sql +22 -0
  27. data/test/modify_key_fk/source.sql +19 -0
  28. data/test/modify_key_fk/target.sql +20 -0
  29. data/test/modify_key_fk_ref/source.sql +18 -0
  30. data/test/modify_key_fk_ref/target.sql +20 -0
  31. data/test/modify_row/source.sql +8 -0
  32. data/test/modify_row/target.sql +9 -0
  33. data/test/modify_table/source.sql +8 -0
  34. data/test/modify_table/target.sql +9 -0
  35. data/test/multi_fk/source.sql +22 -0
  36. data/test/multi_fk/target.sql +20 -0
  37. data/test/multi_key/source.sql +11 -0
  38. data/test/multi_key/target.sql +10 -0
  39. data/test/multi_unique_key/source.sql +11 -0
  40. data/test/multi_unique_key/target.sql +10 -0
  41. data/test/row/source.sql +8 -0
  42. data/test/row/target.sql +6 -0
  43. data/test/suite.rb +7 -0
  44. data/test/table/source.sql +9 -0
  45. data/test/table/target.sql +0 -0
  46. data/test/table_fk/source.sql +13 -0
  47. data/test/table_fk/target.sql +20 -0
  48. data/test/table_fk2/source.sql +17 -0
  49. data/test/table_fk2/target.sql +5 -0
  50. data/test/test_column.rb +93 -0
  51. data/test/test_dbdiff.rb +92 -0
  52. data/test/test_foreign_key.rb +136 -0
  53. data/test/test_key.rb +116 -0
  54. data/test/test_row.rb +84 -0
  55. data/test/test_table.rb +30 -0
  56. data/test/unique_key/source.sql +11 -0
  57. data/test/unique_key/target.sql +9 -0
  58. metadata +127 -0
data/CHANGELOG ADDED
@@ -0,0 +1,6 @@
1
+ = DbDiff CHANGELOG
2
+
3
+ == 0.1.0
4
+
5
+ * Initial Release
6
+
data/LICENSE ADDED
@@ -0,0 +1,58 @@
1
+ DbDiff is copyrighted free software by Eric Kolve <ekolve@gmail.com>.
2
+ You can redistribute it and/or modify it under either the terms of the GPL
3
+ (see COPYING file), or the conditions below:
4
+
5
+ 1. You may make and give away verbatim copies of the source form of the
6
+ software without restriction, provided that you duplicate all of the
7
+ original copyright notices and associated disclaimers.
8
+
9
+ 2. You may modify your copy of the software in any way, provided that
10
+ you do at least ONE of the following:
11
+
12
+ a) place your modifications in the Public Domain or otherwise
13
+ make them Freely Available, such as by posting said
14
+ modifications to Usenet or an equivalent medium, or by allowing
15
+ the author to include your modifications in the software.
16
+
17
+ b) use the modified software only within your corporation or
18
+ organization.
19
+
20
+ c) rename any non-standard executables so the names do not conflict
21
+ with standard executables, which must also be provided.
22
+
23
+ d) make other distribution arrangements with the author.
24
+
25
+ 3. You may distribute the software in object code or executable
26
+ form, provided that you do at least ONE of the following:
27
+
28
+ a) distribute the executables and library files of the software,
29
+ together with instructions (in the manual page or equivalent)
30
+ on where to get the original distribution.
31
+
32
+ b) accompany the distribution with the machine-readable source of
33
+ the software.
34
+
35
+ c) give non-standard executables non-standard names, with
36
+ instructions on where to get the original software distribution.
37
+
38
+ d) make other distribution arrangements with the author.
39
+
40
+ 4. You may modify and include the part of the software into any other
41
+ software (possibly commercial). But some files in the distribution
42
+ are not written by the author, so that they are not under this terms.
43
+
44
+ They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some
45
+ files under the ./missing directory. See each file for the copying
46
+ condition.
47
+
48
+ 5. The scripts and library files supplied as input to or produced as
49
+ output from the software do not automatically fall under the
50
+ copyright of the software, but belong to whomever generated them,
51
+ and may be sold commercially, and may be aggregated with this
52
+ software.
53
+
54
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
55
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
56
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
57
+ PURPOSE.
58
+
data/README ADDED
@@ -0,0 +1,29 @@
1
+ = DbDiff
2
+
3
+ DbDiff allows you to compare two different databases (currently MySQL),
4
+ determine the differences between the two, and apply the deltas.
5
+ Currently the following types of deltas are supported: tables,
6
+ columns, keys, foreign keys and rows.
7
+
8
+ Ultimately, I would like to be able to compare two completely different
9
+ types of DBs (i.e. Oracle <=> MySQL) and bring them up to date as much
10
+ as possible. Things like stored procs and triggers may be out of the realm
11
+ of possibility, since they may not directly translate.
12
+
13
+
14
+ == Dependencies
15
+
16
+ * ruby 1.8.4
17
+ * mysql client
18
+
19
+
20
+ == Authors
21
+
22
+ Copyright (c) 2006 by Eric Kolve <ekolve@gmail.com>
23
+
24
+
25
+ == License
26
+
27
+ This library is distributed under the same terms as Ruby.
28
+ Please see the LICENSE file.
29
+
@@ -0,0 +1,69 @@
1
+ class DbDiff
2
+ class Column < TableElement
3
+ attr_reader :data_type, :default, :numeric_precision, :column_type, :length, :not_null
4
+ attr_accessor :auto_increment
5
+
6
+ def initialize(info = {})
7
+ super
8
+
9
+ @name = info['COLUMN_NAME']
10
+ @data_type = info['DATA_TYPE']
11
+ @column_type = info['COLUMN_TYPE']
12
+ @numeric_precision = info['NUMERIC_PRECISION']
13
+
14
+ @default = (info['COLUMN_DEFAULT'] == 'NULL' ? nil : info['COLUMN_DEFAULT'])
15
+
16
+ # XXX add type and length parsing
17
+ @not_null = (info['IS_NULLABLE'] == 'NO' ? true : false)
18
+ @auto_increment = (info['EXTRA'] == 'auto_increment' ? true : false)
19
+ end
20
+
21
+
22
+ def modify_delta(new_element)
23
+ Delta::ModifyColumn.new(new_element)
24
+ end
25
+
26
+ def drop_delta
27
+ if self.auto_increment
28
+ clone = self.deep_clone
29
+ clone.auto_increment = false
30
+
31
+ # we have to change this to auto_increment = false
32
+ # since its the primary key and we can't drop a primary key without disabling
33
+ # auto_increment
34
+ return [Delta::ModifyColumnRemoveAI.new(clone), Delta::DropColumn.new(clone)]
35
+ else
36
+ return Delta::DropColumn.new(self)
37
+ end
38
+ end
39
+
40
+ def add_delta
41
+ if self.auto_increment
42
+ [Delta::AddColumn.new(self), Delta::ModifyColumnAddAI.new(self)]
43
+ else
44
+ Delta::AddColumn.new(self)
45
+ end
46
+ end
47
+
48
+
49
+ def definition(add_sql = false)
50
+ sql = "`%s` %s" % [self.name, self.column_type]
51
+
52
+ sql += (@not_null ? ' NOT NULL' : ' NULL ')
53
+
54
+ sql += (@default ? " default '#{default}' " : '')
55
+ sql += " auto_increment" if !add_sql && self.auto_increment
56
+
57
+ sql
58
+ end
59
+
60
+ def ==(other)
61
+ attribs = %w(column_type default name not_null auto_increment)
62
+
63
+ matches = attribs.find_all{|a| self.send(a.to_sym) == other.send(a.to_sym)}
64
+ matches.size == attribs.size
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,169 @@
1
+ require 'rubygems'
2
+ require 'mysql'
3
+ require 'dbdiff/delta'
4
+ require 'dbdiff/table_element'
5
+ require 'dbdiff/table'
6
+ require 'dbdiff/column'
7
+ require 'dbdiff/foreign_key'
8
+ require 'dbdiff/key'
9
+ require 'dbdiff/row'
10
+ class DbDiff
11
+
12
+ # XXX validate state of information_schema?
13
+ class Database
14
+ attr_reader :dbh, :tables, :name, :host, :user, :password
15
+ attr_accessor :deltas
16
+
17
+ # XXX add version check
18
+ def initialize(params = {})
19
+ @dbh = Mysql.real_connect(params[:host], params[:user], params[:password], 'information_schema')
20
+ @name = params[:name]
21
+ @host = params[:host]
22
+ @user = params[:user]
23
+ @password = params[:password]
24
+ @deltas = []
25
+
26
+ initialize_tables
27
+ initialize_columns
28
+ initialize_keys
29
+ initialize_foreign_keys
30
+ end
31
+
32
+ # XXX may be able to get rid of this
33
+ def all_tables
34
+ @tables + @deltas.find_all{|d| d.class == Delta::AddTable }.map {|x| x.element}
35
+ end
36
+
37
+ def initialize_tables
38
+ @tables = []
39
+ select_is(:tables, :table_schema => self.name) do |h|
40
+ t = Table.new(h)
41
+ t.charset = charset(t.collation)
42
+
43
+ @tables << t
44
+ end
45
+ end
46
+
47
+ def charset(collation)
48
+ hash = select_is(:character_sets, :default_collate_name => collation)
49
+ return hash['CHARACTER_SET_NAME']
50
+ end
51
+
52
+ def table(name)
53
+ @tables.find{|t| t.name == name}
54
+ end
55
+
56
+ def initialize_columns
57
+ # puts "table name => #{self.name}"
58
+
59
+ select_is(:columns) do |h|
60
+ c = Column.new(h)
61
+ table(c.table_name).columns << c
62
+ end
63
+ end
64
+
65
+ def initialize_keys
66
+
67
+ @tables.each do|t|
68
+ @dbh.select_db(name)
69
+ res = @dbh.query("SHOW INDEXES FROM #{t.name}")
70
+ res.each_hash do |h|
71
+ k = Key.new(t.name, h)
72
+ if y = t.keys.find {|z| z.name == k.name}
73
+ y.merge(k)
74
+ else
75
+ t.keys << k
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+
82
+ def initialize_foreign_keys
83
+ select_is(:key_column_usage) do |h|
84
+ next unless h['REFERENCED_TABLE_NAME']
85
+
86
+ c = ForeignKey.new(h)
87
+
88
+ t = table(c.table_name)
89
+
90
+ if y = t.foreign_keys.find {|z| z.name == c.name}
91
+ y.merge(c)
92
+ else
93
+ t.foreign_keys << c
94
+ end
95
+
96
+
97
+ end
98
+ end
99
+
100
+ def select_is(table, where = {}, &block)
101
+ @dbh.select_db('information_schema')
102
+
103
+ # XXX
104
+ where[:table_schema] = self.name unless table == :character_sets
105
+
106
+ self._select(table, where, &block)
107
+ end
108
+
109
+ def select(table, where = {}, &block)
110
+ @dbh.select_db(self.name)
111
+ self._select(table, where, &block)
112
+ end
113
+
114
+ # easy way of selecting
115
+ def _select(table, where = {}, &block)
116
+ query = "SELECT * from #{table} "
117
+ if where.length > 0
118
+ query += " WHERE "
119
+ query +=
120
+ where.map do |k,v|
121
+ if v
122
+ "#{k}='" + Mysql.escape_string(v) + "'"
123
+ else
124
+ "#{k} IS NULL"
125
+ end
126
+ end.join(" AND ")
127
+
128
+ end
129
+
130
+ # puts "sending query #{query}"
131
+ result = @dbh.query(query)
132
+ if block
133
+ result.each_hash do |h|
134
+ yield(h)
135
+ end
136
+ else
137
+ return result.fetch_hash
138
+ end
139
+ end
140
+
141
+ def add(element)
142
+ @deltas += element.add_delta.to_a
143
+ end
144
+
145
+ def modify(element, new_element)
146
+ @deltas += element.modify_delta(new_element).to_a
147
+ end
148
+
149
+ def drop(element)
150
+ @deltas += element.drop_delta.to_a
151
+ end
152
+
153
+ def load_rows(t)
154
+ t.each do |table_name|
155
+
156
+ table = table(table_name)
157
+
158
+ if table
159
+ select(table_name) do |h|
160
+ table.rows << Row.new(table, h)
161
+ end
162
+ end
163
+
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+
@@ -0,0 +1,266 @@
1
+ class DbDiff
2
+ class Delta
3
+ attr_reader :element
4
+
5
+ def initialize(element)
6
+ @element = element.deep_clone
7
+ end
8
+
9
+ def to_a
10
+ [self]
11
+ end
12
+
13
+ def table(database)
14
+ database.table(element.table_name)
15
+ end
16
+
17
+
18
+ class DropColumn < Delta
19
+
20
+
21
+ def sql
22
+ "ALTER TABLE #{element.table_name} DROP COLUMN `#{element.name}`"
23
+ end
24
+
25
+ def process(database)
26
+ table = table(database)
27
+ table.columns.delete(element)
28
+ end
29
+
30
+ end
31
+
32
+
33
+ class ModifyColumn < Delta
34
+
35
+ def sql
36
+ "ALTER table #{element.table_name} MODIFY COLUMN " + element.definition
37
+ end
38
+
39
+ def process(database)
40
+ table = table(database)
41
+ old_column = table.columns.find{|c| c.name == element.name}
42
+ table.columns.delete(old_column)
43
+ table.columns << element
44
+ end
45
+ end
46
+
47
+ class ModifyColumnRemoveAI < ModifyColumn
48
+
49
+ end
50
+
51
+ class ModifyColumnAddAI < ModifyColumn
52
+
53
+ end
54
+
55
+ class AddColumn < Delta
56
+
57
+ def initialize(element)
58
+ super
59
+ @element.auto_increment = false
60
+ end
61
+
62
+ def sql
63
+ "ALTER TABLE #{element.table_name} ADD COLUMN " + element.definition(true)
64
+ end
65
+
66
+ def process(database)
67
+ table = table(database)
68
+ table.columns << element
69
+ end
70
+ end
71
+
72
+
73
+ class ModifyRow < Delta
74
+
75
+ def sql
76
+ data = element.data
77
+ where = element.primary_key.map {|k| "#{k}=" + Mysql.escape_string(data[k]) }.join(" AND ")
78
+ sql = "UPDATE #{element.table_name} SET "
79
+
80
+ updates = data.keys.map do |c|
81
+ if data[c]
82
+ " #{c}='" + Mysql.escape_string(data[c]) + "'"
83
+ else
84
+ " #{c}= NULL"
85
+ end
86
+ end
87
+
88
+ sql += updates.join(",") + " WHERE #{where}"
89
+
90
+ return sql
91
+ end
92
+
93
+ def process(database)
94
+ table = table(database)
95
+ old_row = table.rows.find{|r| r.name == element.name}
96
+ table.rows.delete(old_row)
97
+ table.rows << element
98
+ end
99
+
100
+ end
101
+
102
+ class AddRow < Delta
103
+
104
+ def sql
105
+ sql = "INSERT INTO #{element.table_name}"
106
+ data = element.data
107
+ columns = data.keys
108
+
109
+ sql += " (" + columns.join(",") + ")"
110
+
111
+ values = columns.map do |c|
112
+ if data[c]
113
+ "'" + Mysql.escape_string(data[c]) + "'"
114
+ else
115
+ "NULL"
116
+ end
117
+ end
118
+
119
+ sql += " VALUES (" + values.join(",") + ")"
120
+
121
+ return sql
122
+ end
123
+
124
+ def process(database)
125
+ table = table(database)
126
+ table.rows.delete(element)
127
+ table.rows << element # XXX test this to make sure modifys work
128
+ end
129
+
130
+ end
131
+
132
+ class DropRow < Delta
133
+
134
+ def sql
135
+ data = element.data
136
+ where = element.primary_key.map {|k| "#{k}=" + Mysql.escape_string(data[k]) }.join(" AND ")
137
+ return "DELETE FROM #{element.table_name} WHERE #{where}"
138
+ end
139
+
140
+ def process(database)
141
+ table = table(database)
142
+ table.rows.delete(element)
143
+ end
144
+ end
145
+
146
+
147
+ class AddKey < Delta
148
+
149
+ def sql
150
+ "ALTER table #{element.table_name} ADD" + element.definition
151
+ end
152
+
153
+ def process(database)
154
+ table = table(database)
155
+ table.keys << element
156
+ end
157
+ end
158
+
159
+
160
+ class ModifyKey < Delta
161
+
162
+ def sql
163
+ "ALTER TABLE #{element.table_name} DROP KEY `#{element.name}`, ADD" + element.definition
164
+ end
165
+
166
+ def process(database)
167
+ table = table(database)
168
+ old_key = table.keys.find{|k| k.name == element.name}
169
+ table.keys.delete(old_key)
170
+ table.keys << element
171
+ end
172
+ end
173
+
174
+ class DropKey < Delta
175
+
176
+ def sql
177
+ "ALTER TABLE #{element.table_name} DROP KEY `#{element.name}`"
178
+ end
179
+
180
+ def process(database)
181
+ table = table(database)
182
+ table.keys.delete(element)
183
+ end
184
+ end
185
+
186
+ class AddForeignKey < Delta
187
+
188
+ def sql
189
+ "ALTER table #{element.table_name} ADD " + element.definition
190
+ end
191
+
192
+ def process(database)
193
+ table(database).foreign_keys << element
194
+ end
195
+
196
+ end
197
+
198
+
199
+ class DropForeignKey < Delta
200
+
201
+ def sql
202
+ "ALTER table #{element.table_name} DROP FOREIGN KEY `#{element.name}`;\n"
203
+ end
204
+
205
+ def process(database)
206
+ table(database).foreign_keys.delete(element)
207
+ end
208
+ end
209
+
210
+ class AddTable < Delta
211
+
212
+ def initialize(element)
213
+ super
214
+ # we don't clone foreign_keys or rows since
215
+ # the table_elements diff will take care of them
216
+ @element.rows = []
217
+ @element.foreign_keys = []
218
+ end
219
+
220
+ def sql
221
+ # XXX may need to SET FOREIGN_KEY_CHECKS = 0;
222
+ sql = "CREATE TABLE `%s` (\n" % element.name
223
+
224
+ # intentionally skip foreign_keys to avoid missing tables
225
+ # foreign_keys get picked up with the table_element diff
226
+ sql += (element.columns.map{|c| c.definition} +
227
+ element.keys.map{|k| k.definition}).join(",\n")
228
+
229
+ sql += "\n) ENGINE=%s DEFAULT CHARSET=%s" % [element.engine, element.charset]
230
+ sql
231
+ end
232
+
233
+ def process(database)
234
+ database.tables << element
235
+ end
236
+
237
+ end
238
+
239
+ class ModifyTable < Delta
240
+
241
+ def sql
242
+ "ALTER TABLE #{element.name} ENGINE=%s DEFAULT CHARSET=%s" % [element.engine, element.charset]
243
+ end
244
+
245
+ def process(database)
246
+ cur_table = database.tables.find{|t| t.name == element.name}
247
+ # need full copy of meta-data, but can't swap out the deltas or columns
248
+ cur_table.engine = element.engine.dup
249
+ cur_table.collation = element.collation.dup
250
+ end
251
+ end
252
+
253
+ class DropTable < Delta
254
+
255
+ def sql
256
+ "DROP TABLE #{element.name}"
257
+ end
258
+
259
+ def process(database)
260
+ database.tables.delete(element)
261
+ end
262
+ end
263
+ end
264
+ end
265
+
266
+
@@ -0,0 +1,68 @@
1
+
2
+ require 'dbdiff/table_element'
3
+ class DbDiff
4
+ class ForeignKey < TableElement
5
+ attr_reader :ref_table, :ref_column_data, :column_data, :name
6
+
7
+ def initialize(info = {})
8
+ super
9
+
10
+ # XXX need to work on tihs
11
+ @name = info['CONSTRAINT_NAME']
12
+ seq_index = info['ORDINAL_POSITION'].to_i - 1
13
+ @column_data = {}
14
+ @ref_column_data = {}
15
+
16
+ @column_data[seq_index] = info['COLUMN_NAME']
17
+
18
+ ref_seq_index = info['POSITION_IN_UNIQUE_CONSTRAINT'].to_i - 1
19
+ @ref_table = info['REFERENCED_TABLE_NAME']
20
+ @ref_column_data[ref_seq_index] = info['REFERENCED_COLUMN_NAME']
21
+ end
22
+
23
+ def merge(fk)
24
+
25
+ unless fk.name == self.name
26
+ raise "Error - names don't match #{fk.name} #{self.name}"
27
+ end
28
+
29
+ @column_data.merge!(fk.column_data)
30
+ @ref_column_data.merge!(fk.ref_column_data)
31
+ end
32
+
33
+ def ref_columns
34
+ @ref_column_data.keys.sort.map {|k| @ref_column_data[k]}
35
+ end
36
+
37
+ def columns
38
+ @column_data.keys.sort.map {|k| @column_data[k]}
39
+ end
40
+
41
+ def definition
42
+ return "CONSTRAINT `#{self.name}` FOREIGN KEY (`" + self.columns.join("`,`") + "`) references #{self.ref_table}(`" +
43
+ self.ref_columns.join("`,`") + "`)"
44
+ end
45
+
46
+ def modify_delta(new_element)
47
+ [self.drop_delta, new_element.add_delta]
48
+ end
49
+
50
+ def drop_delta
51
+ Delta::DropForeignKey.new(self)
52
+ end
53
+
54
+ def add_delta
55
+ Delta::AddForeignKey.new(self)
56
+ end
57
+
58
+ def ==(other)
59
+
60
+ self.name == other.name &&
61
+ self.columns == other.columns &&
62
+ self.table_name == other.table_name &&
63
+ self.ref_table == other.ref_table &&
64
+ self.ref_columns == other.ref_columns
65
+ end
66
+ end
67
+
68
+ end