dm-hibernate-migrations 1.0.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 (40) hide show
  1. data/lib/dm-migrations.rb +3 -0
  2. data/lib/dm-migrations/adapters/dm-do-adapter.rb +284 -0
  3. data/lib/dm-migrations/adapters/dm-mysql-adapter.rb +283 -0
  4. data/lib/dm-migrations/adapters/dm-oracle-adapter.rb +321 -0
  5. data/lib/dm-migrations/adapters/dm-postgres-adapter.rb +159 -0
  6. data/lib/dm-migrations/adapters/dm-sqlite-adapter.rb +96 -0
  7. data/lib/dm-migrations/adapters/dm-sqlserver-adapter.rb +177 -0
  8. data/lib/dm-migrations/adapters/dm-yaml-adapter.rb +23 -0
  9. data/lib/dm-migrations/auto_migration.rb +237 -0
  10. data/lib/dm-migrations/migration.rb +217 -0
  11. data/lib/dm-migrations/migration_runner.rb +85 -0
  12. data/lib/dm-migrations/sql.rb +5 -0
  13. data/lib/dm-migrations/sql/column.rb +5 -0
  14. data/lib/dm-migrations/sql/mysql.rb +53 -0
  15. data/lib/dm-migrations/sql/postgres.rb +78 -0
  16. data/lib/dm-migrations/sql/sqlite.rb +45 -0
  17. data/lib/dm-migrations/sql/table.rb +15 -0
  18. data/lib/dm-migrations/sql/table_creator.rb +102 -0
  19. data/lib/dm-migrations/sql/table_modifier.rb +51 -0
  20. data/lib/spec/example/migration_example_group.rb +73 -0
  21. data/lib/spec/matchers/migration_matchers.rb +106 -0
  22. data/spec/integration/auto_migration_spec.rb +506 -0
  23. data/spec/integration/migration_runner_spec.rb +89 -0
  24. data/spec/integration/migration_spec.rb +138 -0
  25. data/spec/integration/sql_spec.rb +190 -0
  26. data/spec/isolated/require_after_setup_spec.rb +30 -0
  27. data/spec/isolated/require_before_setup_spec.rb +30 -0
  28. data/spec/isolated/require_spec.rb +25 -0
  29. data/spec/rcov.opts +6 -0
  30. data/spec/spec.opts +4 -0
  31. data/spec/spec_helper.rb +16 -0
  32. data/spec/unit/migration_spec.rb +453 -0
  33. data/spec/unit/sql/column_spec.rb +14 -0
  34. data/spec/unit/sql/postgres_spec.rb +97 -0
  35. data/spec/unit/sql/sqlite_extensions_spec.rb +108 -0
  36. data/spec/unit/sql/table_creator_spec.rb +94 -0
  37. data/spec/unit/sql/table_modifier_spec.rb +49 -0
  38. data/spec/unit/sql/table_spec.rb +28 -0
  39. data/spec/unit/sql_spec.rb +7 -0
  40. metadata +157 -0
@@ -0,0 +1,3 @@
1
+ require 'dm-core'
2
+ require 'dm-migrations/migration'
3
+ require 'dm-migrations/auto_migration'
@@ -0,0 +1,284 @@
1
+ require 'dm-migrations/auto_migration'
2
+
3
+ module DataMapper
4
+ module Migrations
5
+
6
+ module DataObjectsAdapter
7
+
8
+ def dialect
9
+ self.class.to_s.sub(/.*::/, '').sub(/Adapter$/,'')
10
+ end
11
+
12
+ # Returns whether the storage_name exists.
13
+ #
14
+ # @param [String] storage_name
15
+ # a String defining the name of a storage, for example a table name.
16
+ #
17
+ # @return [Boolean]
18
+ # true if the storage exists
19
+ #
20
+ # @api semipublic
21
+ def storage_exists?(storage_name)
22
+ statement = <<-SQL.compress_lines
23
+ SELECT COUNT(*)
24
+ FROM "information_schema"."tables"
25
+ WHERE "table_type" = 'BASE TABLE'
26
+ AND "table_schema" = ?
27
+ AND "table_name" = ?
28
+ SQL
29
+
30
+ select(statement, schema_name, storage_name).first > 0
31
+ end
32
+
33
+ # Returns whether the field exists.
34
+ #
35
+ # @param [String] storage_name
36
+ # a String defining the name of a storage, for example a table name.
37
+ # @param [String] field
38
+ # a String defining the name of a field, for example a column name.
39
+ #
40
+ # @return [Boolean]
41
+ # true if the field exists.
42
+ #
43
+ # @api semipublic
44
+ def field_exists?(storage_name, column_name)
45
+ statement = <<-SQL.compress_lines
46
+ SELECT COUNT(*)
47
+ FROM "information_schema"."columns"
48
+ WHERE "table_schema" = ?
49
+ AND "table_name" = ?
50
+ AND "column_name" = ?
51
+ SQL
52
+
53
+ select(statement, schema_name, storage_name, column_name).first > 0
54
+ end
55
+
56
+ # @api semipublic
57
+ def upgrade_model_storage(model)
58
+ name = self.name
59
+ properties = model.properties_with_subclasses(name)
60
+
61
+ if success = create_model_storage(model)
62
+ return properties
63
+ end
64
+
65
+ table_name = model.storage_name(name)
66
+
67
+ with_connection do |connection|
68
+ properties.map do |property|
69
+ schema_hash = property_schema_hash(property)
70
+ next if field_exists?(table_name, schema_hash[:name])
71
+
72
+ statement = alter_table_add_column_statement(connection, table_name, schema_hash)
73
+ command = connection.create_command(statement)
74
+ command.execute_non_query
75
+
76
+ property
77
+ end.compact
78
+ end
79
+ end
80
+
81
+ # @api semipublic
82
+ def create_model_storage(model)
83
+ name = self.name
84
+ properties = model.properties_with_subclasses(name)
85
+
86
+ return false if storage_exists?(model.storage_name(name))
87
+ return false if properties.empty?
88
+
89
+ with_connection do |connection|
90
+ statements = [ create_table_statement(connection, model, properties) ]
91
+ statements.concat(create_index_statements(model))
92
+ statements.concat(create_unique_index_statements(model))
93
+
94
+ statements.each do |statement|
95
+ command = connection.create_command(statement)
96
+ command.execute_non_query
97
+ end
98
+ end
99
+
100
+ true
101
+ end
102
+
103
+ # @api semipublic
104
+ def destroy_model_storage(model)
105
+ return true unless supports_drop_table_if_exists? || storage_exists?(model.storage_name(name))
106
+ execute(drop_table_statement(model))
107
+ true
108
+ end
109
+
110
+ module SQL #:nodoc:
111
+ # private ## This cannot be private for current migrations
112
+
113
+ # Adapters that support AUTO INCREMENT fields for CREATE TABLE
114
+ # statements should overwrite this to return true
115
+ #
116
+ # @api private
117
+ def supports_serial?
118
+ false
119
+ end
120
+
121
+ # @api private
122
+ def supports_drop_table_if_exists?
123
+ false
124
+ end
125
+
126
+ # @api private
127
+ def schema_name
128
+ raise NotImplementedError, "#{self.class}#schema_name not implemented"
129
+ end
130
+
131
+ # @api private
132
+ def alter_table_add_column_statement(connection, table_name, schema_hash)
133
+ "ALTER TABLE #{quote_name(table_name)} ADD COLUMN #{property_schema_statement(connection, schema_hash)}"
134
+ end
135
+
136
+ # @api private
137
+ def create_table_statement(connection, model, properties)
138
+ statement = <<-SQL.compress_lines
139
+ CREATE TABLE #{quote_name(model.storage_name(name))}
140
+ (#{properties.map { |property| property_schema_statement(connection, property_schema_hash(property)) }.join(', ')},
141
+ PRIMARY KEY(#{ properties.key.map { |property| quote_name(property.field) }.join(', ')}))
142
+ SQL
143
+
144
+ statement
145
+ end
146
+
147
+ # @api private
148
+ def drop_table_statement(model)
149
+ table_name = quote_name(model.storage_name(name))
150
+ if supports_drop_table_if_exists?
151
+ "DROP TABLE IF EXISTS #{table_name}"
152
+ else
153
+ "DROP TABLE #{table_name}"
154
+ end
155
+ end
156
+
157
+ # @api private
158
+ def create_index_statements(model)
159
+ name = self.name
160
+ table_name = model.storage_name(name)
161
+
162
+ indexes(model).map do |index_name, fields|
163
+ <<-SQL.compress_lines
164
+ CREATE INDEX #{quote_name("index_#{table_name}_#{index_name}")} ON
165
+ #{quote_name(table_name)} (#{fields.map { |field| quote_name(field) }.join(', ')})
166
+ SQL
167
+ end
168
+ end
169
+
170
+ # @api private
171
+ def create_unique_index_statements(model)
172
+ name = self.name
173
+ table_name = model.storage_name(name)
174
+ key = model.key(name).map { |property| property.field }
175
+ unique_indexes = unique_indexes(model).reject { |index_name, fields| fields == key }
176
+
177
+ unique_indexes.map do |index_name, fields|
178
+ <<-SQL.compress_lines
179
+ CREATE UNIQUE INDEX #{quote_name("unique_#{table_name}_#{index_name}")} ON
180
+ #{quote_name(table_name)} (#{fields.map { |field| quote_name(field) }.join(', ')})
181
+ SQL
182
+ end
183
+ end
184
+
185
+ # @api private
186
+ def property_schema_hash(property)
187
+ primitive = property.primitive
188
+ type = property.type
189
+ type_map = self.class.type_map
190
+
191
+ schema = (type_map[property.class] || type_map[primitive] || type_map[type]).merge(:name => property.field)
192
+
193
+ schema_primitive = schema[:primitive]
194
+
195
+ if primitive == String && schema_primitive != 'TEXT' && schema_primitive != 'CLOB' && schema_primitive != 'NVARCHAR'
196
+ schema[:length] = property.length
197
+ elsif primitive == BigDecimal || primitive == Float
198
+ schema[:precision] = property.precision
199
+ schema[:scale] = property.scale
200
+ end
201
+
202
+ schema[:allow_nil] = property.allow_nil?
203
+ schema[:serial] = property.serial?
204
+
205
+ default = property.default
206
+
207
+ if default.nil? || default.respond_to?(:call)
208
+ # remove the default if the property does not allow nil
209
+ schema.delete(:default) unless schema[:allow_nil]
210
+ else
211
+ schema[:default] = if type.respond_to?(:dump)
212
+ type.dump(default, property)
213
+ else
214
+ default
215
+ end
216
+ end
217
+
218
+ schema
219
+ end
220
+
221
+ # @api private
222
+ def property_schema_statement(connection, schema)
223
+ statement = quote_name(schema[:name])
224
+ statement << " #{schema[:primitive]}"
225
+
226
+ length = schema[:length]
227
+
228
+ if schema[:precision] && schema[:scale]
229
+ statement << "(#{[ :precision, :scale ].map { |key| connection.quote_value(schema[key]) }.join(', ')})"
230
+ elsif length == 'max'
231
+ statement << '(max)'
232
+ elsif length
233
+ statement << "(#{connection.quote_value(length)})"
234
+ end
235
+
236
+ statement << " DEFAULT #{connection.quote_value(schema[:default])}" if schema.key?(:default)
237
+ statement << ' NOT NULL' unless schema[:allow_nil]
238
+ statement
239
+ end
240
+
241
+ # @api private
242
+ def indexes(model)
243
+ model.properties(name).indexes
244
+ end
245
+
246
+ # @api private
247
+ def unique_indexes(model)
248
+ model.properties(name).unique_indexes
249
+ end
250
+ end # module SQL
251
+
252
+ include SQL
253
+
254
+ module ClassMethods
255
+ # Default types for all data object based adapters.
256
+ #
257
+ # @return [Hash] default types for data objects adapters.
258
+ #
259
+ # @api private
260
+ def type_map
261
+ length = Property::String::DEFAULT_LENGTH
262
+ precision = Property::Numeric::DEFAULT_PRECISION
263
+ scale = Property::Decimal::DEFAULT_SCALE
264
+
265
+ @type_map ||= {
266
+ Property::Binary => { :primitive => 'BLOB' },
267
+ Object => { :primitive => 'TEXT' },
268
+ Integer => { :primitive => 'INTEGER' },
269
+ String => { :primitive => 'VARCHAR', :length => length },
270
+ Class => { :primitive => 'VARCHAR', :length => length },
271
+ BigDecimal => { :primitive => 'DECIMAL', :precision => precision, :scale => scale },
272
+ Float => { :primitive => 'FLOAT', :precision => precision },
273
+ DateTime => { :primitive => 'TIMESTAMP' },
274
+ Date => { :primitive => 'DATE' },
275
+ Time => { :primitive => 'TIMESTAMP' },
276
+ TrueClass => { :primitive => 'BOOLEAN' },
277
+ Property::Text => { :primitive => 'TEXT' },
278
+ }.freeze
279
+ end
280
+ end
281
+ end
282
+
283
+ end
284
+ end
@@ -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