xmigra 1.4.0 → 1.5.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.
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