dm-migrations 0.10.2 → 1.0.0.rc1

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