ardm-migrations 1.2.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 (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