dm-migrations 0.10.2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/.gitignore +36 -0
  2. data/Gemfile +141 -0
  3. data/Rakefile +2 -3
  4. data/VERSION +1 -1
  5. data/dm-migrations.gemspec +50 -18
  6. data/lib/dm-migrations.rb +2 -0
  7. data/lib/dm-migrations/adapters/dm-do-adapter.rb +276 -0
  8. data/lib/dm-migrations/adapters/dm-mysql-adapter.rb +283 -0
  9. data/lib/dm-migrations/adapters/dm-oracle-adapter.rb +321 -0
  10. data/lib/dm-migrations/adapters/dm-postgres-adapter.rb +159 -0
  11. data/lib/dm-migrations/adapters/dm-sqlite-adapter.rb +96 -0
  12. data/lib/dm-migrations/adapters/dm-sqlserver-adapter.rb +177 -0
  13. data/lib/dm-migrations/adapters/dm-yaml-adapter.rb +23 -0
  14. data/lib/dm-migrations/auto_migration.rb +238 -0
  15. data/lib/dm-migrations/migration.rb +3 -3
  16. data/lib/dm-migrations/sql.rb +2 -2
  17. data/lib/dm-migrations/sql/mysql.rb +3 -3
  18. data/lib/dm-migrations/sql/{postgresql.rb → postgres.rb} +3 -3
  19. data/lib/dm-migrations/sql/{sqlite3.rb → sqlite.rb} +3 -3
  20. data/spec/integration/auto_migration_spec.rb +506 -0
  21. data/spec/integration/migration_runner_spec.rb +12 -2
  22. data/spec/integration/migration_spec.rb +28 -14
  23. data/spec/integration/sql_spec.rb +22 -21
  24. data/spec/isolated/require_after_setup_spec.rb +30 -0
  25. data/spec/isolated/require_before_setup_spec.rb +30 -0
  26. data/spec/isolated/require_spec.rb +25 -0
  27. data/spec/spec_helper.rb +10 -25
  28. data/spec/unit/migration_spec.rb +320 -319
  29. data/spec/unit/sql/{postgresql_spec.rb → postgres_spec.rb} +17 -17
  30. data/spec/unit/sql/{sqlite3_extensions_spec.rb → sqlite_extensions_spec.rb} +14 -14
  31. data/tasks/local_gemfile.rake +18 -0
  32. data/tasks/spec.rake +0 -3
  33. metadata +72 -32
@@ -0,0 +1,283 @@
1
+ require 'dm-migrations/auto_migration'
2
+ require 'dm-migrations/adapters/dm-do-adapter'
3
+
4
+ module DataMapper
5
+ module Migrations
6
+ module MysqlAdapter
7
+
8
+ DEFAULT_ENGINE = 'InnoDB'.freeze
9
+ DEFAULT_CHARACTER_SET = 'utf8'.freeze
10
+ DEFAULT_COLLATION = 'utf8_unicode_ci'.freeze
11
+
12
+ include DataObjectsAdapter
13
+
14
+ # @api private
15
+ def self.included(base)
16
+ base.extend DataObjectsAdapter::ClassMethods
17
+ base.extend ClassMethods
18
+ end
19
+
20
+ # @api semipublic
21
+ def storage_exists?(storage_name)
22
+ select('SHOW TABLES LIKE ?', storage_name).first == storage_name
23
+ end
24
+
25
+ # @api semipublic
26
+ def field_exists?(storage_name, field)
27
+ result = select("SHOW COLUMNS FROM #{quote_name(storage_name)} LIKE ?", field).first
28
+ result ? result.field == field : false
29
+ end
30
+
31
+ module SQL #:nodoc:
32
+ # private ## This cannot be private for current migrations
33
+
34
+ # @api private
35
+ def supports_serial?
36
+ true
37
+ end
38
+
39
+ # @api private
40
+ def supports_drop_table_if_exists?
41
+ true
42
+ end
43
+
44
+ # @api private
45
+ def schema_name
46
+ # TODO: is there a cleaner way to find out the current DB we are connected to?
47
+ normalized_uri.path.split('/').last
48
+ end
49
+
50
+ # @api private
51
+ def create_table_statement(connection, model, properties)
52
+ "#{super} ENGINE = #{DEFAULT_ENGINE} CHARACTER SET #{character_set} COLLATE #{collation}"
53
+ end
54
+
55
+ # @api private
56
+ def property_schema_hash(property)
57
+ schema = super
58
+
59
+ if property.kind_of?(Property::Text)
60
+ schema[:primitive] = text_column_statement(property.length)
61
+ schema.delete(:default)
62
+ end
63
+
64
+ if property.kind_of?(Property::Integer)
65
+ min = property.min
66
+ max = property.max
67
+
68
+ schema[:primitive] = integer_column_statement(min..max) if min && max
69
+ end
70
+
71
+ schema
72
+ end
73
+
74
+ # @api private
75
+ def property_schema_statement(connection, schema)
76
+ statement = super
77
+
78
+ if supports_serial? && schema[:serial]
79
+ statement << ' AUTO_INCREMENT'
80
+ end
81
+
82
+ statement
83
+ end
84
+
85
+ # @api private
86
+ def character_set
87
+ @character_set ||= show_variable('character_set_connection') || DEFAULT_CHARACTER_SET
88
+ end
89
+
90
+ # @api private
91
+ def collation
92
+ @collation ||= show_variable('collation_connection') || DEFAULT_COLLATION
93
+ end
94
+
95
+ # @api private
96
+ def show_variable(name)
97
+ result = select('SHOW VARIABLES LIKE ?', name).first
98
+ result ? result.value.freeze : nil
99
+ end
100
+
101
+ private
102
+
103
+ # Return SQL statement for the text column
104
+ #
105
+ # @param [Integer] length
106
+ # the max allowed length
107
+ #
108
+ # @return [String]
109
+ # the statement to create the text column
110
+ #
111
+ # @api private
112
+ def text_column_statement(length)
113
+ if length < 2**8 then 'TINYTEXT'
114
+ elsif length < 2**16 then 'TEXT'
115
+ elsif length < 2**24 then 'MEDIUMTEXT'
116
+ elsif length < 2**32 then 'LONGTEXT'
117
+
118
+ # http://www.postgresql.org/files/documentation/books/aw_pgsql/node90.html
119
+ # Implies that PostgreSQL doesn't have a size limit on text
120
+ # fields, so this param validation happens here instead of
121
+ # DM::Property#initialize.
122
+ else
123
+ raise ArgumentError, "length of #{length} exceeds maximum size supported"
124
+ end
125
+ end
126
+
127
+ # Return SQL statement for the integer column
128
+ #
129
+ # @param [Range] range
130
+ # the min/max allowed integers
131
+ #
132
+ # @return [String]
133
+ # the statement to create the integer column
134
+ #
135
+ # @api private
136
+ def integer_column_statement(range)
137
+ '%s(%d)%s' % [
138
+ integer_column_type(range),
139
+ integer_display_size(range),
140
+ integer_statement_sign(range),
141
+ ]
142
+ end
143
+
144
+ # Return the integer column type
145
+ #
146
+ # Use the smallest available column type that will satisfy the
147
+ # allowable range of numbers
148
+ #
149
+ # @param [Range] range
150
+ # the min/max allowed integers
151
+ #
152
+ # @return [String]
153
+ # the column type
154
+ #
155
+ # @api private
156
+ def integer_column_type(range)
157
+ if range.first < 0
158
+ signed_integer_column_type(range)
159
+ else
160
+ unsigned_integer_column_type(range)
161
+ end
162
+ end
163
+
164
+ # Return the signed integer column type
165
+ #
166
+ # @param [Range] range
167
+ # the min/max allowed integers
168
+ #
169
+ # @return [String]
170
+ #
171
+ # @api private
172
+ def signed_integer_column_type(range)
173
+ min = range.first
174
+ max = range.last
175
+
176
+ tinyint = 2**7
177
+ smallint = 2**15
178
+ integer = 2**31
179
+ mediumint = 2**23
180
+ bigint = 2**63
181
+
182
+ if min >= -tinyint && max < tinyint then 'TINYINT'
183
+ elsif min >= -smallint && max < smallint then 'SMALLINT'
184
+ elsif min >= -mediumint && max < mediumint then 'MEDIUMINT'
185
+ elsif min >= -integer && max < integer then 'INT'
186
+ elsif min >= -bigint && max < bigint then 'BIGINT'
187
+ else
188
+ raise ArgumentError, "min #{min} and max #{max} exceeds supported range"
189
+ end
190
+ end
191
+
192
+ # Return the unsigned integer column type
193
+ #
194
+ # @param [Range] range
195
+ # the min/max allowed integers
196
+ #
197
+ # @return [String]
198
+ #
199
+ # @api private
200
+ def unsigned_integer_column_type(range)
201
+ max = range.last
202
+
203
+ if max < 2**8 then 'TINYINT'
204
+ elsif max < 2**16 then 'SMALLINT'
205
+ elsif max < 2**24 then 'MEDIUMINT'
206
+ elsif max < 2**32 then 'INT'
207
+ elsif max < 2**64 then 'BIGINT'
208
+ else
209
+ raise ArgumentError, "min #{range.first} and max #{max} exceeds supported range"
210
+ end
211
+ end
212
+
213
+ # Return the integer column display size
214
+ #
215
+ # Adjust the display size to match the maximum number of
216
+ # expected digits. This is more for documentation purposes
217
+ # and does not affect what can actually be stored in a
218
+ # specific column
219
+ #
220
+ # @param [Range] range
221
+ # the min/max allowed integers
222
+ #
223
+ # @return [Integer]
224
+ # the display size for the integer
225
+ #
226
+ # @api private
227
+ def integer_display_size(range)
228
+ [ range.first.to_s.length, range.last.to_s.length ].max
229
+ end
230
+
231
+ # Return the integer sign statement
232
+ #
233
+ # @param [Range] range
234
+ # the min/max allowed integers
235
+ #
236
+ # @return [String, nil]
237
+ # statement if unsigned, nil if signed
238
+ #
239
+ # @api private
240
+ def integer_statement_sign(range)
241
+ ' UNSIGNED' unless range.first < 0
242
+ end
243
+
244
+ # @api private
245
+ def indexes(model)
246
+ filter_indexes(model, super)
247
+ end
248
+
249
+ # @api private
250
+ def unique_indexes(model)
251
+ filter_indexes(model, super)
252
+ end
253
+
254
+ # Filter out any indexes with an unindexable column in MySQL
255
+ #
256
+ # @api private
257
+ def filter_indexes(model, indexes)
258
+ field_map = model.properties(name).field_map
259
+ indexes.select do |index_name, fields|
260
+ fields.all? { |field| !field_map[field].kind_of?(Property::Text) }
261
+ end
262
+ end
263
+ end # module SQL
264
+
265
+ include SQL
266
+
267
+ module ClassMethods
268
+ # Types for MySQL databases.
269
+ #
270
+ # @return [Hash] types for MySQL databases.
271
+ #
272
+ # @api private
273
+ def type_map
274
+ @type_map ||= super.merge(
275
+ DateTime => { :primitive => 'DATETIME' },
276
+ Time => { :primitive => 'DATETIME' }
277
+ ).freeze
278
+ end
279
+ end
280
+
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,321 @@
1
+ require 'dm-migrations/auto_migration'
2
+ require 'dm-migrations/adapters/dm-do-adapter'
3
+
4
+ module DataMapper
5
+ module Migrations
6
+ module OracleAdapter
7
+
8
+ include DataObjectsAdapter
9
+
10
+ # @api private
11
+ def self.included(base)
12
+ base.extend DataObjectsAdapter::ClassMethods
13
+ base.extend ClassMethods
14
+ end
15
+
16
+ # @api semipublic
17
+ def storage_exists?(storage_name)
18
+ statement = <<-SQL.compress_lines
19
+ SELECT COUNT(*)
20
+ FROM all_tables
21
+ WHERE owner = ?
22
+ AND table_name = ?
23
+ SQL
24
+
25
+ select(statement, schema_name, oracle_upcase(storage_name)).first > 0
26
+ end
27
+
28
+ # @api semipublic
29
+ def sequence_exists?(sequence_name)
30
+ return false unless sequence_name
31
+ statement = <<-SQL.compress_lines
32
+ SELECT COUNT(*)
33
+ FROM all_sequences
34
+ WHERE sequence_owner = ?
35
+ AND sequence_name = ?
36
+ SQL
37
+
38
+ select(statement, schema_name, oracle_upcase(sequence_name)).first > 0
39
+ end
40
+
41
+ # @api semipublic
42
+ def field_exists?(storage_name, field_name)
43
+ statement = <<-SQL.compress_lines
44
+ SELECT COUNT(*)
45
+ FROM all_tab_columns
46
+ WHERE owner = ?
47
+ AND table_name = ?
48
+ AND column_name = ?
49
+ SQL
50
+
51
+ select(statement, schema_name, oracle_upcase(storage_name), oracle_upcase(field_name)).first > 0
52
+ end
53
+
54
+ # @api semipublic
55
+ def storage_fields(storage_name)
56
+ statement = <<-SQL.compress_lines
57
+ SELECT column_name
58
+ FROM all_tab_columns
59
+ WHERE owner = ?
60
+ AND table_name = ?
61
+ SQL
62
+
63
+ select(statement, schema_name, oracle_upcase(storage_name))
64
+ end
65
+
66
+ # @api semipublic
67
+ def create_model_storage(model)
68
+ name = self.name
69
+ properties = model.properties_with_subclasses(name)
70
+ table_name = model.storage_name(name)
71
+ truncate_or_delete = self.class.auto_migrate_with
72
+ table_is_truncated = truncate_or_delete && @truncated_tables && @truncated_tables[table_name]
73
+
74
+ return false if storage_exists?(table_name) && !table_is_truncated
75
+ return false if properties.empty?
76
+
77
+ with_connection do |connection|
78
+ # if table was truncated then check if all columns for properties are present
79
+ # TODO: check all other column definition options
80
+ if table_is_truncated && storage_has_all_fields?(table_name, properties)
81
+ @truncated_tables[table_name] = nil
82
+ else
83
+ # forced drop of table if properties are different
84
+ if truncate_or_delete
85
+ destroy_model_storage(model, true)
86
+ end
87
+
88
+ statements = [ create_table_statement(connection, model, properties) ]
89
+ statements.concat(create_index_statements(model))
90
+ statements.concat(create_unique_index_statements(model))
91
+ statements.concat(create_sequence_statements(model))
92
+
93
+ statements.each do |statement|
94
+ command = connection.create_command(statement)
95
+ command.execute_non_query
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ true
102
+ end
103
+
104
+ # @api semipublic
105
+ def destroy_model_storage(model, forced = false)
106
+ table_name = model.storage_name(name)
107
+ klass = self.class
108
+ truncate_or_delete = klass.auto_migrate_with
109
+ if storage_exists?(table_name)
110
+ if truncate_or_delete && !forced
111
+ case truncate_or_delete
112
+ when :truncate
113
+ execute(truncate_table_statement(model))
114
+ when :delete
115
+ execute(delete_table_statement(model))
116
+ else
117
+ raise ArgumentError, "Unsupported auto_migrate_with option"
118
+ end
119
+ @truncated_tables ||= {}
120
+ @truncated_tables[table_name] = true
121
+ else
122
+ execute(drop_table_statement(model))
123
+ @truncated_tables[table_name] = nil if @truncated_tables
124
+ end
125
+ end
126
+ # added destroy of sequences
127
+ reset_sequences = klass.auto_migrate_reset_sequences
128
+ table_is_truncated = @truncated_tables && @truncated_tables[table_name]
129
+ unless truncate_or_delete && !reset_sequences && !forced
130
+ if sequence_exists?(model_sequence_name(model))
131
+ statement = if table_is_truncated && !forced
132
+ reset_sequence_statement(model)
133
+ else
134
+ drop_sequence_statement(model)
135
+ end
136
+ execute(statement) if statement
137
+ end
138
+ end
139
+ true
140
+ end
141
+
142
+ private
143
+
144
+ def storage_has_all_fields?(table_name, properties)
145
+ properties.map { |property| oracle_upcase(property.field) }.sort == storage_fields(table_name).sort
146
+ end
147
+
148
+ # If table or column name contains just lowercase characters then do uppercase
149
+ # as uppercase version will be used in Oracle data dictionary tables
150
+ def oracle_upcase(name)
151
+ name =~ /[A-Z]/ ? name : name.upcase
152
+ end
153
+
154
+ module SQL #:nodoc:
155
+ # private ## This cannot be private for current migrations
156
+
157
+ # @api private
158
+ def schema_name
159
+ @schema_name ||= select("SELECT SYS_CONTEXT('userenv','current_schema') FROM dual").first.freeze
160
+ end
161
+
162
+ # @api private
163
+ def create_sequence_statements(model)
164
+ name = self.name
165
+ table_name = model.storage_name(name)
166
+ serial = model.serial(name)
167
+
168
+ statements = []
169
+ if sequence_name = model_sequence_name(model)
170
+ sequence_name = quote_name(sequence_name)
171
+ column_name = quote_name(serial.field)
172
+
173
+ statements << <<-SQL.compress_lines
174
+ CREATE SEQUENCE #{sequence_name} NOCACHE
175
+ SQL
176
+
177
+ # create trigger only if custom sequence name was not specified
178
+ unless serial.options[:sequence]
179
+ statements << <<-SQL.compress_lines
180
+ CREATE OR REPLACE TRIGGER #{quote_name(default_trigger_name(table_name))}
181
+ BEFORE INSERT ON #{quote_name(table_name)} FOR EACH ROW
182
+ BEGIN
183
+ IF inserting THEN
184
+ IF :new.#{column_name} IS NULL THEN
185
+ SELECT #{sequence_name}.NEXTVAL INTO :new.#{column_name} FROM dual;
186
+ END IF;
187
+ END IF;
188
+ END;
189
+ SQL
190
+ end
191
+ end
192
+
193
+ statements
194
+ end
195
+
196
+ # @api private
197
+ def drop_sequence_statement(model)
198
+ if sequence_name = model_sequence_name(model)
199
+ "DROP SEQUENCE #{quote_name(sequence_name)}"
200
+ else
201
+ nil
202
+ end
203
+ end
204
+
205
+ # @api private
206
+ def reset_sequence_statement(model)
207
+ if sequence_name = model_sequence_name(model)
208
+ sequence_name = quote_name(sequence_name)
209
+ <<-SQL.compress_lines
210
+ DECLARE
211
+ cval INTEGER;
212
+ BEGIN
213
+ SELECT #{sequence_name}.NEXTVAL INTO cval FROM dual;
214
+ EXECUTE IMMEDIATE 'ALTER SEQUENCE #{sequence_name} INCREMENT BY -' || cval || ' MINVALUE 0';
215
+ SELECT #{sequence_name}.NEXTVAL INTO cval FROM dual;
216
+ EXECUTE IMMEDIATE 'ALTER SEQUENCE #{sequence_name} INCREMENT BY 1';
217
+ END;
218
+ SQL
219
+ else
220
+ nil
221
+ end
222
+
223
+ end
224
+
225
+ # @api private
226
+ def truncate_table_statement(model)
227
+ "TRUNCATE TABLE #{quote_name(model.storage_name(name))}"
228
+ end
229
+
230
+ # @api private
231
+ def delete_table_statement(model)
232
+ "DELETE FROM #{quote_name(model.storage_name(name))}"
233
+ end
234
+
235
+ private
236
+
237
+ def model_sequence_name(model)
238
+ name = self.name
239
+ table_name = model.storage_name(name)
240
+ serial = model.serial(name)
241
+
242
+ if serial
243
+ serial.options[:sequence] || default_sequence_name(table_name)
244
+ else
245
+ nil
246
+ end
247
+ end
248
+
249
+ def default_sequence_name(table_name)
250
+ # truncate table name if necessary to fit in max length of identifier
251
+ "#{table_name[0,self.class::IDENTIFIER_MAX_LENGTH-4]}_seq"
252
+ end
253
+
254
+ def default_trigger_name(table_name)
255
+ # truncate table name if necessary to fit in max length of identifier
256
+ "#{table_name[0,self.class::IDENTIFIER_MAX_LENGTH-4]}_pkt"
257
+ end
258
+
259
+ end # module SQL
260
+
261
+ include SQL
262
+
263
+ module ClassMethods
264
+ # Types for Oracle databases.
265
+ #
266
+ # @return [Hash] types for Oracle databases.
267
+ #
268
+ # @api private
269
+ def type_map
270
+ length = Property::String::DEFAULT_LENGTH
271
+ precision = Property::Numeric::DEFAULT_PRECISION
272
+ scale = Property::Decimal::DEFAULT_SCALE
273
+
274
+ @type_map ||= {
275
+ Integer => { :primitive => 'NUMBER', :precision => precision, :scale => 0 },
276
+ String => { :primitive => 'VARCHAR2', :length => length },
277
+ Class => { :primitive => 'VARCHAR2', :length => length },
278
+ BigDecimal => { :primitive => 'NUMBER', :precision => precision, :scale => nil },
279
+ Float => { :primitive => 'BINARY_FLOAT', },
280
+ DateTime => { :primitive => 'DATE' },
281
+ Date => { :primitive => 'DATE' },
282
+ Time => { :primitive => 'DATE' },
283
+ TrueClass => { :primitive => 'NUMBER', :precision => 1, :scale => 0 },
284
+ Property::Text => { :primitive => 'CLOB' },
285
+ }.freeze
286
+ end
287
+
288
+ # Use table truncate or delete for auto_migrate! to speed up test execution
289
+ #
290
+ # @param [Symbol] :truncate, :delete or :drop_and_create (or nil)
291
+ # do not specify parameter to return current value
292
+ #
293
+ # @return [Symbol] current value of auto_migrate_with option (nil returned for :drop_and_create)
294
+ #
295
+ # @api semipublic
296
+ def auto_migrate_with(value = :not_specified)
297
+ return @auto_migrate_with if value == :not_specified
298
+ value = nil if value == :drop_and_create
299
+ raise ArgumentError unless [nil, :truncate, :delete].include?(value)
300
+ @auto_migrate_with = value
301
+ end
302
+
303
+ # Set if sequences will or will not be reset during auto_migrate!
304
+ #
305
+ # @param [TrueClass, FalseClass] reset sequences?
306
+ # do not specify parameter to return current value
307
+ #
308
+ # @return [Symbol] current value of auto_migrate_reset_sequences option (default value is true)
309
+ #
310
+ # @api semipublic
311
+ def auto_migrate_reset_sequences(value = :not_specified)
312
+ return @auto_migrate_reset_sequences.nil? ? true : @auto_migrate_reset_sequences if value == :not_specified
313
+ raise ArgumentError unless [true, false].include?(value)
314
+ @auto_migrate_reset_sequences = value
315
+ end
316
+
317
+ end
318
+
319
+ end
320
+ end
321
+ end