dm-hibernate-migrations 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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