miguel 0.1.0.pre1

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,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 #