dbdiff 0.1.0

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