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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 87e678fcb7c1e98cff100798bd30948628becbcb
4
- data.tar.gz: d89d555130e4e53e28f449f1cfdcd5ded31bf942
3
+ metadata.gz: 0f93c4d5e00f442d8bd288980b7d3754b5dca202
4
+ data.tar.gz: 9485199a710af1bd8f2c0ecbfc5ae9186c5e0097
5
5
  SHA512:
6
- metadata.gz: e35d004c20dc96dfd27418281ce19ded3b1c639d8aa7cb3591ba713c29b3c1246a4389ef63801a270b6409cfef22968f6d0eaa97f71ca2db574b35eedf99b933
7
- data.tar.gz: ade9c348aacb41f7a81aa669c60511076fbf20b5ea0464f129473e2a859b0dfd87a558b979910eb88104550d0b32e9f8c3cc9ff683f11055bf282ed8006674f8
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
- ["BEGIN;", yield, "COMMIT;"].join("\n")
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module XMigra
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0"
3
3
  end
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.0
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-05-25 00:00:00.000000000 Z
12
+ date: 2015-07-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler