miguel 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,242 @@
1
+ # Schema migrator.
2
+
3
+ require 'miguel/schema'
4
+ require 'miguel/dumper'
5
+
6
+ module Miguel
7
+
8
+ # Class for generating database migration from one schema to another.
9
+ class Migrator
10
+
11
+ private
12
+
13
+ # Separate items in before and after arrays into old items, same items and new items.
14
+ def separate( before, after )
15
+ # Note that we have to use ==, so we can't use & and - operators which use eql?.
16
+ same = after.select{ |x| before.find{ |y| x == y } }
17
+ [
18
+ before.reject{ |x| same.find{ |y| x == y } },
19
+ same,
20
+ after.reject{ |x| same.find{ |y| x == y } },
21
+ ]
22
+ end
23
+
24
+ # Convert foreign keys from given tables into [ table name, foreign key ] pairs for easier comparison.
25
+ def prepare_keys( tables )
26
+ result = []
27
+ for table in tables
28
+ for key in table.foreign_keys
29
+ result << [ table.name, key ]
30
+ end
31
+ end
32
+ result
33
+ end
34
+
35
+ # Convert [ table name, foreign key ] pairs into hash of foreign keys per table.
36
+ def split_keys( table_keys )
37
+ result = {}
38
+ for name, key in table_keys
39
+ ( result[ name ] ||= [] ) << key
40
+ end
41
+ result
42
+ end
43
+
44
+ # Generate code for adding given foreign keys.
45
+ def dump_add_foreign_keys( out, table_keys )
46
+ for name, keys in split_keys( table_keys )
47
+ out.dump "alter_table #{name.inspect}" do
48
+ for key in keys
49
+ out << "add_foreign_key #{key.out_columns}, #{key.out_table_name}#{key.out_canonic_opts}"
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # Generate code for dropping given foreign keys.
56
+ def dump_drop_foreign_keys( out, table_keys )
57
+ for name, keys in split_keys( table_keys )
58
+ out.dump "alter_table #{name.inspect}" do
59
+ for key in keys
60
+ out << "drop_foreign_key #{key.out_columns} # #{key.out_table_name}#{key.out_canonic_opts}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Generate code for adding given tables.
67
+ def dump_add_tables( out, tables )
68
+ for table in tables
69
+ out.dump "create_table #{table.out_name}" do
70
+ for column in table.columns
71
+ column.dump( out )
72
+ end
73
+ for index in table.indexes
74
+ index.dump( out )
75
+ end
76
+ # No foreign keys here - those are added in a separate pass later.
77
+ end
78
+ end
79
+ end
80
+
81
+ # Generate code for dropping given tables.
82
+ def dump_drop_tables( out, tables )
83
+ for table in tables
84
+ out << "drop_table #{table.out_name}"
85
+ end
86
+ end
87
+
88
+ # Generate code for dropping given indexes.
89
+ def dump_drop_indexes( out, indexes )
90
+ for index in indexes
91
+ out << "drop_index #{index.out_columns}#{index.out_canonic_opts(' # ')}"
92
+ end
93
+ end
94
+
95
+ # Generate code for adding given indexes.
96
+ def dump_add_indexes( out, indexes )
97
+ for index in indexes
98
+ out << "add_index #{index.out_columns}#{index.out_canonic_opts}"
99
+ end
100
+ end
101
+
102
+ # Generate code for dropping given columns.
103
+ def dump_drop_columns( out, columns )
104
+ for column in columns
105
+ if column.type == :primary_key
106
+ out << "drop_constraint #{column.out_name}, :type => :primary_key#{column.out_opts(' # ')}"
107
+ else
108
+ out << "drop_column #{column.out_name} # #{column.out_type}#{column.out_opts}"
109
+ end
110
+ end
111
+ end
112
+
113
+ # Generate code for adding given columns.
114
+ def dump_add_columns( out, columns )
115
+ for column in columns
116
+ if column.type == :primary_key
117
+ out << "add_primary_key #{column.out_name}#{column.out_opts}"
118
+ else
119
+ out << "add_column #{column.out_name}, #{column.out_type}#{column.out_opts}"
120
+ end
121
+ end
122
+ end
123
+
124
+ # Generate code for altering given column.
125
+ def dump_alter_column( out, from, to )
126
+ if from.allow_null != to.allow_null && to.allow_null
127
+ out << "set_column_allow_null #{to.out_name}"
128
+ end
129
+ if from.type_opts != to.type_opts
130
+ out << "set_column_type #{to.out_name}, #{to.out_type}#{to.out_opts}"
131
+ end
132
+ if from.default != to.default
133
+ out << "set_column_default #{to.out_name}, #{to.out_default}"
134
+ end
135
+ if from.allow_null != to.allow_null && ! to.allow_null
136
+ out << "set_column_not_null #{to.out_name}"
137
+ end
138
+ end
139
+
140
+ # Generate code for altering given columns.
141
+ def dump_alter_columns( out, from_columns, to_columns )
142
+ pairs = from_columns.zip( to_columns )
143
+ for from, to in pairs
144
+ fail "invalid column pair #{from.name} -> #{to.name}" unless from.name == to.name
145
+ dump_alter_column( out, from, to )
146
+ end
147
+ end
148
+
149
+ # Generate code for altering given table.
150
+ def dump_alter_table( out, from, to )
151
+ old_indexes, same_indexes, new_indexes = separate( from.indexes, to.indexes )
152
+
153
+ from_names = from.column_names
154
+ to_names = to.column_names
155
+
156
+ old_names, same_names, new_names = separate( from_names, to_names )
157
+
158
+ old_columns = from.named_columns( old_names )
159
+ new_columns = to.named_columns( new_names )
160
+
161
+ from_columns = from.named_columns( same_names )
162
+ to_columns = to.named_columns( same_names )
163
+
164
+ from_columns, same_columns, to_columns = separate( from_columns, to_columns )
165
+
166
+ return if [ old_indexes, new_indexes, old_columns, new_columns, to_columns ].all?{ |x| x.empty? }
167
+
168
+ out.dump "alter_table #{to.out_name}" do
169
+ dump_drop_indexes( out, old_indexes )
170
+ dump_drop_columns( out, old_columns )
171
+ dump_alter_columns( out, from_columns, to_columns )
172
+ dump_add_columns( out, new_columns )
173
+ dump_add_indexes( out, new_indexes )
174
+ end
175
+ end
176
+
177
+ # Generate code for altering given tables.
178
+ def dump_alter_tables( out, from_tables, to_tables )
179
+ pairs = from_tables.zip( to_tables )
180
+ for from, to in pairs
181
+ fail "invalid table pair #{from.name} -> #{to.name}" unless from.name == to.name
182
+ dump_alter_table( out, from, to )
183
+ end
184
+ end
185
+
186
+ public
187
+
188
+ # Generate code for changing one schema to another.
189
+ def changes( from, to, out = Dumper.new )
190
+ from_keys = prepare_keys( from.tables )
191
+ to_keys = prepare_keys( to.tables )
192
+
193
+ old_keys, same_keys, new_keys = separate( from_keys, to_keys )
194
+
195
+ from_names = from.table_names
196
+ to_names = to.table_names
197
+
198
+ old_names, same_names, new_names = separate( from_names, to_names )
199
+
200
+ old_tables = from.named_tables( old_names )
201
+ new_tables = to.named_tables( new_names )
202
+
203
+ from_tables = from.named_tables( same_names )
204
+ to_tables = to.named_tables( same_names )
205
+
206
+ dump_drop_foreign_keys( out, old_keys )
207
+ dump_drop_tables( out, old_tables )
208
+ dump_alter_tables( out, from_tables, to_tables )
209
+ dump_add_tables( out, new_tables )
210
+ dump_add_foreign_keys( out, new_keys )
211
+
212
+ out
213
+ end
214
+
215
+ # Generate one way Sequel migration.
216
+ def change_migration( from, to, out = Dumper.new )
217
+ out.dump "Sequel.migration" do
218
+ out.dump "change" do
219
+ changes( from, to, out )
220
+ end
221
+ end
222
+ end
223
+
224
+ # Generate both ways Sequel migration.
225
+ def full_migration( from, to, out = Dumper.new )
226
+ out.dump "Sequel.migration" do
227
+ out.dump "up" do
228
+ changes( from, to, out )
229
+ end
230
+ out.dump "down" do
231
+ changes( to, from, out )
232
+ end
233
+ end
234
+ end
235
+
236
+ alias migration full_migration
237
+
238
+ end
239
+
240
+ end
241
+
242
+ # EOF #