xmigra 1.0.1 → 1.1.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.
@@ -0,0 +1,39 @@
1
+
2
+ module XMigra
3
+ class SchemaManipulator
4
+ DBINFO_FILE = 'database.yaml'
5
+ PERMISSIONS_FILE = 'permissions.yaml'
6
+ ACCESS_SUBDIR = 'access'
7
+ INDEXES_SUBDIR = 'indexes'
8
+ STRUCTURE_SUBDIR = 'structure'
9
+ VERINC_FILE = 'branch-upgrade.yaml'
10
+
11
+ def initialize(path)
12
+ @path = Pathname.new(path)
13
+ @db_info = YAML.load_file(@path + DBINFO_FILE)
14
+ raise TypeError, "Expected Hash in #{DBINFO_FILE}" unless Hash === @db_info
15
+ @db_info = Hash.new do |h, k|
16
+ raise Error, "#{DBINFO_FILE} missing key #{k.inspect}"
17
+ end.update(@db_info)
18
+
19
+ db_system = @db_info['system']
20
+ extend(
21
+ @db_specifics = DatabaseSupportModules.find {|m|
22
+ m::SYSTEM_NAME == db_system
23
+ } || NoSpecifics
24
+ )
25
+
26
+ extend(
27
+ @vcs_specifics = VersionControlSupportModules.find {|m|
28
+ m.manages(path)
29
+ } || NoSpecifics
30
+ )
31
+ end
32
+
33
+ attr_reader :path
34
+
35
+ def branch_upgrade_file
36
+ @path.join(STRUCTURE_SUBDIR, VERINC_FILE)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,183 @@
1
+
2
+ require 'xmigra/schema_manipulator'
3
+
4
+ module XMigra
5
+ class SchemaUpdater < SchemaManipulator
6
+ DEV_SCRIPT_WARNING = <<-"END_OF_TEXT"
7
+ *********************************************************
8
+ *** WARNING ***
9
+ *********************************************************
10
+
11
+ THIS SCRIPT IS FOR USE ONLY ON DEVELOPMENT DATABASES.
12
+
13
+ IF RUN ON AN EMPTY DATABASE IT WILL CREATE A DEVELOPMENT
14
+ DATABASE THAT IS NOT GUARANTEED TO FOLLOW ANY COMMITTED
15
+ MIGRATION PATH.
16
+
17
+ RUNNING THIS SCRIPT ON A PRODUCTION DATABASE WILL FAIL.
18
+ END_OF_TEXT
19
+
20
+ def initialize(path)
21
+ super(path)
22
+
23
+ @file_based_groups = []
24
+
25
+ begin
26
+ @file_based_groups << (@access_artifacts = AccessArtifactCollection.new(
27
+ @path.join(ACCESS_SUBDIR),
28
+ :db_specifics=>@db_specifics,
29
+ :filename_metavariable=>@db_info.fetch('filename metavariable', nil)
30
+ ))
31
+ @file_based_groups << (@indexes = IndexCollection.new(
32
+ @path.join(INDEXES_SUBDIR),
33
+ :db_specifics=>@db_specifics
34
+ ))
35
+ @file_based_groups << (@migrations = MigrationChain.new(
36
+ @path.join(STRUCTURE_SUBDIR),
37
+ :db_specifics=>@db_specifics
38
+ ))
39
+
40
+ @branch_upgrade = BranchUpgrade.new(branch_upgrade_file)
41
+ @file_based_groups << [@branch_upgrade] if @branch_upgrade.found?
42
+ rescue Error
43
+ raise
44
+ rescue StandardError
45
+ raise Error, "Error initializing #{self.class} components"
46
+ end
47
+
48
+ @production = false
49
+ end
50
+
51
+ attr_accessor :production
52
+ attr_reader :migrations, :access_artifacts, :indexes, :branch_upgrade
53
+
54
+ def inspect
55
+ "<#{self.class.name}: path=#{path.to_s.inspect}, db=#{@db_specifics}, vcs=#{@vcs_specifics}>"
56
+ end
57
+
58
+ def in_ddl_transaction
59
+ yield
60
+ end
61
+
62
+ def ddl_block_separator; "\n"; end
63
+
64
+ def update_sql
65
+ raise XMigra::Error, "Incomplete migration chain" unless @migrations.complete?
66
+ raise XMigra::Error, "Unchained migrations exist" unless @migrations.includes_all?
67
+ if respond_to? :warning
68
+ @branch_upgrade.warnings.each {|w| warning(w)}
69
+ if @branch_upgrade.found? && !@branch_upgrade.applicable?(@migrations)
70
+ warning("#{branch_upgrade.file_path} does not apply to the current migration chain.")
71
+ end
72
+ end
73
+
74
+ check_working_copy!
75
+
76
+ intro_comment = @db_info.fetch('script comment', '')
77
+ intro_comment << if production
78
+ sql_comment_block(vcs_information || "")
79
+ else
80
+ sql_comment_block(DEV_SCRIPT_WARNING)
81
+ end
82
+ intro_comment << "\n\n"
83
+
84
+ # If supported, wrap transactionality around modifications
85
+ intro_comment + in_ddl_transaction do
86
+ script_parts = [
87
+ # Check for blatantly incorrect application of script, e.g. running
88
+ # on master or template database.
89
+ :check_execution_environment_sql,
90
+
91
+ # Create schema version control (SVC) tables if they don't exist
92
+ :ensure_version_tables_sql,
93
+
94
+ # Create and fill a temporary table with migration IDs known by
95
+ # the script with order information
96
+ :create_and_fill_migration_table_sql,
97
+
98
+ # Create and fill a temporary table with index information known by
99
+ # the script
100
+ :create_and_fill_indexes_table_sql,
101
+
102
+ # Check that all migrations applied to the database are known to
103
+ # the script (as far back as the most recent "version bridge" record)
104
+ :check_preceding_migrations_sql,
105
+
106
+ # Check that there are no "gaps" in the chain of migrations
107
+ # that have already been applied
108
+ :check_chain_continuity_sql,
109
+
110
+ # Mark migrations in the temporary table that should be installed
111
+ :select_for_install_sql,
112
+
113
+ # Check production configuration of database
114
+ :production_config_check_sql,
115
+
116
+ # Remove all access artifacts
117
+ :remove_access_artifacts_sql,
118
+
119
+ # Remove all undesired indexes
120
+ :remove_undesired_indexes_sql,
121
+
122
+ # Apply a branch upgrade if indicated
123
+ :branch_upgrade_sql,
124
+
125
+ # Apply selected migrations
126
+ :apply_migration_sql,
127
+
128
+ # Create all access artifacts
129
+ :create_access_artifacts_sql,
130
+
131
+ # Create any desired indexes that don't yet exist
132
+ :create_new_indexes_sql,
133
+
134
+ # Any cleanup needed
135
+ :upgrade_cleanup_sql,
136
+ ]
137
+
138
+ amend_script_parts(script_parts)
139
+
140
+ script_parts.map {|mn| self.send(mn)}.flatten.compact.join(ddl_block_separator)
141
+ end
142
+ end
143
+
144
+ def amend_script_parts(parts)
145
+ end
146
+
147
+ def sql_comment_block(text)
148
+ text.lines.collect {|l| '-- ' + l.chomp + "\n"}.join('')
149
+ end
150
+
151
+ def check_working_copy!
152
+ raise VersionControlError, "XMigra source not under version control" if production
153
+ end
154
+
155
+ def create_access_artifacts_sql
156
+ scripts = []
157
+ @access_artifacts.each_definition_sql {|s| scripts << s}
158
+ return scripts unless scripts.empty?
159
+ end
160
+
161
+ def apply_migration_sql
162
+ # Apply selected migrations
163
+ @migrations.collect do |m|
164
+ m.migration_application_sql
165
+ end
166
+ end
167
+
168
+ def branch_upgrade_sql
169
+ end
170
+
171
+ def upgrade_cleanup_sql
172
+ end
173
+
174
+ def vcs_information
175
+ end
176
+
177
+ def each_file_path
178
+ @file_based_groups.each do |group|
179
+ group.each {|item| yield item.file_path}
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,20 @@
1
+
2
+ require 'xmigra/access_artifact'
3
+
4
+ module XMigra
5
+ class StoredProcedure < AccessArtifact
6
+ OBJECT_TYPE = "PROCEDURE"
7
+
8
+ # Construct with a hash (as if loaded from a stored procedure YAML file)
9
+ def initialize(sproc_info)
10
+ @name = sproc_info["name"].dup.freeze
11
+ @definition = sproc_info["sql"].dup.freeze
12
+ end
13
+
14
+ attr_reader :name
15
+
16
+ def depends_on
17
+ []
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,275 @@
1
+
2
+ module XMigra
3
+ module GitSpecifics
4
+ VersionControlSupportModules << self
5
+
6
+ MASTER_HEAD_ATTRIBUTE = 'xmigra-master'
7
+ MASTER_BRANCH_SUBDIR = 'xmigra-master'
8
+
9
+ class << self
10
+ def manages(path)
11
+ run_git(:status, :check_exit=>true)
12
+ end
13
+
14
+ def run_git(subcmd, *args)
15
+ options = (Hash === args[-1]) ? args.pop : {}
16
+ check_exit = options.fetch(:check_exit, false)
17
+ no_result = !options.fetch(:get_result, true)
18
+
19
+ cmd_parts = ["git", subcmd.to_s]
20
+ cmd_parts.concat(
21
+ args.flatten.collect {|a| '""'.insert(1, a.to_s)}
22
+ )
23
+ case PLATFORM
24
+ when :unix
25
+ cmd_parts << "2>/dev/null"
26
+ end if options[:quiet]
27
+
28
+ cmd_str = cmd_parts.join(' ')
29
+
30
+ output = `#{cmd_str}`
31
+ return ($?.success? ? output : nil) if options[:get_result] == :on_success
32
+ return $?.success? if check_exit
33
+ raise(VersionControlError, "Git command failed with exit code #{$?.exitstatus}") unless $?.success?
34
+ return output unless no_result
35
+ end
36
+
37
+ def attr_values(attr, path, options={})
38
+ value_list = run_git('check-attr', attr, '--', path).each_line.map do |line|
39
+ line.chomp.split(/: /, 3)[2]
40
+ end
41
+ return value_list unless options[:single]
42
+ raise VersionControlError, options[:single] + ' ambiguous' if value_list.length > 1
43
+ if (value_list.empty? || value_list == ['unspecified']) && options[:required]
44
+ raise VersionControlError, options[:single] + ' undefined'
45
+ end
46
+ return value_list[0]
47
+ end
48
+ end
49
+
50
+ def git(*args)
51
+ Dir.chdir(self.path) do |pwd|
52
+ GitSpecifics.run_git(*args)
53
+ end
54
+ end
55
+
56
+ def check_working_copy!
57
+ return unless production
58
+
59
+ file_paths = Array.from_generator(method(:each_file_path))
60
+ unversioned_files = git(
61
+ 'diff-index',
62
+ %w{-z --no-commit-id --name-only HEAD},
63
+ '--',
64
+ self.path
65
+ ).split("\000").collect do |path|
66
+ File.expand_path(self.path + path)
67
+ end
68
+
69
+ # Check that file_paths and unversioned_files are disjoint
70
+ unless (file_paths & unversioned_files).empty?
71
+ raise VersionControlError, "Some source files differ from their committed versions"
72
+ end
73
+
74
+ git_fetch_master_branch
75
+ migrations.each do |m|
76
+ # Check that the migration has not changed in the currently checked-out branch
77
+ fpath = m.file_path
78
+
79
+ history = git(:log, %w{--format=%H --}, fpath).split
80
+ if history[1]
81
+ raise VersionControlError, "'#{fpath}' has been modified in the current branch of the repository since its introduction"
82
+ end
83
+ end
84
+
85
+ # Since a production script was requested, warn if we are not generating
86
+ # from a production branch
87
+ if branch_use != :production
88
+ raise VersionControlError, "The working tree is not a commit in the master history."
89
+ end
90
+ end
91
+
92
+ def vcs_information
93
+ return [
94
+ "Branch: #{branch_identifier}",
95
+ "Path: #{git_internal_path}",
96
+ "Commit: #{git_schema_commit}"
97
+ ].join("\n")
98
+ end
99
+
100
+ def branch_identifier
101
+ return (if self.production
102
+ self.git_branch_info[0]
103
+ else
104
+ return @git_branch_identifier if defined? @git_branch_identifier
105
+
106
+ @git_branch_identifier = (
107
+ self.git_master_head(:required=>false) ||
108
+ self.git_local_branch_identifier(:note_modifications=>true)
109
+ )
110
+ end)
111
+ end
112
+
113
+ def branch_use(commit=nil)
114
+ if commit
115
+ self.git_fetch_master_branch
116
+
117
+ # If there are no commits between the master head and *commit*, then
118
+ # *commit* is production-ish
119
+ return (self.git_commits_in? self.git_master_local_branch..commit) ? :development : :production
120
+ end
121
+
122
+ return nil unless self.git_master_head(:required=>false)
123
+
124
+ return self.git_branch_info[1]
125
+ end
126
+
127
+ def vcs_move(old_path, new_path)
128
+ git(:mv, old_path, new_path, :get_result=>false)
129
+ end
130
+
131
+ def vcs_remove(path)
132
+ git(:rm, path, :get_result=>false)
133
+ end
134
+
135
+ def production_pattern
136
+ ".+"
137
+ end
138
+
139
+ def production_pattern=(pattern)
140
+ raise VersionControlError, "Under version control by git, XMigra does not support production patterns."
141
+ end
142
+
143
+ def get_conflict_info
144
+ structure_dir = Pathname.new(self.path) + SchemaManipulator::STRUCTURE_SUBDIR
145
+ head_file = structure_dir + MigrationChain::HEAD_FILE
146
+ stage_numbers = []
147
+ git('ls-files', '-uz', '--', head_file).split("\000").each {|ref|
148
+ if m = /[0-7]{6} [0-9a-f]{40} (\d)\t\S*/.match(ref)
149
+ stage_numbers |= [m[1].to_i]
150
+ end
151
+ }
152
+ return nil unless stage_numbers.sort == [1, 2, 3]
153
+
154
+ chain_head = lambda do |stage_number|
155
+ return YAML.parse(
156
+ git(:show, ":#{stage_number}:#{head_file}")
157
+ ).transform
158
+ end
159
+
160
+ # Ours (2) before theirs (3)...
161
+ heads = [2, 3].collect(&chain_head)
162
+ # ... unless merging from upstream
163
+ if self.git_merging_from_upstream?
164
+ heads.reverse!
165
+ end
166
+
167
+ branch_point = chain_head.call(1)[MigrationChain::LATEST_CHANGE]
168
+
169
+ conflict = MigrationConflict.new(structure_dir, branch_point, heads)
170
+
171
+ # Standard git usage never commits directly to the master branch, and
172
+ # there is no effective way to tell if this is happening.
173
+ conflict.branch_use = :development
174
+
175
+ tool = self
176
+ conflict.after_fix = proc {tool.resolve_conflict!(head_file)}
177
+
178
+ return conflict
179
+ end
180
+
181
+ def resolve_conflict!(path)
182
+ git(:add, '--', path, :get_result=>false)
183
+ end
184
+
185
+ def git_master_head(options={})
186
+ options = {:required=>true}.merge(options)
187
+ return @git_master_head if defined? @git_master_head
188
+ master_head = GitSpecifics.attr_values(
189
+ MASTER_HEAD_ATTRIBUTE,
190
+ self.path + SchemaManipulator::DBINFO_FILE,
191
+ :single=>'Master branch',
192
+ :required=>options[:required]
193
+ )
194
+ return nil if master_head.nil?
195
+ return @git_master_head = master_head
196
+ end
197
+
198
+ def git_branch
199
+ return @git_branch if defined? @git_branch
200
+ return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}).chomp
201
+ end
202
+
203
+ def git_schema_commit
204
+ return @git_commit if defined? @git_commit
205
+ reported_commit = git(:log, %w{-n1 --format=%H --}, self.path).chomp
206
+ raise VersionControlError, "Schema not committed" if reported_commit.empty?
207
+ return @git_commit = reported_commit
208
+ end
209
+
210
+ def git_branch_info
211
+ return @git_branch_info if defined? @git_branch_info
212
+
213
+ self.git_fetch_master_branch
214
+
215
+ # If there are no commits between the master head and HEAD, this working
216
+ # copy is production-ish
217
+ return (@git_branch_info = if self.branch_use('HEAD') == :production
218
+ [self.git_master_head, :production]
219
+ else
220
+ [self.git_local_branch_identifier, :development]
221
+ end)
222
+ end
223
+
224
+ def git_local_branch_identifier(options={})
225
+ host = `hostname`
226
+ path = git('rev-parse', '--show-toplevel')
227
+ return "#{git_branch} of #{path} on #{host} (commit #{git_schema_commit})"
228
+ end
229
+
230
+ def git_fetch_master_branch
231
+ return if @git_master_branch_fetched
232
+ master_url, remote_branch = self.git_master_head.split('#', 2)
233
+
234
+ git(:fetch, '-f', master_url, "#{remote_branch}:#{git_master_local_branch}", :get_result=>false, :quiet=>true)
235
+ @git_master_branch_fetched = true
236
+ end
237
+
238
+ def git_master_local_branch
239
+ "#{MASTER_BRANCH_SUBDIR}/#{git_branch}"
240
+ end
241
+
242
+ def git_internal_path
243
+ return @git_internal_path if defined? @git_internal_path
244
+ path_prefix = git('rev-parse', %w{--show-prefix}).chomp[0..-2]
245
+ internal_path = '.'
246
+ if path_prefix.length > 0
247
+ internal_path += '/' + path_prefix
248
+ end
249
+ return @git_internal_path = internal_path
250
+ end
251
+
252
+ def git_merging_from_upstream?
253
+ upstream = git('rev-parse', '@{u}', :get_result=>:on_success, :quiet=>true)
254
+ return false if upstream.nil?
255
+
256
+ # Check if there are any commits in #{upstream}..MERGE_HEAD
257
+ begin
258
+ return !(self.git_commits_in? upstream..'MERGE_HEAD')
259
+ rescue VersionControlError
260
+ return false
261
+ end
262
+ end
263
+
264
+ def git_commits_in?(range, path=nil)
265
+ git(
266
+ :log,
267
+ '--pretty=format:%H',
268
+ '-1',
269
+ "#{range.begin.strip}..#{range.end.strip}",
270
+ '--',
271
+ path || self.path
272
+ ) != ''
273
+ end
274
+ end
275
+ end