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,927 @@
1
+ require 'xmigra'
2
+
3
+ module XMigra
4
+ class Program
5
+ ILLEGAL_PATH_CHARS = "\"<>:|"
6
+ ILLEGAL_FILENAME_CHARS = ILLEGAL_PATH_CHARS + "/\\"
7
+
8
+ class TerminatingOption < Exception; end
9
+ class ArgumentError < XMigra::Error; end
10
+ module QuietError; end
11
+
12
+ class << self
13
+ def subcommand(name, description, &block)
14
+ (@subcommands ||= {})[name] = block
15
+ (@subcommand_descriptions ||= {})[name] = description
16
+ end
17
+
18
+ # Run the given command line.
19
+ #
20
+ # An array of command line arguments may be given as the only argument
21
+ # or arguments may be given as call parameters. Returns nil if the
22
+ # command completed or a TerminatingOption object if a terminating
23
+ # option (typically "--help") was passed.
24
+ def run(*argv)
25
+ options = (Hash === argv.last) ? argv.pop : {}
26
+ argv = argv[0] if argv.length == 1 && Array === argv[0]
27
+ prev_subcommand = @active_subcommand
28
+ begin
29
+ @active_subcommand = subcmd = argv.shift
30
+
31
+ begin
32
+ if subcmd == "help" || subcmd.nil?
33
+ help(argv)
34
+ return
35
+ end
36
+
37
+ begin
38
+ (@subcommands[subcmd] || method(:show_subcommands_as_help)).call(argv)
39
+ rescue StandardError => error
40
+ raise unless options[:error]
41
+ options[:error].call(error)
42
+ end
43
+ rescue TerminatingOption => stop
44
+ return stop
45
+ end
46
+ ensure
47
+ @active_subcommand = prev_subcommand
48
+ end
49
+ end
50
+
51
+ def help(argv)
52
+ if (argv.length != 1) || (argv[0] == '--help')
53
+ show_subcommands
54
+ return
55
+ end
56
+
57
+ argv << "--help"
58
+ run(argv)
59
+ end
60
+
61
+ def show_subcommands(_1=nil)
62
+ puts
63
+ puts "Use '#{File.basename($0)} help <subcommand>' for help on one of these subcommands:"
64
+ puts
65
+
66
+ descs = @subcommand_descriptions
67
+ cmd_width = descs.enum_for(:each_key).max_by {|i| i.length}.length + 2
68
+ descs.each_pair do |cmd, description|
69
+ printf("%*s - ", cmd_width, cmd)
70
+ description.lines.each_with_index do |line, i|
71
+ indent = if (i > 0)..(i == description.lines.count - 1)
72
+ cmd_width + 3
73
+ else
74
+ 0
75
+ end
76
+ puts(" " * indent + line.chomp)
77
+ end
78
+ end
79
+ end
80
+
81
+ def show_subcommands_as_help(_1=nil)
82
+ show_subcommands
83
+ raise ArgumentError.new("Invalid subcommand").extend(QuietError)
84
+ end
85
+
86
+ def command_line(argv, use, cmdopts = {})
87
+ options = OpenStruct.new
88
+ argument_desc = cmdopts[:argument_desc]
89
+
90
+ optparser = OptionParser.new do |flags|
91
+ subcmd = @active_subcommand || "<subcmd>"
92
+ flags.banner = [
93
+ "Usage: #{File.basename($0)} #{subcmd} [<options>]",
94
+ argument_desc
95
+ ].compact.join(' ')
96
+ flags.banner << "\n\n" + cmdopts[:help].chomp if cmdopts[:help]
97
+
98
+ flags.separator ''
99
+ flags.separator 'Subcommand options:'
100
+
101
+ if use[:target_type]
102
+ options.target_type = :unspecified
103
+ allowed = [:exact, :substring, :regexp]
104
+ flags.on(
105
+ "--by=TYPE", allowed,
106
+ "Specify how TARGETs are matched",
107
+ "against subject strings",
108
+ "(#{allowed.collect {|i| i.to_s}.join(', ')})"
109
+ ) do |type|
110
+ options.target_type = type
111
+ end
112
+ end
113
+
114
+ if use[:dev_branch]
115
+ options.dev_branch = false
116
+ flags.on("--dev-branch", "Favor development branch usage assumption") do
117
+ options.dev_branch = true
118
+ end
119
+ end
120
+
121
+ unless use[:edit].nil?
122
+ options.edit = use[:edit] ? true : false
123
+ flags.banner << "\n\n" << (<<END_OF_HELP).chomp
124
+ When opening an editor, the program specified by the environment variable
125
+ VISUAL is preferred, then the one specified by EDITOR. If neither of these
126
+ environment variables is set no editor will be opened.
127
+ END_OF_HELP
128
+ flags.on("--[no-]edit", "Open the resulting file in an editor",
129
+ "(defaults to #{options.edit})") do |v|
130
+ options.edit = %w{EDITOR VISUAL}.any? {|k| ENV.has_key?(k)} && v
131
+ end
132
+ end
133
+
134
+ if use[:search_type]
135
+ options.search_type = :changes
136
+ allowed = [:changes, :sql]
137
+ flags.on(
138
+ "--match=SUBJECT", allowed,
139
+ "Specify the type of subject against",
140
+ "which TARGETs match",
141
+ "(#{allowed.collect {|i| i.to_s}.join(', ')})"
142
+ ) do |type|
143
+ options.search_type = type
144
+ end
145
+ end
146
+
147
+ if use[:outfile]
148
+ options.outfile = nil
149
+ flags.on("-o", "--outfile=FILE", "Output to FILE") do |fpath|
150
+ options.outfile = File.expand_path(fpath)
151
+ end
152
+ end
153
+
154
+ if use[:production]
155
+ options.production = false
156
+ flags.on("-p", "--production", "Generate script for production databases") do
157
+ options.production = true
158
+ end
159
+ end
160
+
161
+ options.source_dir = Dir.pwd
162
+ flags.on("--source=DIR", "Work from/on the schema in DIR") do |dir|
163
+ options.source_dir = File.expand_path(dir)
164
+ end
165
+
166
+ flags.on_tail("-h", "--help", "Show this message") do
167
+ puts
168
+ puts flags
169
+ raise TerminatingOption.new('--help')
170
+ end
171
+ end
172
+
173
+ argv = optparser.parse(argv)
174
+
175
+ if use[:target_type] && options.target_type == :unspecified
176
+ options.target_type = case options.search_type
177
+ when :changes then :strict
178
+ else :substring
179
+ end
180
+ end
181
+
182
+ return argv, options
183
+ end
184
+
185
+ def output_to(fpath_or_nil)
186
+ if fpath_or_nil.nil?
187
+ yield(STDOUT)
188
+ else
189
+ File.open(fpath_or_nil, "w") do |stream|
190
+ yield(stream)
191
+ end
192
+ end
193
+ end
194
+
195
+ def argument_error_unless(test, message)
196
+ return if test
197
+ raise ArgumentError, XMigra.program_message(message, :cmd=>@active_subcommand)
198
+ end
199
+
200
+ def edit(fpath)
201
+ case
202
+ when (editor = ENV['VISUAL']) && PLATFORM == :mswin
203
+ system(%Q{start #{editor} "#{fpath}"})
204
+ when editor = ENV['VISUAL']
205
+ system(%Q{#{editor} "#{fpath}" &})
206
+ when editor = ENV['EDITOR']
207
+ system(%Q{#{editor} "#{fpath}"})
208
+ end
209
+ end
210
+ end
211
+
212
+ subcommand 'overview', "Explain usage of this tool" do |argv|
213
+ argument_error_unless([[], ["-h"], ["--help"]].include?(argv),
214
+ "'%prog %cmd' does not accept arguments.")
215
+
216
+ formalizations = {
217
+ /xmigra/i=>'XMigra',
218
+ }
219
+
220
+ section = proc do |name, content|
221
+ puts
222
+ puts name
223
+ puts "=" * name.length
224
+ puts XMigra.program_message(
225
+ content,
226
+ :prog=>/%program_cmd\b/
227
+ )
228
+ end
229
+
230
+ puts XMigra.program_message(<<END_HEADER) # Overview
231
+
232
+ ===========================================================================
233
+ # Usage of %program_name
234
+ ===========================================================================
235
+ END_HEADER
236
+
237
+ begin; section['Introduction', <<END_SECTION]
238
+
239
+ %program_name is a tool designed to assist development of software using
240
+ relational databases for persistent storage. During the development cycle, this
241
+ tool helps manage:
242
+
243
+ - Migration of production databases to newer versions, including migration
244
+ between parallel, released versions.
245
+
246
+ - Using transactional scripts, so that unexpected database conditions do not
247
+ lead to corrupt production databases.
248
+
249
+ - Protection of production databases from changes still under development.
250
+
251
+ - Parallel development of features requiring database changes.
252
+
253
+ - Assignment of permissions to database objects.
254
+
255
+ To accomplish this, the database schema to be created is decomposed into
256
+ several parts and formatted in text files according to certain rules. The
257
+ %program_name tool is then used to manipulate, query, or generate scripts from
258
+ the set of files.
259
+ END_SECTION
260
+ end
261
+ begin; section['Schema Files and Folders', <<END_SECTION]
262
+
263
+ SCHEMA (root folder/directory of decomposed schema)
264
+ +-- database.yaml
265
+ +-- permissions.yaml (optional)
266
+ +-- structure
267
+ | +-- head.yaml
268
+ | +-- <migration files>
269
+ | ...
270
+ +-- access
271
+ | +-- <stored procedure definition files>
272
+ | +-- <view definition files>
273
+ | +-- <user defined function definition files>
274
+ | ...
275
+ +-- indexes
276
+ +-- <index definition files>
277
+ ...
278
+
279
+ --------------------------------------------------------------------------
280
+ NOTE: In case-sensitive filesystems, all file and directory names dictated
281
+ by %program_name are lowercase.
282
+ --------------------------------------------------------------------------
283
+
284
+ All data files used by %program_name conform to the YAML 1.0 data format
285
+ specification. Please refer to that specification for information
286
+ on the specifics of encoding particular values. This documentation, at many
287
+ points, makes reference to "sections" of a .yaml file; such a section is,
288
+ technically, an entry in the mapping at the top level of the .yaml file with
289
+ the given key. The simplest understanding of this is that the section name
290
+ followed immediately (no whitespace) by a colon (':') and at least one space
291
+ character appears in the left-most column, and the section contents appear
292
+ either on the line after the colon-space or in an indented block starting on
293
+ the next line (often used with a scalar block indicator ('|' or '>') following
294
+ the colon-space).
295
+
296
+ The decomposed database schema lives within a filesystem subtree rooted at a
297
+ single folder (i.e. directory). For examples in this documentation, that
298
+ folder will be called SCHEMA. Two important files are stored directly in the
299
+ SCHEMA directory: database.yaml and permissions.yaml. The "database.yaml" file
300
+ provides general information about the database for which scripts are to be
301
+ generated. Please see the section below detailing this file's contents for
302
+ more information. The "permissions.yaml" file specifies permissions to be
303
+ granted when generating a permission-granting script (run
304
+ '%program_cmd help permissions' for more information).
305
+
306
+ Within the SCHEMA folder, %program_name expects three other folders: structure,
307
+ access, and indexes.
308
+ END_SECTION
309
+ end
310
+ begin; section['The "SCHEMA/structure" Folder', <<END_SECTION]
311
+
312
+ Every relational database has structures in which it stores the persistent
313
+ data of the related application(s). These database objects are special in
314
+ relation to other parts of the database because they contain information that
315
+ cannot be reproduced just from the schema definition. Yet bug fixes and
316
+ feature additions will need to update this structure and good programming
317
+ practice dictates that such changes, and the functionalities relying on them,
318
+ need to be tested. Testability, in turn, dictates a repeatable sequence of
319
+ actions to be executed on the database starting from a known state.
320
+
321
+ %program_name models the evolution of the persistent data storage structures
322
+ of a database as a chain of "migrations," each of which makes changes to the
323
+ database storage structure from a previous, known state of the database. The
324
+ first migration starts at an empty database and each subsequent migration
325
+ starts where the previous migration left off. Each migration is stored in
326
+ a file within the SCHEMA/structure folder. The names of migration files start
327
+ with a date (YYYY-MM-DD) and include a short description of the change. As
328
+ with other files used by the %program_name tool, migration files are in the
329
+ YAML format, using the ".yaml" extension. Because some set of migrations
330
+ will determine the state of production databases, migrations themselves (as
331
+ used to produce production upgrade scripts) must be "set in stone" -- once
332
+ committed to version control (on a production branch) they must never change
333
+ their content.
334
+
335
+ Migration files are usually generated by running the '%program_cmd new'
336
+ command (see '%program_cmd help new' for more information) and then editing
337
+ the resulting file. The migration file has several sections: "starting from",
338
+ "sql", "changes", and "description". The "starting from" section indicates the
339
+ previous migration in the chain (or "empty database" for the first migration).
340
+ SQL code that effects the migration on the database is the content of the "sql"
341
+ section (usually given as a YAML literal block). The "changes" section
342
+ supports '%program_cmd history', allowing a more user-friendly look at the
343
+ evolution of a subset of the database structure over the migration chain.
344
+ Finally, the "description" section is intended for a prose description of the
345
+ migration, and is included in the upgrade metadata stored in the database. Use
346
+ of the '%program_cmd new' command is recommended; it handles several tiresome
347
+ and error-prone tasks: creating a new migration file with a conformant name,
348
+ setting the "starting from" section to the correct value, and updating
349
+ SCHEMA/structure/head.yaml to reference the newly generated file.
350
+
351
+ The SCHEMA/structure/head.yaml file deserves special note: it contains a
352
+ reference to the last migration to be applied. Because of this, parallel
353
+ development of database changes will cause conflicts in the contents of this
354
+ file. This is by design, and '%program_cmd unbranch' will assist in resolving
355
+ these conflicts.
356
+
357
+ Care must be taken when committing migration files to version control; because
358
+ the structure of production databases will be determined by the chain of
359
+ migrations (starting at an empty database, going up to some certain point),
360
+ it is imperative that migrations used to build these production upgrade scripts
361
+ not be modified once committed to the version control system. When building
362
+ a production upgrade script, %program_name verifies that this constraint is
363
+ followed. Therefore, if the need arises to commit a migration file that may
364
+ require amendment, the best practice is to commit it to a development branch.
365
+
366
+ Migrating a database from one released version (which may receive bug fixes
367
+ or critical feature updates) to another released version which developed along
368
+ a parallel track is generally a tricky endeavor. Please see the section on
369
+ "branch upgrades" below for information on how %program_name supports this
370
+ use case.
371
+ END_SECTION
372
+ end
373
+ begin; section['The "SCHEMA/access" Folder', <<END_SECTION]
374
+
375
+ In addition to the structures that store persistent data, many relational
376
+ databases also support persistent constructs for providing consistent access
377
+ (creation, retrieval, update, and deletion) to the persistent data even as the
378
+ actual storage structure changes, allowing for a degree of backward
379
+ compatibility with applications. These constructs do not, of themselves,
380
+ contain persistent data, but rather specify procedures for accessing the
381
+ persistent data.
382
+
383
+ In %program_name, such constructs are defined in the SCHEMA/access folder, with
384
+ each construct (usually) having its own file. The name of the file must be
385
+ a valid SQL name for the construct defined. The filename may be accessed
386
+ within the definition by the filename metavariable, by default "[{filename}]"
387
+ (without quotation marks); this assists renaming such constructs, making the
388
+ operation of renaming the construct a simple rename of the containing file
389
+ within the filesystem (and version control repository). Use of files in this
390
+ way creates a history of each "access object's" definition in the version
391
+ control system organized by the name of the object.
392
+
393
+ The definition of the access object is given in the "sql" section of the
394
+ definition file, usually with a YAML literal block. This SQL MUST define the
395
+ object for which the containing file is named; failure to do so will result in
396
+ failure of the script when it is run against the database. After deleting
397
+ all access objects previously created by %program_name, the generated script
398
+ first checks that the access object does not exist, then runs the definition
399
+ SQL, and finally checks that the object now exists.
400
+
401
+ In addition to the SQL definition, %program_name needs to know what kind of
402
+ object is to be created by this definition. This information is presented in
403
+ the "define" section, and is currently limited to "function",
404
+ "stored procedure", and "view".
405
+
406
+ Some database management systems enforce a rule that statements defining access
407
+ objects (or at least, some kinds of access objects) may not reference access
408
+ objects that do not yet exist. (A good example is Microsoft SQL Server's rule
409
+ about user defined functions that means a definition for the function A may
410
+ only call the user defined function B if B exists when A is defined.) To
411
+ accommodate this situation, %program_name provides an optional "referencing"
412
+ section in the access object definition file. The content of this section
413
+ must be a YAML sequence of scalars, each of which is the name of an access
414
+ object file (the name must be given the same way the filename is written, not
415
+ just a way that is equivalent in the target SQL language). The scalar values
416
+ must be appropriately escaped as necessary (e.g. Microsoft SQL Server uses
417
+ square brackets as a quotation mechanism, and square brackets have special
418
+ meaning in YAML, so it is necessary use quoted strings or a scalar block to
419
+ contain them). Any access objects listed in this way will be created before
420
+ the referencing object.
421
+ END_SECTION
422
+ end
423
+ begin; section['The "SCHEMA/indexes" Folder', <<END_SECTION]
424
+
425
+ Database indexes vary from the other kinds of definitions supported by
426
+ %program_name: while SCHEMA/structure elements only hold data and
427
+ SCHEMA/access elements are defined entirely by their code in the schema and
428
+ can thus be efficiently re-created, indexes have their whole definition in
429
+ the schema, but store data gleaned from the persistent data. Re-creation of
430
+ an index is an expensive operation that should be avoided when unnecessary.
431
+
432
+ To accomplish this end, %program_name looks in the SCHEMA/indexes folder for
433
+ index definitions. The generated scripts will drop and (re-)create only
434
+ indexes whose definitions are changed. %program_name uses a very literal
435
+ comparison of the SQL text used to create the index to determine "change;"
436
+ even so much as a single whitespace added or removed, even if insignificant to
437
+ the database management system, will be enough to cause the index to be dropped
438
+ and re-created.
439
+
440
+ Index definition files use only the "sql" section to provide the SQL definition
441
+ of the index. Index definitions do not support use of the filename
442
+ metavariable because renaming an index would cause it to be dropped and
443
+ re-created.
444
+ END_SECTION
445
+ end
446
+ begin; section['The "SCHEMA/database.yaml" File', <<END_SECTION]
447
+
448
+ The SCHEMA/database.yaml file consists of several sections that provide general
449
+ information about the database schema. The following subsection detail some
450
+ contents that may be included in this file.
451
+
452
+ system
453
+ ------
454
+
455
+ The "system" section specifies for %program_name which database management
456
+ system shall be targeted for the generation of scripts. Currently the
457
+ supported values are:
458
+
459
+ - Microsoft SQL Server
460
+
461
+ Each system can also have sub-settings that modify the generated scripts.
462
+
463
+ Microsoft SQL Server:
464
+ The "MSSQL 2005 compatible" setting in SCEMA/database.yaml, if set to
465
+ "true", causes INSERT statements to be generated in a more verbose and
466
+ SQL Server 2005 compatible manner.
467
+
468
+ Also, each system may modify in other ways the behavior of the generator or
469
+ the interpretation of the definition files:
470
+
471
+ Microsoft SQL Server:
472
+ The SQL in the definition files may use the "GO" metacommand also found in
473
+ Microsoft SQL Server Management Studio and sqlcmd.exe. This metacommand
474
+ must be on a line by itself where used. It should produce the same results
475
+ as it would in MSSMS or sqlcmd.exe, except that the overall script is
476
+ transacted.
477
+
478
+ script comment
479
+ --------------
480
+
481
+ The "script comment" section defines a body of SQL to be inserted at the top
482
+ of all generated scripts. This is useful for including copyright information
483
+ in the resulting SQL.
484
+
485
+ filename metavariable
486
+ ---------------------
487
+
488
+ The "filename metavariable" section allows the schema to override the filename
489
+ metavariable that is used for access object definitions. The default value
490
+ is "[{filename}]" (excluding the quotation marks). If that string is required
491
+ in one or more access object definitions, this section allows the schema to
492
+ dictate another value.
493
+ END_SECTION
494
+ end
495
+ begin; section['Script Generation Modes', <<END_SECTION]
496
+
497
+ %program_name supports two modes of upgrade script creation: development and
498
+ production. (Permission script generation is not constrained to these modes.)
499
+ Upgrade script generation defaults to development mode, which does less
500
+ work to generate a script and skips tests intended to ensure that the script
501
+ could be generated again at some future point from the contents of the version
502
+ control repository. The resulting script can only be run on an empty database
503
+ or a database that was set up with a development mode script earlier on the
504
+ same migration chain; running a development mode script on a database created
505
+ with a production script fails by design (preventing a production database from
506
+ undergoing a migration that has not been duly recorded in the version control
507
+ system). Development scripts have a big warning comment at the beginning of
508
+ the script as a reminder that they are not suitable to use on a production
509
+ system.
510
+
511
+ Use of the '--production' flag with the '%program_cmd upgrade' command
512
+ enables production mode, which carries out a significant number of
513
+ additional checks. These checks serve two purposes: making sure that all
514
+ migrations to be applied to a production database are recorded in the version
515
+ control system and that all of the definition files in the whole schema
516
+ represent a single, coherent point in the version history (i.e. all files are
517
+ from the same revision). Where a case arises that a script needs to be
518
+ generated that cannot meet these two criteria, it is almost certainly a case
519
+ that calls for a development script. There is always the option of creating
520
+ a new production branch and committing the %program_name schema files to that
521
+ branch if a production script is needed, thus meeting the criteria of the test.
522
+
523
+ Note that "development mode" and "production mode" are not about the quality
524
+ of the scripts generated or the "build mode" of the application that may access
525
+ the resulting database, but rather about the designation of the database
526
+ to which the generated scripts may be applied. "Production" scripts certainly
527
+ should be tested in a non-production environment before they are applied to
528
+ a production environment with irreplaceable data. But "development" scripts,
529
+ by design, can never be run on production systems (so that the production
530
+ systems only move from one well-documented state to another).
531
+ END_SECTION
532
+ end
533
+ begin; section['Branch Upgrades', <<END_SECTION]
534
+
535
+ Maintaining a single, canonical chain of database schema migrations released to
536
+ customers dramatically reduces the amount of complexity inherent in
537
+ same-version product upgrades. But expecting development and releases to stay
538
+ on a single migration chain is overly optimistic; there are many circumstances
539
+ that lead to divergent migration chains across instances of the database. A
540
+ bug fix on an already released version or some emergency feature addition for
541
+ an enormous profit opportunity are entirely possible, and any database
542
+ evolution system that breaks under those requirements will be an intolerable
543
+ hinderance.
544
+
545
+ %program_name supports these situations through the mechanism of "branch
546
+ upgrades." A branch upgrade is a migration containing SQL commands to effect
547
+ the conversion of the head of the current branch's migration chain (i.e. the
548
+ state of the database after the most recent migration in the current branch)
549
+ into some state along another branch's migration chain. These commands are not
550
+ applied when the upgrade script for this branch is run. Rather, they are saved
551
+ in the database and run only if, and then prior to, an upgrade script for the
552
+ targeted branch is run against the same database.
553
+
554
+ Unlike regular migrations, changes to the branch upgrade migration MAY be
555
+ committed to the version control repository.
556
+
557
+ ::::::::::::::::::::::::::::::::::: EXAMPLE :::::::::::::::::::::::::::::::::::
558
+ :: ::
559
+
560
+ Product X is about to release version 2.0. Version 1.4 of Product X has
561
+ been in customers' hands for 18 months and seven bug fixes involving database
562
+ structure changes have been implemented in that time. Our example company has
563
+ worked out the necessary SQL commands to convert the current head of the 1.4
564
+ migration chain to the same resulting structure as the head of the 2.0
565
+ migration chain. Those SQL commands, and the appropriate metadata about the
566
+ target branch (version 2.0), the completed migration (the one named as the head
567
+ of the 2.0 branch), and the head of the 1.4 branch are all put into the
568
+ SCHEMA/structure/branch-upgrade.yaml file as detailed below. %program_name
569
+ can then script the storage of these commands into the database for execution
570
+ by a Product X version 2.0 upgrade script.
571
+
572
+ Once the branch-upgrade.yaml file is created and committed to version control
573
+ in the version 1.4 branch, two upgrade scripts need to be generated: a
574
+ version 1.4 upgrade script and a version 2.0 upgrade script, each from its
575
+ respective branch in version control. For each version 1.4 installation, the
576
+ 1.4 script will first be run, bringing the installation up to the head of
577
+ version 1.4 and installing the instructions for upgrading to a version 2.0
578
+ database. Then the version 2.0 script will be run, which will execute the
579
+ stored instructions for bringing the database from version 1.4 to version 2.0.
580
+
581
+ Had the branch upgrade not brought the version 1.4 database all the way up to
582
+ the head of version 2.0 (i.e. if the YAML file indicates a completed migration
583
+ prior to the version 2.0 head), the version 2.0 script would then apply any
584
+ following migrations in order to bring the database up to the version 2.0 head.
585
+
586
+ :: ::
587
+ :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
588
+
589
+ In addition to the "sql" section, which has the same function as the "sql"
590
+ section of a regular migration, the SCHEMA/structure/branch-upgrade.yaml file
591
+ has three other required sections.
592
+
593
+ starting from
594
+ -------------
595
+
596
+ As with regular migrations, the branch-upgrade.yaml specifies a "starting from"
597
+ point, which should always be the head migration of the current branch (see
598
+ SCHEMA/structure/head.yaml). If this section does not match with the latest
599
+ migration in the migration chain, no branch upgrade information will be
600
+ included in the resulting upgrade script and a warning will be issued during
601
+ script generation. This precaution prevents out-of-date branch upgrade
602
+ commands from being run.
603
+
604
+ resulting branch
605
+ ----------------
606
+
607
+ The "resulting branch" section must give the identifier of the branch
608
+ containing the migration chain into which the included SQL commands will
609
+ migrate the current chain head. This identifier can be obtained with the
610
+ '%program_cmd branchid' command run against a working copy of the
611
+ target branch.
612
+
613
+ completes migration to
614
+ ----------------------
615
+
616
+ Because the migration chain of the target branch is likely to be extended over
617
+ time, it is necessary to pin down the intended result state of the branch
618
+ upgrade to one particular migration in the target chain. The migration name
619
+ listed in the "completes migration to" section should be the name of the
620
+ migration (the file basename) which brings the target branch database to the
621
+ same state as the head state of the current branch after applying the
622
+ branch upgrade commands.
623
+ END_SECTION
624
+ end
625
+ puts
626
+ end
627
+
628
+ subcommand 'new', "Create a new migration file" do |argv|
629
+ args, options = command_line(argv, {:edit=>true},
630
+ :argument_desc=>"MIGRATION_SUMMARY",
631
+ :help=> <<END_OF_HELP)
632
+ This command generates a new migration file and ties it into the current
633
+ migration chain. The name of the new file is generated from today's date and
634
+ the given MIGRATION_SUMMARY. The resulting new file may be opened in an
635
+ editor (see the --[no-]edit option).
636
+ END_OF_HELP
637
+
638
+ argument_error_unless(args.length == 1,
639
+ "'%prog %cmd' takes one argument.")
640
+ migration_summary = args[0]
641
+ argument_error_unless(
642
+ migration_summary.chars.all? {|c| !ILLEGAL_FILENAME_CHARS.include?(c)},
643
+ "Migration summary may not contain any of: " + ILLEGAL_FILENAME_CHARS
644
+ )
645
+
646
+ tool = NewMigrationAdder.new(options.source_dir).extend(WarnToStderr)
647
+ new_fpath = tool.add_migration(migration_summary)
648
+
649
+ edit(new_fpath) if options.edit
650
+ end
651
+
652
+ subcommand 'upgrade', "Generate an upgrade script" do |argv|
653
+ args, options = command_line(argv, {:production=>true, :outfile=>true},
654
+ :help=> <<END_OF_HELP)
655
+ Running this command will generate an update script from the source schema.
656
+ Generation of a production script involves more checks on the status of the
657
+ schema source files but produces a script that may be run on a development,
658
+ production, or empty database. If the generated script is not specified for
659
+ production it may only be run on a development or empty database; it will not
660
+ run on production databases.
661
+ END_OF_HELP
662
+
663
+ argument_error_unless(args.length == 0,
664
+ "'%prog %cmd' does not take any arguments.")
665
+
666
+ sql_gen = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
667
+ sql_gen.production = options.production
668
+
669
+ output_to(options.outfile) do |out_stream|
670
+ out_stream.print(sql_gen.update_sql)
671
+ end
672
+ end
673
+
674
+ subcommand 'render', "Generate SQL script for an access object" do |argv|
675
+ args, options = command_line(argv, {:outfile=>true},
676
+ :argument_desc=>"ACCESS_OBJECT_FILE",
677
+ :help=> <<END_OF_HELP)
678
+ This command outputs the creation script for the access object defined in the
679
+ file at path ACCESS_OBJECT_FILE, making any substitutions in the same way as
680
+ the update script generator.
681
+ END_OF_HELP
682
+
683
+ argument_error_unless(args.length == 1,
684
+ "'%prog %cmd' takes one argument.")
685
+ argument_error_unless(File.exist?(args[0]),
686
+ "'%prog %cmd' must target an existing access object definition.")
687
+
688
+ sql_gen = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
689
+
690
+ artifact = sql_gen.access_artifacts[args[0]] || sql_gen.access_artifacts.at_path(args[0])
691
+ output_to(options.outfile) do |out_stream|
692
+ out_stream.print(artifact.creation_sql)
693
+ end
694
+ end
695
+
696
+ subcommand 'unbranch', "Fix a branched migration chain" do |argv|
697
+ args, options = command_line(argv, {:dev_branch=>true},
698
+ :help=> <<END_OF_HELP)
699
+ Use this command to fix a branched migration chain. The need for this command
700
+ usually arises when code is pulled from the repository into the working copy.
701
+
702
+ Because this program checks that migration files are unaltered when building
703
+ a production upgrade script it is important to use this command only:
704
+
705
+ A) after updating in any branch, or
706
+
707
+ B) after merging into (including synching) a development branch
708
+
709
+ If used in case B, the resulting changes may make the migration chain in the
710
+ current branch ineligible for generating production upgrade scripts. When the
711
+ development branch is (cleanly) merged back to the production branch it will
712
+ still be possible to generate a production upgrade script from the production
713
+ branch. In case B the resulting script (generated in development mode) should
714
+ be thoroughly tested.
715
+
716
+ Because of the potential danger to a production branch, this command checks
717
+ the branch usage before executing. Inherent branch usage can be set through
718
+ the 'productionpattern' command. If the target working copy has not been
719
+ marked with a production-branch pattern, the branch usage is ambiguous and
720
+ this command makes the fail-safe assumption that the branch is used for
721
+ production. This assumption can be overriden with the --dev-branch option.
722
+ Note that the --dev-branch option will NOT override the production-branch
723
+ pattern if one exists.
724
+ END_OF_HELP
725
+
726
+ argument_error_unless(args.length == 0,
727
+ "'%prog %cmd' does not take any arguments.")
728
+
729
+ tool = SchemaManipulator.new(options.source_dir).extend(WarnToStderr)
730
+ conflict = tool.get_conflict_info
731
+
732
+ unless conflict
733
+ STDERR.puts("No conflict!")
734
+ return
735
+ end
736
+
737
+ if conflict.scope == :repository
738
+ if conflict.branch_use == :production
739
+ STDERR.puts(<<END_OF_MESSAGE)
740
+
741
+ The target working copy is on a production branch. Because fixing the branched
742
+ migration chain would require modifying a committed migration, this operation
743
+ would result in a migration chain incapable of producing a production upgrade
744
+ script, leaving this branch unable to fulfill its purpose.
745
+
746
+ END_OF_MESSAGE
747
+ raise(XMigra::Error, "Branch use conflict")
748
+ end
749
+
750
+ dev_branch = (conflict.branch_use == :development) || options.dev_branch
751
+
752
+ unless dev_branch
753
+ STDERR.puts(<<END_OF_MESSAGE)
754
+
755
+ The target working copy is neither marked for production branch recognition
756
+ nor was the --dev-branch option given on the command line. Because fixing the
757
+ branched migration chain would require modifying a committed migration, this
758
+ operation would result in a migration chain incapable of producing a production
759
+ upgrage which, because the usage of the working copy's branch is ambiguous,
760
+ might leave this branch unable to fulfill its purpose.
761
+
762
+ END_OF_MESSAGE
763
+ raise(XMigra::Error, "Potential branch use conflict")
764
+ end
765
+
766
+ if conflict.branch_use == :undefined
767
+ STDERR.puts(<<END_OF_MESSAGE)
768
+
769
+ The branch of the target working copy is not marked with a production branch
770
+ recognition pattern. The --dev-branch option was given to override the
771
+ ambiguity, but it is much safer to use the 'productionpattern' command to
772
+ permanently mark the schema so that production branches can be automatically
773
+ recognized.
774
+
775
+ END_OF_MESSAGE
776
+ # Warning, not error
777
+ end
778
+ end
779
+
780
+ conflict.fix_conflict!
781
+ end
782
+
783
+ subcommand 'productionpattern', "Set the recognition pattern for production branches" do |argv|
784
+ args, options = command_line(argv, {},
785
+ :argument_desc=>"PATTERN",
786
+ :help=> <<END_OF_HELP)
787
+ This command sets the production branch recognition pattern for the schema.
788
+ The pattern given will determine whether this program treats the current
789
+ working copy as a production or development branch. The PATTERN given
790
+ is a Ruby Regexp that is used to evaluate the branch identifier of the working
791
+ copy. Each supported version control system has its own type of branch
792
+ identifier:
793
+
794
+ Subversion: The path within the repository, starting with a slash (e.g.
795
+ "/trunk", "/branches/my-branch", "/foo/bar%20baz")
796
+
797
+ If PATTERN matches the branch identifier, the branch is considered to be a
798
+ production branch. If PATTERN does not match, then the branch is a development
799
+ branch. Some operations (e.g. 'unbranch') are prevented on production branches
800
+ to avoid making the branch ineligible for generating production upgrade
801
+ scripts.
802
+
803
+ In specifying PATTERN, it is not necessary to escape Ruby special characters
804
+ (especially including the slash character), but special characters for the
805
+ shell or command interpreter need their usual escaping. The matching algorithm
806
+ used for PATTERN does not require the match to start at the beginning of the
807
+ branch identifier; specify the anchor as part of PATTERN if desired.
808
+ END_OF_HELP
809
+
810
+ argument_error_unless(args.length == 1,
811
+ "'%prog %cmd' takes one argument.")
812
+ Regexp.compile(args[0])
813
+
814
+ tool = SchemaManipulator.new(options.source_dir).extend(WarnToStderr)
815
+
816
+ tool.production_pattern = args[0]
817
+ end
818
+
819
+ subcommand 'branchid', "Print the branch identifier string" do |argv|
820
+ args, options = command_line(argv, {},
821
+ :help=> <<END_OF_HELP)
822
+ This command prints the branch identifier string to standard out (followed by
823
+ a newline).
824
+ END_OF_HELP
825
+
826
+ argument_error_unless(args.length == 0,
827
+ "'%prog %cmd' does not take any arguments.")
828
+
829
+ tool = SchemaManipulator.new(options.source_dir).extend(WarnToStderr)
830
+
831
+ puts tool.branch_identifier
832
+ end
833
+
834
+ subcommand 'history', "Show all SQL from migrations changing the target" do |argv|
835
+ args, options = command_line(argv, {:outfile=>true, :target_type=>true, :search_type=>true},
836
+ :argument_desc=>"TARGET [TARGET [...]]",
837
+ :help=> <<END_OF_HELP)
838
+ Use this command to get the SQL run by the upgrade script that modifies any of
839
+ the specified TARGETs. By default this command uses a full item match against
840
+ the contents of each item in each migration's "changes" key (i.e.
841
+ --by=exact --match=changes). Migration SQL is printed in order of application
842
+ to the database.
843
+ END_OF_HELP
844
+
845
+ argument_error_unless(args.length >= 1,
846
+ "'%prog %cmd' requires at least one argument.")
847
+
848
+ target_matches = case options.target_type
849
+ when :substring
850
+ proc {|subject| args.any? {|a| subject.include?(a)}}
851
+ when :regexp
852
+ patterns = args.map {|a| Regexp.compile(a)}
853
+ proc {|subject| patterns.any? {|pat| pat.match(subject)}}
854
+ else
855
+ targets = Set.new(args)
856
+ proc {|subject| targets.include?(subject)}
857
+ end
858
+
859
+ criteria_met = case options.search_type
860
+ when :sql
861
+ proc {|migration| target_matches.call(migration.sql)}
862
+ else
863
+ proc {|migration| migration.changes.any? {|subject| target_matches.call(subject)}}
864
+ end
865
+
866
+ tool = SchemaUpdater.new(options.source_dir).extend(WarnToStderr)
867
+
868
+ output_to(options.outfile) do |out_stream|
869
+ tool.migrations.each do |migration|
870
+ next unless criteria_met.call(migration)
871
+
872
+ out_stream << tool.sql_comment_block(File.basename(migration.file_path))
873
+ out_stream << migration.sql
874
+ out_stream << "\n" << tool.batch_separator if tool.respond_to? :batch_separator
875
+ out_stream << "\n"
876
+ end
877
+ end
878
+ end
879
+
880
+ subcommand 'permissions', "Generate a permission assignment script" do |argv|
881
+ args, options = command_line(argv, {:outfile=>true},
882
+ :help=> <<END_OF_HELP)
883
+ This command generates and outputs a script that assigns permissions within
884
+ a database instance. The permission information is read from the
885
+ permissions.yaml file in the schema root directory (the same directory in which
886
+ database.yaml resides) and has the format:
887
+
888
+ dbo.MyTable:
889
+ Alice: SELECT
890
+ Bob:
891
+ - SELECT
892
+ - INSERT
893
+
894
+ (More specifically: The top-level object is a mapping whose scalar keys are
895
+ the names of the objects to be modified and whose values are mappings from
896
+ security principals to either a single permission or a sequence of
897
+ permissions.) The file is in YAML format; use quoted strings if necessary
898
+ (e.g. for Microsoft SQL Server "square bracket escaping", enclose the name in
899
+ single or double quotes within the permissions.yaml file to avoid
900
+ interpretation of the square brackets as delimiting a sequence).
901
+
902
+ Before establishing the permissions listed in permissions.yaml, the generated
903
+ script first removes any permissions previously granted through use of an
904
+ XMigra permissions script. To accomplish this, the script establishes a table
905
+ if it does not yet exist. The code for this precedes the code to remove
906
+ previous permissions. Thus, the resulting script has the sequence:
907
+
908
+ - Establish permission tracking table (if not present)
909
+ - Revoke all previously granted permissions (only those granted
910
+ by a previous XMigra script)
911
+ - Grant permissions indicated in permissions.yaml
912
+
913
+ To facilitate review of the script, the term "GRANT" is avoided except for
914
+ the statements granting the permissions laid out in the source file.
915
+ END_OF_HELP
916
+
917
+ argument_error_unless(args.length == 0,
918
+ "'%prog %cmd' does not take any arguments.")
919
+
920
+ sql_gen = PermissionScriptWriter.new(options.source_dir).extend(WarnToStderr)
921
+
922
+ output_to(options.outfile) do |out_stream|
923
+ out_stream.print(sql_gen.permissions_sql)
924
+ end
925
+ end
926
+ end
927
+ end