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.
- data/lib/dm-migrations.rb +3 -0
- data/lib/dm-migrations/adapters/dm-do-adapter.rb +284 -0
- data/lib/dm-migrations/adapters/dm-mysql-adapter.rb +283 -0
- data/lib/dm-migrations/adapters/dm-oracle-adapter.rb +321 -0
- data/lib/dm-migrations/adapters/dm-postgres-adapter.rb +159 -0
- data/lib/dm-migrations/adapters/dm-sqlite-adapter.rb +96 -0
- data/lib/dm-migrations/adapters/dm-sqlserver-adapter.rb +177 -0
- data/lib/dm-migrations/adapters/dm-yaml-adapter.rb +23 -0
- data/lib/dm-migrations/auto_migration.rb +237 -0
- data/lib/dm-migrations/migration.rb +217 -0
- data/lib/dm-migrations/migration_runner.rb +85 -0
- data/lib/dm-migrations/sql.rb +5 -0
- data/lib/dm-migrations/sql/column.rb +5 -0
- data/lib/dm-migrations/sql/mysql.rb +53 -0
- data/lib/dm-migrations/sql/postgres.rb +78 -0
- data/lib/dm-migrations/sql/sqlite.rb +45 -0
- data/lib/dm-migrations/sql/table.rb +15 -0
- data/lib/dm-migrations/sql/table_creator.rb +102 -0
- data/lib/dm-migrations/sql/table_modifier.rb +51 -0
- data/lib/spec/example/migration_example_group.rb +73 -0
- data/lib/spec/matchers/migration_matchers.rb +106 -0
- data/spec/integration/auto_migration_spec.rb +506 -0
- data/spec/integration/migration_runner_spec.rb +89 -0
- data/spec/integration/migration_spec.rb +138 -0
- data/spec/integration/sql_spec.rb +190 -0
- data/spec/isolated/require_after_setup_spec.rb +30 -0
- data/spec/isolated/require_before_setup_spec.rb +30 -0
- data/spec/isolated/require_spec.rb +25 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/unit/migration_spec.rb +453 -0
- data/spec/unit/sql/column_spec.rb +14 -0
- data/spec/unit/sql/postgres_spec.rb +97 -0
- data/spec/unit/sql/sqlite_extensions_spec.rb +108 -0
- data/spec/unit/sql/table_creator_spec.rb +94 -0
- data/spec/unit/sql/table_modifier_spec.rb +49 -0
- data/spec/unit/sql/table_spec.rb +28 -0
- data/spec/unit/sql_spec.rb +7 -0
- metadata +157 -0
@@ -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
|
@@ -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::DEFAULT_PRECISION
|
147
|
+
scale = Property::Decimal::DEFAULT_SCALE
|
148
|
+
|
149
|
+
@type_map ||= 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
|