ardm-migrations 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +35 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +53 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +39 -0
  7. data/Rakefile +4 -0
  8. data/ardm-migrations.gemspec +27 -0
  9. data/db/migrations/1_create_people_table.rb +12 -0
  10. data/db/migrations/2_add_dob_to_people.rb +13 -0
  11. data/db/migrations/config.rb +4 -0
  12. data/examples/Rakefile +144 -0
  13. data/examples/sample_migration.rb +58 -0
  14. data/examples/sample_migration_spec.rb +50 -0
  15. data/lib/ardm-migrations.rb +1 -0
  16. data/lib/dm-migrations/adapters/dm-do-adapter.rb +295 -0
  17. data/lib/dm-migrations/adapters/dm-mysql-adapter.rb +299 -0
  18. data/lib/dm-migrations/adapters/dm-oracle-adapter.rb +332 -0
  19. data/lib/dm-migrations/adapters/dm-postgres-adapter.rb +159 -0
  20. data/lib/dm-migrations/adapters/dm-sqlite-adapter.rb +96 -0
  21. data/lib/dm-migrations/adapters/dm-sqlserver-adapter.rb +177 -0
  22. data/lib/dm-migrations/adapters/dm-yaml-adapter.rb +23 -0
  23. data/lib/dm-migrations/auto_migration.rb +239 -0
  24. data/lib/dm-migrations/exceptions/duplicate_migration.rb +6 -0
  25. data/lib/dm-migrations/migration.rb +300 -0
  26. data/lib/dm-migrations/migration_runner.rb +85 -0
  27. data/lib/dm-migrations/sql/column.rb +5 -0
  28. data/lib/dm-migrations/sql/mysql.rb +61 -0
  29. data/lib/dm-migrations/sql/postgres.rb +82 -0
  30. data/lib/dm-migrations/sql/sqlite.rb +51 -0
  31. data/lib/dm-migrations/sql/table.rb +15 -0
  32. data/lib/dm-migrations/sql/table_creator.rb +109 -0
  33. data/lib/dm-migrations/sql/table_modifier.rb +57 -0
  34. data/lib/dm-migrations/sql.rb +5 -0
  35. data/lib/dm-migrations/version.rb +5 -0
  36. data/lib/dm-migrations.rb +3 -0
  37. data/lib/spec/example/migration_example_group.rb +73 -0
  38. data/lib/spec/matchers/migration_matchers.rb +106 -0
  39. data/spec/integration/auto_migration_spec.rb +553 -0
  40. data/spec/integration/auto_upgrade_spec.rb +40 -0
  41. data/spec/integration/migration_runner_spec.rb +89 -0
  42. data/spec/integration/migration_spec.rb +157 -0
  43. data/spec/integration/sql_spec.rb +250 -0
  44. data/spec/isolated/require_after_setup_spec.rb +30 -0
  45. data/spec/isolated/require_before_setup_spec.rb +30 -0
  46. data/spec/isolated/require_spec.rb +25 -0
  47. data/spec/rcov.opts +6 -0
  48. data/spec/spec.opts +4 -0
  49. data/spec/spec_helper.rb +18 -0
  50. data/spec/unit/migration_spec.rb +453 -0
  51. data/spec/unit/sql/column_spec.rb +14 -0
  52. data/spec/unit/sql/postgres_spec.rb +97 -0
  53. data/spec/unit/sql/sqlite_extensions_spec.rb +108 -0
  54. data/spec/unit/sql/table_creator_spec.rb +94 -0
  55. data/spec/unit/sql/table_modifier_spec.rb +49 -0
  56. data/spec/unit/sql/table_spec.rb +28 -0
  57. data/spec/unit/sql_spec.rb +7 -0
  58. data/tasks/spec.rake +38 -0
  59. data/tasks/yard.rake +9 -0
  60. data/tasks/yardstick.rake +19 -0
  61. metadata +150 -0
@@ -0,0 +1,332 @@
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 = DataMapper::Ext::String.compress_lines(<<-SQL)
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 = DataMapper::Ext::String.compress_lines(<<-SQL)
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 = DataMapper::Ext::String.compress_lines(<<-SQL)
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 = DataMapper::Ext::String.compress_lines(<<-SQL)
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
+ def drop_table_statement(model)
67
+ table_name = quote_name(model.storage_name(name))
68
+ "DROP TABLE #{table_name} CASCADE CONSTRAINTS"
69
+ end
70
+
71
+
72
+ # @api semipublic
73
+ def create_model_storage(model)
74
+ name = self.name
75
+ properties = model.properties_with_subclasses(name)
76
+ table_name = model.storage_name(name)
77
+ truncate_or_delete = self.class.auto_migrate_with
78
+ table_is_truncated = truncate_or_delete && @truncated_tables && @truncated_tables[table_name]
79
+
80
+ return false if storage_exists?(table_name) && !table_is_truncated
81
+ return false if properties.empty?
82
+
83
+ with_connection do |connection|
84
+ # if table was truncated then check if all columns for properties are present
85
+ # TODO: check all other column definition options
86
+ if table_is_truncated && storage_has_all_fields?(table_name, properties)
87
+ @truncated_tables[table_name] = nil
88
+ else
89
+ # forced drop of table if properties are different
90
+ if truncate_or_delete
91
+ destroy_model_storage(model, true)
92
+ end
93
+
94
+ statements = [ create_table_statement(connection, model, properties) ]
95
+ statements.concat(create_index_statements(model))
96
+ statements.concat(create_unique_index_statements(model))
97
+ statements.concat(create_sequence_statements(model))
98
+
99
+ statements.each do |statement|
100
+ command = connection.create_command(statement)
101
+ command.execute_non_query
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ true
108
+ end
109
+
110
+ # @api semipublic
111
+ def destroy_model_storage(model, forced = false)
112
+ table_name = model.storage_name(name)
113
+ klass = self.class
114
+ truncate_or_delete = klass.auto_migrate_with
115
+ if storage_exists?(table_name)
116
+ if truncate_or_delete && !forced
117
+ case truncate_or_delete
118
+ when :truncate
119
+ execute(truncate_table_statement(model))
120
+ when :delete
121
+ execute(delete_table_statement(model))
122
+ else
123
+ raise ArgumentError, "Unsupported auto_migrate_with option"
124
+ end
125
+ @truncated_tables ||= {}
126
+ @truncated_tables[table_name] = true
127
+ else
128
+ execute(drop_table_statement(model))
129
+ @truncated_tables[table_name] = nil if @truncated_tables
130
+ end
131
+ end
132
+ # added destroy of sequences
133
+ reset_sequences = klass.auto_migrate_reset_sequences
134
+ table_is_truncated = @truncated_tables && @truncated_tables[table_name]
135
+ unless truncate_or_delete && !reset_sequences && !forced
136
+ if sequence_exists?(model_sequence_name(model))
137
+ statement = if table_is_truncated && !forced
138
+ reset_sequence_statement(model)
139
+ else
140
+ drop_sequence_statement(model)
141
+ end
142
+ execute(statement) if statement
143
+ end
144
+ end
145
+ true
146
+ end
147
+
148
+ private
149
+
150
+ def storage_has_all_fields?(table_name, properties)
151
+ properties.map { |property| oracle_upcase(property.field) }.sort == storage_fields(table_name).sort
152
+ end
153
+
154
+ # If table or column name contains just lowercase characters then do uppercase
155
+ # as uppercase version will be used in Oracle data dictionary tables
156
+ def oracle_upcase(name)
157
+ name =~ /[A-Z]/ ? name : name.upcase
158
+ end
159
+
160
+ module SQL #:nodoc:
161
+ # private ## This cannot be private for current migrations
162
+
163
+ # @api private
164
+ def schema_name
165
+ @schema_name ||= select("SELECT SYS_CONTEXT('userenv','current_schema') FROM dual").first.freeze
166
+ end
167
+
168
+ # @api private
169
+ def create_sequence_statements(model)
170
+ name = self.name
171
+ table_name = model.storage_name(name)
172
+ serial = model.serial(name)
173
+
174
+ statements = []
175
+ if sequence_name = model_sequence_name(model)
176
+ sequence_name = quote_name(sequence_name)
177
+ column_name = quote_name(serial.field)
178
+
179
+ statements << DataMapper::Ext::String.compress_lines(<<-SQL)
180
+ CREATE SEQUENCE #{sequence_name} NOCACHE
181
+ SQL
182
+
183
+ # create trigger only if custom sequence name was not specified
184
+ unless serial.options[:sequence]
185
+ statements << DataMapper::Ext::String.compress_lines(<<-SQL)
186
+ CREATE OR REPLACE TRIGGER #{quote_name(default_trigger_name(table_name))}
187
+ BEFORE INSERT ON #{quote_name(table_name)} FOR EACH ROW
188
+ BEGIN
189
+ IF inserting THEN
190
+ IF :new.#{column_name} IS NULL THEN
191
+ SELECT #{sequence_name}.NEXTVAL INTO :new.#{column_name} FROM dual;
192
+ END IF;
193
+ END IF;
194
+ END;
195
+ SQL
196
+ end
197
+ end
198
+
199
+ statements
200
+ end
201
+
202
+ # @api private
203
+ def drop_sequence_statement(model)
204
+ if sequence_name = model_sequence_name(model)
205
+ "DROP SEQUENCE #{quote_name(sequence_name)}"
206
+ else
207
+ nil
208
+ end
209
+ end
210
+
211
+ # @api private
212
+ def reset_sequence_statement(model)
213
+ if sequence_name = model_sequence_name(model)
214
+ sequence_name = quote_name(sequence_name)
215
+ DataMapper::Ext::String.compress_lines(<<-SQL)
216
+ DECLARE
217
+ cval INTEGER;
218
+ BEGIN
219
+ SELECT #{sequence_name}.NEXTVAL INTO cval FROM dual;
220
+ EXECUTE IMMEDIATE 'ALTER SEQUENCE #{sequence_name} INCREMENT BY -' || cval || ' MINVALUE 0';
221
+ SELECT #{sequence_name}.NEXTVAL INTO cval FROM dual;
222
+ EXECUTE IMMEDIATE 'ALTER SEQUENCE #{sequence_name} INCREMENT BY 1';
223
+ END;
224
+ SQL
225
+ else
226
+ nil
227
+ end
228
+
229
+ end
230
+
231
+ # @api private
232
+ def truncate_table_statement(model)
233
+ "TRUNCATE TABLE #{quote_name(model.storage_name(name))}"
234
+ end
235
+
236
+ # @api private
237
+ def delete_table_statement(model)
238
+ "DELETE FROM #{quote_name(model.storage_name(name))}"
239
+ end
240
+
241
+ private
242
+
243
+ def model_sequence_name(model)
244
+ name = self.name
245
+ table_name = model.storage_name(name)
246
+ serial = model.serial(name)
247
+
248
+ if serial
249
+ serial.options[:sequence] || default_sequence_name(table_name)
250
+ else
251
+ nil
252
+ end
253
+ end
254
+
255
+ def default_sequence_name(table_name)
256
+ # truncate table name if necessary to fit in max length of identifier
257
+ "#{table_name[0,self.class::IDENTIFIER_MAX_LENGTH-4]}_seq"
258
+ end
259
+
260
+ def default_trigger_name(table_name)
261
+ # truncate table name if necessary to fit in max length of identifier
262
+ "#{table_name[0,self.class::IDENTIFIER_MAX_LENGTH-4]}_pkt"
263
+ end
264
+
265
+ # @api private
266
+ def add_column_statement
267
+ 'ADD'
268
+ end
269
+
270
+ end # module SQL
271
+
272
+ include SQL
273
+
274
+ module ClassMethods
275
+ # Types for Oracle databases.
276
+ #
277
+ # @return [Hash] types for Oracle databases.
278
+ #
279
+ # @api private
280
+ def type_map
281
+ length = Property::String.length
282
+ precision = Property::Numeric.precision
283
+ scale = Property::Decimal.scale
284
+
285
+ {
286
+ Integer => { :primitive => 'NUMBER', :precision => precision, :scale => 0 },
287
+ String => { :primitive => 'VARCHAR2', :length => length },
288
+ Class => { :primitive => 'VARCHAR2', :length => length },
289
+ BigDecimal => { :primitive => 'NUMBER', :precision => precision, :scale => nil },
290
+ Float => { :primitive => 'BINARY_FLOAT', },
291
+ DateTime => { :primitive => 'DATE' },
292
+ Date => { :primitive => 'DATE' },
293
+ Time => { :primitive => 'DATE' },
294
+ TrueClass => { :primitive => 'NUMBER', :precision => 1, :scale => 0 },
295
+ Property::Text => { :primitive => 'CLOB' },
296
+ }.freeze
297
+ end
298
+
299
+ # Use table truncate or delete for auto_migrate! to speed up test execution
300
+ #
301
+ # @param [Symbol] :truncate, :delete or :drop_and_create (or nil)
302
+ # do not specify parameter to return current value
303
+ #
304
+ # @return [Symbol] current value of auto_migrate_with option (nil returned for :drop_and_create)
305
+ #
306
+ # @api semipublic
307
+ def auto_migrate_with(value = :not_specified)
308
+ return @auto_migrate_with if value == :not_specified
309
+ value = nil if value == :drop_and_create
310
+ raise ArgumentError unless [nil, :truncate, :delete].include?(value)
311
+ @auto_migrate_with = value
312
+ end
313
+
314
+ # Set if sequences will or will not be reset during auto_migrate!
315
+ #
316
+ # @param [TrueClass, FalseClass] reset sequences?
317
+ # do not specify parameter to return current value
318
+ #
319
+ # @return [Symbol] current value of auto_migrate_reset_sequences option (default value is true)
320
+ #
321
+ # @api semipublic
322
+ def auto_migrate_reset_sequences(value = :not_specified)
323
+ return @auto_migrate_reset_sequences.nil? ? true : @auto_migrate_reset_sequences if value == :not_specified
324
+ raise ArgumentError unless [true, false].include?(value)
325
+ @auto_migrate_reset_sequences = value
326
+ end
327
+
328
+ end
329
+
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,159 @@
1
+ require 'dm-migrations/auto_migration'
2
+ require 'dm-migrations/adapters/dm-do-adapter'
3
+
4
+ module DataMapper
5
+ module Migrations
6
+ module PostgresAdapter
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 upgrade_model_storage(model)
18
+ without_notices { super }
19
+ end
20
+
21
+ # @api semipublic
22
+ def create_model_storage(model)
23
+ without_notices { super }
24
+ end
25
+
26
+ # @api semipublic
27
+ def destroy_model_storage(model)
28
+ if supports_drop_table_if_exists?
29
+ without_notices { super }
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ module SQL #:nodoc:
36
+ # private ## This cannot be private for current migrations
37
+
38
+ # @api private
39
+ def supports_drop_table_if_exists?
40
+ @supports_drop_table_if_exists ||= postgres_version >= '8.2'
41
+ end
42
+
43
+ # @api private
44
+ def schema_name
45
+ @schema_name ||= select('SELECT current_schema()').first.freeze
46
+ end
47
+
48
+ # @api private
49
+ def postgres_version
50
+ @postgres_version ||= select('SELECT version()').first.split[1].freeze
51
+ end
52
+
53
+ # @api private
54
+ def without_notices
55
+ # execute the block with NOTICE messages disabled
56
+ begin
57
+ execute('SET client_min_messages = warning')
58
+ yield
59
+ ensure
60
+ execute('RESET client_min_messages')
61
+ end
62
+ end
63
+
64
+ # @api private
65
+ def property_schema_hash(property)
66
+ schema = super
67
+
68
+ primitive = property.primitive
69
+
70
+ # Postgres does not support precision and scale for Float
71
+ if primitive == Float
72
+ schema.delete(:precision)
73
+ schema.delete(:scale)
74
+ end
75
+
76
+ if property.kind_of?(Property::Integer)
77
+ min = property.min
78
+ max = property.max
79
+
80
+ schema[:primitive] = integer_column_statement(min..max) if min && max
81
+ end
82
+
83
+ if schema[:serial]
84
+ schema[:primitive] = serial_column_statement(min..max)
85
+ end
86
+
87
+ schema
88
+ end
89
+
90
+ private
91
+
92
+ # Return SQL statement for the integer column
93
+ #
94
+ # @param [Range] range
95
+ # the min/max allowed integers
96
+ #
97
+ # @return [String]
98
+ # the statement to create the integer column
99
+ #
100
+ # @api private
101
+ def integer_column_statement(range)
102
+ min = range.first
103
+ max = range.last
104
+
105
+ smallint = 2**15
106
+ integer = 2**31
107
+ bigint = 2**63
108
+
109
+ if min >= -smallint && max < smallint then 'SMALLINT'
110
+ elsif min >= -integer && max < integer then 'INTEGER'
111
+ elsif min >= -bigint && max < bigint then 'BIGINT'
112
+ else
113
+ raise ArgumentError, "min #{min} and max #{max} exceeds supported range"
114
+ end
115
+ end
116
+
117
+ # Return SQL statement for the serial column
118
+ #
119
+ # @param [Integer] max
120
+ # the max allowed integer
121
+ #
122
+ # @return [String]
123
+ # the statement to create the serial column
124
+ #
125
+ # @api private
126
+ def serial_column_statement(range)
127
+ max = range.last
128
+
129
+ if max.nil? || max < 2**31 then 'SERIAL'
130
+ elsif max < 2**63 then 'BIGSERIAL'
131
+ else
132
+ raise ArgumentError, "min #{range.first} and max #{max} exceeds supported range"
133
+ end
134
+ end
135
+ end # module SQL
136
+
137
+ include SQL
138
+
139
+ module ClassMethods
140
+ # Types for PostgreSQL databases.
141
+ #
142
+ # @return [Hash] types for PostgreSQL databases.
143
+ #
144
+ # @api private
145
+ def type_map
146
+ precision = Property::Numeric.precision
147
+ scale = Property::Decimal.scale
148
+
149
+ super.merge(
150
+ Property::Binary => { :primitive => 'BYTEA' },
151
+ BigDecimal => { :primitive => 'NUMERIC', :precision => precision, :scale => scale },
152
+ Float => { :primitive => 'DOUBLE PRECISION' }
153
+ ).freeze
154
+ end
155
+ end
156
+
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,96 @@
1
+ require 'dm-migrations/auto_migration'
2
+ require 'dm-migrations/adapters/dm-do-adapter'
3
+
4
+ module DataMapper
5
+ module Migrations
6
+ module SqliteAdapter
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
+ table_info(storage_name).any?
19
+ end
20
+
21
+ # @api semipublic
22
+ def field_exists?(storage_name, column_name)
23
+ table_info(storage_name).any? do |row|
24
+ row.name == column_name
25
+ end
26
+ end
27
+
28
+ module SQL #:nodoc:
29
+ # private ## This cannot be private for current migrations
30
+
31
+ # @api private
32
+ def supports_serial?
33
+ @supports_serial ||= sqlite_version >= '3.1.0'
34
+ end
35
+
36
+ # @api private
37
+ def supports_drop_table_if_exists?
38
+ @supports_drop_table_if_exists ||= sqlite_version >= '3.3.0'
39
+ end
40
+
41
+ # @api private
42
+ def table_info(table_name)
43
+ select("PRAGMA table_info(#{quote_name(table_name)})")
44
+ end
45
+
46
+ # @api private
47
+ def create_table_statement(connection, model, properties)
48
+ statement = DataMapper::Ext::String.compress_lines(<<-SQL)
49
+ CREATE TABLE #{quote_name(model.storage_name(name))}
50
+ (#{properties.map { |property| property_schema_statement(connection, property_schema_hash(property)) }.join(', ')}
51
+ SQL
52
+
53
+ # skip adding the primary key if one of the columns is serial. In
54
+ # SQLite the serial column must be the primary key, so it has already
55
+ # been defined
56
+ unless properties.any? { |property| property.serial? }
57
+ statement << ", PRIMARY KEY(#{properties.key.map { |property| quote_name(property.field) }.join(', ')})"
58
+ end
59
+
60
+ statement << ')'
61
+ statement
62
+ end
63
+
64
+ # @api private
65
+ def property_schema_statement(connection, schema)
66
+ statement = super
67
+
68
+ if supports_serial? && schema[:serial]
69
+ statement << ' PRIMARY KEY AUTOINCREMENT'
70
+ end
71
+
72
+ statement
73
+ end
74
+
75
+ # @api private
76
+ def sqlite_version
77
+ @sqlite_version ||= select('SELECT sqlite_version(*)').first.freeze
78
+ end
79
+ end # module SQL
80
+
81
+ include SQL
82
+
83
+ module ClassMethods
84
+ # Types for SQLite 3 databases.
85
+ #
86
+ # @return [Hash] types for SQLite 3 databases.
87
+ #
88
+ # @api private
89
+ def type_map
90
+ super.merge(Class => { :primitive => 'VARCHAR' }).freeze
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end