xmigra 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/xmigra/db_support/mssql.rb +12 -5
- data/lib/xmigra/db_support/psql.rb +12 -2
- data/lib/xmigra/new_migration_adder.rb +63 -0
- data/lib/xmigra/program.rb +23 -2
- data/lib/xmigra/schema_updater.rb +8 -3
- data/lib/xmigra/vcs_support/git.rb +29 -0
- data/lib/xmigra/vcs_support/svn.rb +103 -1
- data/lib/xmigra/version.rb +1 -1
- data/test/git_vcs.rb +29 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f93c4d5e00f442d8bd288980b7d3754b5dca202
|
4
|
+
data.tar.gz: 9485199a710af1bd8f2c0ecbfc5ae9186c5e0097
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2d7104989df02da32dc2adb897e9a4abf986b1422f541431080540ea76b101065bf42617536c7b2f7609da19e82917a98e810fc66d89f935366f53da827ca297
|
7
|
+
data.tar.gz: a7a66fd0a47c0dca11235f3c8593981fc4fb4ec89f33ef69c0d08b9aa665abba68d0854fffbc149913a9391d30af79c24af6f6704cc045b045e9acacd49b9e7f
|
@@ -57,7 +57,7 @@ module XMigra
|
|
57
57
|
return @stats_objs
|
58
58
|
end
|
59
59
|
|
60
|
-
def in_ddl_transaction
|
60
|
+
def in_ddl_transaction(options={})
|
61
61
|
parts = []
|
62
62
|
parts << <<-"END_OF_SQL"
|
63
63
|
SET ANSI_NULLS ON
|
@@ -71,18 +71,25 @@ GO
|
|
71
71
|
|
72
72
|
SET NOCOUNT ON
|
73
73
|
GO
|
74
|
-
|
74
|
+
-------------------- ERROR REFERENCE LINE --------------------
|
75
|
+
DECLARE @BATCH_START_OFFSET INTEGER; SET @BATCH_START_OFFSET = 0;
|
75
76
|
BEGIN TRY
|
76
77
|
BEGIN TRAN;
|
77
78
|
END_OF_SQL
|
78
79
|
|
80
|
+
offset_lines = 5
|
79
81
|
each_batch(yield) do |batch|
|
80
82
|
batch_literal = MSSQLSpecifics.string_literal("\n" + batch)
|
81
|
-
parts << "EXEC sp_executesql @statement = #{batch_literal};"
|
83
|
+
parts << "SET @BATCH_START_OFFSET = #{offset_lines}; EXEC sp_executesql @statement = #{batch_literal}; SET @BATCH_START_OFFSET = 0;"
|
84
|
+
offset_lines += parts[-1].count("\n") + 1
|
82
85
|
end
|
83
86
|
|
87
|
+
if options[:dry_run]
|
88
|
+
parts << " PRINT N'Dry-run successful. Rolling back changes.'; ROLLBACK TRAN;"
|
89
|
+
else
|
90
|
+
parts << " COMMIT TRAN;"
|
91
|
+
end
|
84
92
|
parts << <<-"END_OF_SQL"
|
85
|
-
COMMIT TRAN;
|
86
93
|
END TRY
|
87
94
|
BEGIN CATCH
|
88
95
|
ROLLBACK TRAN;
|
@@ -93,7 +100,7 @@ BEGIN CATCH
|
|
93
100
|
|
94
101
|
PRINT N'Update failed: ' + ERROR_MESSAGE();
|
95
102
|
PRINT N' State: ' + CAST(ERROR_STATE() AS NVARCHAR);
|
96
|
-
PRINT N' Line: ' + CAST(ERROR_LINE() AS NVARCHAR)
|
103
|
+
PRINT N' Line: ' + CAST(@BATCH_START_OFFSET + ERROR_LINE() - 1 AS NVARCHAR) + N' after ERROR REFERENCE LINE'
|
97
104
|
|
98
105
|
SELECT
|
99
106
|
@ErrorMessage = N'Update failed: ' + ERROR_MESSAGE(),
|
@@ -23,8 +23,18 @@ module XMigra
|
|
23
23
|
|
24
24
|
def filename_metavariable; "[{filename}]"; end
|
25
25
|
|
26
|
-
def in_ddl_transaction
|
27
|
-
|
26
|
+
def in_ddl_transaction(options={})
|
27
|
+
transaction_wrapup = begin
|
28
|
+
if options[:dry_run]
|
29
|
+
PgSQLSpecifics.in_plpgsql(%Q{
|
30
|
+
RAISE NOTICE 'Dry-run successful. Rolling back changes.';
|
31
|
+
}) + "\nROLLBACK;"
|
32
|
+
else
|
33
|
+
"COMMIT;"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
["BEGIN;", yield, transaction_wrapup].join("\n")
|
28
38
|
end
|
29
39
|
|
30
40
|
def check_execution_environment_sql
|
@@ -5,6 +5,11 @@ module XMigra
|
|
5
5
|
class NewMigrationAdder < SchemaManipulator
|
6
6
|
OBSOLETE_VERINC_FILE = 'version-upgrade-obsolete.yaml'
|
7
7
|
|
8
|
+
# Return this class (not an instance of it) from a Proc yielded by
|
9
|
+
# each_possible_production_chain_extension_handler to continue through the
|
10
|
+
# handler chain.
|
11
|
+
class IgnoreHandler; end
|
12
|
+
|
8
13
|
def initialize(path)
|
9
14
|
super(path)
|
10
15
|
end
|
@@ -23,6 +28,13 @@ module XMigra
|
|
23
28
|
end
|
24
29
|
Hash === head_info or raise XMigra::Error, "Invalid #{MigrationChain::HEAD_FILE} format"
|
25
30
|
|
31
|
+
if !head_info.empty? && respond_to?(:vcs_production_contents) && (production_head_contents = vcs_production_contents(head_file))
|
32
|
+
production_head_info = YAML.load(production_head_contents)
|
33
|
+
extending_production = head_info[MigrationChain::LATEST_CHANGE] == production_head_info[MigrationChain::LATEST_CHANGE]
|
34
|
+
else
|
35
|
+
extending_production = false
|
36
|
+
end
|
37
|
+
|
26
38
|
new_fpath = struct_dir.join(
|
27
39
|
[Date.today.strftime("%Y-%m-%d"), summary].join(' ') + '.yaml'
|
28
40
|
)
|
@@ -68,7 +80,58 @@ module XMigra
|
|
68
80
|
mv_method.call(bufp, obufp)
|
69
81
|
end
|
70
82
|
|
83
|
+
production_chain_extended if extending_production
|
84
|
+
|
71
85
|
return new_fpath
|
72
86
|
end
|
87
|
+
|
88
|
+
# Called when the chain of migrations in the production/master branch is
|
89
|
+
# extended with a new migration.
|
90
|
+
#
|
91
|
+
# This method calls each_possible_production_chain_extension_handler to
|
92
|
+
# generate a chain of handlers.
|
93
|
+
#
|
94
|
+
def production_chain_extended
|
95
|
+
Dir.chdir(self.path) do
|
96
|
+
each_possible_production_chain_extension_handler do |handler|
|
97
|
+
if handler.kind_of? Proc
|
98
|
+
handler_result = handler[]
|
99
|
+
break true unless handler_result == IgnoreHandler
|
100
|
+
else
|
101
|
+
handler_result = system(handler)
|
102
|
+
break true unless handler_result.nil?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Yield command strings or Proc instances to attempt handling the
|
109
|
+
# production-chain-extension event
|
110
|
+
#
|
111
|
+
# Strings yielded by this method will be executed using Kernel#system. If
|
112
|
+
# this results in `nil` (command does not exist), processing will continue
|
113
|
+
# through the remaining handlers.
|
114
|
+
#
|
115
|
+
# Procs yielded by this method will be executed without any parameters.
|
116
|
+
# Unless the invocation returns IgnoreHandler, event processing will
|
117
|
+
# terminate after invocation.
|
118
|
+
#
|
119
|
+
def each_possible_production_chain_extension_handler
|
120
|
+
yield "on-prod-chain-extended-local"
|
121
|
+
if respond_to?(:vcs_prod_chain_extension_handler) && (vcs_handler = vcs_prod_chain_extension_handler)
|
122
|
+
yield vcs_handler.to_s
|
123
|
+
end
|
124
|
+
yield "on-prod-chain-extended"
|
125
|
+
yield Proc.new {
|
126
|
+
next unless respond_to? :warning
|
127
|
+
warning(<<END_MESSAGE)
|
128
|
+
This command has extended the production migration chain.
|
129
|
+
|
130
|
+
Backing up your development database now may be advantageous in case you need
|
131
|
+
to accept migrations developed in parallel from upstream before merging your
|
132
|
+
work back to the mainline.
|
133
|
+
END_MESSAGE
|
134
|
+
}
|
135
|
+
end
|
73
136
|
end
|
74
137
|
end
|
data/lib/xmigra/program.rb
CHANGED
@@ -161,6 +161,14 @@ END_OF_HELP
|
|
161
161
|
end
|
162
162
|
end
|
163
163
|
|
164
|
+
if use[:dry_run]
|
165
|
+
options.dry_run = false
|
166
|
+
flags.on("--dry-run", "Generated script will test upgrade",
|
167
|
+
" commands without committing changes") do
|
168
|
+
options.dry_run = true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
164
172
|
options.source_dir = Dir.pwd
|
165
173
|
flags.on("--source=DIR", "Work from/on the schema in DIR") do |dir|
|
166
174
|
options.source_dir = File.expand_path(dir)
|
@@ -349,7 +357,9 @@ migration, and is included in the upgrade metadata stored in the database. Use
|
|
349
357
|
of the '%program_cmd new' command is recommended; it handles several tiresome
|
350
358
|
and error-prone tasks: creating a new migration file with a conformant name,
|
351
359
|
setting the "starting from" section to the correct value, and updating
|
352
|
-
SCHEMA/structure/head.yaml to reference the newly generated file.
|
360
|
+
SCHEMA/structure/head.yaml to reference the newly generated file. It can also
|
361
|
+
print a warning or run a command when the source condition indicates a database
|
362
|
+
backup would be a good idea.
|
353
363
|
|
354
364
|
The SCHEMA/structure/head.yaml file deserves special note: it contains a
|
355
365
|
reference to the last migration to be applied. Because of this, parallel
|
@@ -672,6 +682,16 @@ This command generates a new migration file and ties it into the current
|
|
672
682
|
migration chain. The name of the new file is generated from today's date and
|
673
683
|
the given MIGRATION_SUMMARY. The resulting new file may be opened in an
|
674
684
|
editor (see the --[no-]edit option).
|
685
|
+
|
686
|
+
If this command extends the production migration chain, it will attempt to
|
687
|
+
locate a handler function ("on-prod-chain-extended-local", a VCS-specified
|
688
|
+
handler, or "on-prod-chain-extended") executable from the schema root
|
689
|
+
directory. If it cannot locate one of these, it will print a message about
|
690
|
+
the advisability of taking a backup of the development database.
|
691
|
+
|
692
|
+
Git VCS specifics: The #{GitSpecifics::PRODUCTION_CHAIN_EXTENSION_COMMAND} attribute on the
|
693
|
+
database.yaml file can be used to specify a handler for the production chain
|
694
|
+
extension.
|
675
695
|
END_OF_HELP
|
676
696
|
|
677
697
|
argument_error_unless(args.length == 1,
|
@@ -689,7 +709,7 @@ END_OF_HELP
|
|
689
709
|
end
|
690
710
|
|
691
711
|
subcommand 'upgrade', "Generate an upgrade script" do |argv|
|
692
|
-
args, options = command_line(argv, {:production=>true, :outfile=>true},
|
712
|
+
args, options = command_line(argv, {:production=>true, :outfile=>true, :dry_run=>true},
|
693
713
|
:help=> <<END_OF_HELP)
|
694
714
|
Running this command will generate an update script from the source schema.
|
695
715
|
Generation of a production script involves more checks on the status of the
|
@@ -705,6 +725,7 @@ END_OF_HELP
|
|
705
725
|
sql_gen = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
|
706
726
|
sql_gen.load_plugin!
|
707
727
|
sql_gen.production = options.production
|
728
|
+
sql_gen.dry_run = options.dry_run
|
708
729
|
|
709
730
|
output_to(options.outfile) do |out_stream|
|
710
731
|
out_stream.print(sql_gen.update_sql)
|
@@ -50,16 +50,21 @@ RUNNING THIS SCRIPT ON A PRODUCTION DATABASE WILL FAIL.
|
|
50
50
|
end
|
51
51
|
|
52
52
|
@production = false
|
53
|
+
@dry_run = false
|
53
54
|
end
|
54
55
|
|
55
|
-
attr_accessor :production
|
56
|
+
attr_accessor :production, :dry_run
|
56
57
|
attr_reader :migrations, :access_artifacts, :indexes, :branch_upgrade
|
57
58
|
|
58
59
|
def inspect
|
59
60
|
"<#{self.class.name}: path=#{path.to_s.inspect}, db=#{@db_specifics}, vcs=#{@vcs_specifics}>"
|
60
61
|
end
|
61
62
|
|
62
|
-
def in_ddl_transaction
|
63
|
+
def in_ddl_transaction(options = {})
|
64
|
+
if options[:dry_run]
|
65
|
+
raise(XMigra::Error, 'DDL transaction not supported; dry-run unavailable.')
|
66
|
+
end
|
67
|
+
|
63
68
|
yield
|
64
69
|
end
|
65
70
|
|
@@ -90,7 +95,7 @@ RUNNING THIS SCRIPT ON A PRODUCTION DATABASE WILL FAIL.
|
|
90
95
|
intro_comment << "\n\n"
|
91
96
|
|
92
97
|
# If supported, wrap transactionality around modifications
|
93
|
-
intro_comment + in_ddl_transaction do
|
98
|
+
intro_comment + in_ddl_transaction(:dry_run => @dry_run) do
|
94
99
|
script_parts = [
|
95
100
|
# Check for blatantly incorrect application of script, e.g. running
|
96
101
|
# on master or template database.
|
@@ -6,6 +6,7 @@ module XMigra
|
|
6
6
|
|
7
7
|
MASTER_HEAD_ATTRIBUTE = 'xmigra-master'
|
8
8
|
MASTER_BRANCH_SUBDIR = 'xmigra-master'
|
9
|
+
PRODUCTION_CHAIN_EXTENSION_COMMAND = 'xmigra-on-production-chain-extended'
|
9
10
|
|
10
11
|
class AttributesFile
|
11
12
|
def initialize(effect_root, access=:shared)
|
@@ -244,6 +245,34 @@ module XMigra
|
|
244
245
|
git(:rm, path, :get_result=>false)
|
245
246
|
end
|
246
247
|
|
248
|
+
def vcs_production_contents(path)
|
249
|
+
return nil unless git_master_head(:required => false)
|
250
|
+
git_fetch_master_branch
|
251
|
+
git(:show, [git_master_local_branch, git_internal_path].join(':'), :quiet=>true)
|
252
|
+
rescue VersionControlError
|
253
|
+
return nil
|
254
|
+
end
|
255
|
+
|
256
|
+
def vcs_prod_chain_extension_handler
|
257
|
+
attr_val = GitSpecifics.attr_values(
|
258
|
+
PRODUCTION_CHAIN_EXTENSION_COMMAND,
|
259
|
+
self.path + SchemaManipulator::DBINFO_FILE,
|
260
|
+
:required=>false,
|
261
|
+
)[0]
|
262
|
+
|
263
|
+
# Check for special value
|
264
|
+
return nil if attr_val == 'unspecified'
|
265
|
+
|
266
|
+
handler_path = Pathname(attr_val)
|
267
|
+
if handler_path.absolute?
|
268
|
+
return handler_path if handler_path.exist?
|
269
|
+
else
|
270
|
+
handler_path = self.path + handler_path
|
271
|
+
return handler_path if handler_path.exist?
|
272
|
+
end
|
273
|
+
return attr_val
|
274
|
+
end
|
275
|
+
|
247
276
|
def production_pattern
|
248
277
|
".+"
|
249
278
|
end
|
@@ -22,7 +22,7 @@ module XMigra
|
|
22
22
|
def run_svn(subcmd, *args)
|
23
23
|
options = (Hash === args[-1]) ? args.pop : {}
|
24
24
|
no_result = !options.fetch(:get_result, true)
|
25
|
-
raw_result = options.fetch(:raw, false)
|
25
|
+
raw_result = options.fetch(:raw, false) || subcmd.to_s == 'cat'
|
26
26
|
|
27
27
|
cmd_parts = ["svn", subcmd.to_s]
|
28
28
|
cmd_parts << "--xml" unless no_result || raw_result
|
@@ -235,5 +235,107 @@ END_OF_MESSAGE
|
|
235
235
|
return @subversion_info if defined? @subversion_info
|
236
236
|
return @subversion_info = subversion(:info, self.path)
|
237
237
|
end
|
238
|
+
|
239
|
+
def vcs_production_contents(path)
|
240
|
+
path = Pathname(path)
|
241
|
+
|
242
|
+
# Check for a production pattern. If none exists, there is no way to
|
243
|
+
# identify which branches are production, so essentially no production
|
244
|
+
# content:
|
245
|
+
prod_pat = self.production_pattern
|
246
|
+
return nil if prod_pat.nil?
|
247
|
+
prod_pat = Regexp.compile(prod_pat.chomp)
|
248
|
+
|
249
|
+
# Is the current branch a production branch? If so, cat the committed
|
250
|
+
# version:
|
251
|
+
if branch_identifier =~ prod_pat
|
252
|
+
return svn(:cat, path.to_s)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Use an SvnHistoryTracer to walk back through the history of self.path
|
256
|
+
# looking for a copy from a production branch.
|
257
|
+
tracer = SvnHistoryTracer.new(self.path)
|
258
|
+
|
259
|
+
while !(match = tracer.earliest_loaded_repopath =~ prod_pat) && tracer.load_parent_commit
|
260
|
+
# loop
|
261
|
+
end
|
262
|
+
|
263
|
+
if match
|
264
|
+
subversion(:cat, "-r#{tracer.earliest_loaded_revision}", path.to_s)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class SvnHistoryTracer
|
270
|
+
include SubversionSpecifics
|
271
|
+
|
272
|
+
def initialize(path)
|
273
|
+
@path = Pathname(path)
|
274
|
+
info_doc = subversion(:info, path.to_s)
|
275
|
+
@root_url = info_doc.elements['string(info/entry/repository/root)']
|
276
|
+
@most_recent_commit = info_doc.elements['string(info/entry/@revision)'].to_i
|
277
|
+
@history = []
|
278
|
+
@next_query = [branch_identifier, @most_recent_commit]
|
279
|
+
@history.unshift(@next_query.dup)
|
280
|
+
end
|
281
|
+
|
282
|
+
attr_reader :path, :most_recent_commit, :history
|
283
|
+
|
284
|
+
def load_parent_commit
|
285
|
+
log_doc = next_earlier_log
|
286
|
+
if copy_elt = copying_element(log_doc)
|
287
|
+
trailing_part = branch_identifier[copy_elt.text.length..-1]
|
288
|
+
@next_query = [
|
289
|
+
copy_elt.attributes['copyfrom-path'] + trailing_part,
|
290
|
+
copy_elt.attributes['copyfrom-rev'].to_i
|
291
|
+
]
|
292
|
+
@history.unshift(@next_query)
|
293
|
+
@next_query.dup
|
294
|
+
elsif change_elt = log_doc.elements['/log/logentry']
|
295
|
+
@next_query[1] = change_elt.attributes['revision'].to_i - 1
|
296
|
+
@next_query.dup if @next_query[1] > 0
|
297
|
+
else
|
298
|
+
@next_query[1] -= 1
|
299
|
+
@next_query.dup if @next_query[1] > 0
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def history_exhausted?
|
304
|
+
@next_query[1] <= 0
|
305
|
+
end
|
306
|
+
|
307
|
+
def earliest_loaded_repopath
|
308
|
+
history[0][0]
|
309
|
+
end
|
310
|
+
|
311
|
+
def earliest_loaded_url
|
312
|
+
@root_url + history[0][0]
|
313
|
+
end
|
314
|
+
|
315
|
+
def earliest_loaded_revision
|
316
|
+
history[0][1]
|
317
|
+
end
|
318
|
+
|
319
|
+
def earliest_loaded_pinned_url(rel_path=nil)
|
320
|
+
pin_rev = @history[0][1]
|
321
|
+
if rel_path.nil?
|
322
|
+
[earliest_loaded_url, pin_rev.to_s].join('@')
|
323
|
+
else
|
324
|
+
rel_path = Pathname(rel_path)
|
325
|
+
"#{earliest_loaded_url}/#{rel_path}@#{pin_rev}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def copying_element(log_doc)
|
330
|
+
log_doc.each_element %Q{/log/logentry/paths/path[@copyfrom-path]} do |elt|
|
331
|
+
return elt if elt.text == @next_query[0]
|
332
|
+
return elt if @next_query[0].start_with? (elt.text + '/')
|
333
|
+
end
|
334
|
+
return nil
|
335
|
+
end
|
336
|
+
|
337
|
+
def next_earlier_log
|
338
|
+
subversion(:log, '-l1', '-v', "-r#{@next_query[1]}:1", self.path)
|
339
|
+
end
|
238
340
|
end
|
239
341
|
end
|
data/lib/xmigra/version.rb
CHANGED
data/test/git_vcs.rb
CHANGED
@@ -239,4 +239,33 @@ if GIT_PRESENT
|
|
239
239
|
end
|
240
240
|
end
|
241
241
|
end
|
242
|
+
|
243
|
+
run_test "XMigra detects extension of the production migration chain" do
|
244
|
+
capture_chain_extension = Module.new do
|
245
|
+
def production_chain_extended(*args)
|
246
|
+
(@captured_call_args ||= []) << args
|
247
|
+
end
|
248
|
+
|
249
|
+
attr_reader :captured_call_args
|
250
|
+
end
|
251
|
+
|
252
|
+
2.temp_dirs do |upstream, repo|
|
253
|
+
initialize_git_repo(upstream)
|
254
|
+
|
255
|
+
Dir.chdir(upstream) do
|
256
|
+
commit_a_migration "first table"
|
257
|
+
make_this_branch_master
|
258
|
+
end
|
259
|
+
|
260
|
+
`git clone "#{upstream.expand_path}" "#{repo}" 2>/dev/null`
|
261
|
+
|
262
|
+
Dir.chdir(repo) do
|
263
|
+
XMigra::NewMigrationAdder.new('.') do |tool|
|
264
|
+
tool.extend(capture_chain_extension)
|
265
|
+
tool.add_migration('Create foo table')
|
266
|
+
assert_eq tool.captured_call_args, [[]]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
242
271
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xmigra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Next IT Corporation
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-07-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|