schema-evolution-manager 0.9.24

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ module SchemaEvolutionManager
2
+
3
+ # Represents a single file to make a schema migration, typically
4
+ # stored in the scripts subdirectory.
5
+ class MigrationFile
6
+
7
+ class Attribute
8
+
9
+ attr_reader :name, :valid_values
10
+
11
+ def initialize(name, valid_values)
12
+ @name = Preconditions.check_not_blank(name, "name cannot be blank")
13
+ @valid_values = Preconditions.assert_class(valid_values, Array)
14
+ end
15
+
16
+ unless defined?(ATTRIBUTES)
17
+ ATTRIBUTES = [Attribute.new("transaction", ["single", "none"])]
18
+ end
19
+
20
+ end
21
+
22
+ class AttributeValue
23
+
24
+ attr_reader :attribute, :value
25
+
26
+ def initialize(attribute_name, value)
27
+ Preconditions.assert_class(attribute_name, String)
28
+ @value = Preconditions.assert_class(value, String)
29
+
30
+ @attribute = Attribute::ATTRIBUTES.find { |a| a.name == attribute_name }
31
+ Preconditions.check_not_null(@attribute,
32
+ "Attribute with name[%s] not found. Must be one of: %s" %
33
+ [attribute_name,
34
+ Attribute::ATTRIBUTES.map { |a| a.name }.join(" ")])
35
+
36
+ Preconditions.check_state(@attribute.valid_values.include?(@value),
37
+ "Attribute[%s] - Invalid value[%s]. Must be one of: %s" %
38
+ [@attribute.name, @value, @attribute.valid_values.join(" ")])
39
+ end
40
+
41
+ end
42
+
43
+ unless defined?(DEFAULTS)
44
+ DEFAULTS = [AttributeValue.new("transaction", "single")]
45
+ end
46
+
47
+ attr_reader :path, :attribute_values
48
+
49
+ def initialize(path)
50
+ @path = path
51
+ Preconditions.check_state(File.exists?(@path), "File[#{@path}] does not exist")
52
+ @attribute_values = parse_attribute_values
53
+ end
54
+
55
+ private
56
+
57
+ # Returns a list of AttributeValues from the file itself,
58
+ # including all defaults set by SEM. AttributeValues are defined
59
+ # in comments in the file.
60
+ def parse_attribute_values
61
+ values = []
62
+ each_property do |name, value|
63
+ values << AttributeValue.new(name, value)
64
+ end
65
+
66
+ DEFAULTS.each do |default|
67
+ if values.find { |v| v.attribute.name == default.attribute.name }.nil?
68
+ values << default
69
+ end
70
+ end
71
+
72
+ values
73
+ end
74
+
75
+ # Parse properties from the comments. Looks for this pattern:
76
+ #
77
+ # -- sem.attribute.name = value
78
+ #
79
+ # and yields each matching row with |name, value|
80
+ def each_property
81
+ IO.readlines(path).each do |l|
82
+ stripped = l.strip
83
+ if stripped.match(/^\-\-\s+sem\.attribute\./)
84
+ stripped.sub!(/^\-\-\s+sem\.attribute\./, '')
85
+ name, value = stripped.split(/\=/, 2).map(&:strip)
86
+ yield name, value
87
+ end
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,61 @@
1
+ module SchemaEvolutionManager
2
+ # Modeled after http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/base/Preconditions.html
3
+ class Preconditions
4
+
5
+ def Preconditions.check_argument(expression, error_message=nil)
6
+ if !expression
7
+ raise error_message || "check_argument failed"
8
+ end
9
+ nil
10
+ end
11
+
12
+ def Preconditions.check_state(expression, error_message=nil)
13
+ if !expression
14
+ raise error_message || "check_state failed"
15
+ end
16
+ nil
17
+ end
18
+
19
+ def Preconditions.check_not_null(reference, error_message=nil)
20
+ if reference.nil?
21
+ raise error_message || "argument cannot be nil"
22
+ end
23
+ reference
24
+ end
25
+
26
+ def Preconditions.check_not_blank(reference, error_message=nil)
27
+ if reference.to_s.strip == ""
28
+ raise error_message || "argument cannot be blank"
29
+ end
30
+ reference
31
+ end
32
+
33
+ # Throws an error if opts is not empty. Useful when parsing
34
+ # arguments to a function
35
+ def Preconditions.assert_empty_opts(opts)
36
+ if !opts.empty?
37
+ raise "Invalid opts: #{opts.keys.inspect}\n#{opts.inspect}"
38
+ end
39
+ end
40
+
41
+ # Asserts that value is not nill and is_?(klass). Returns
42
+ # value. Common use is
43
+ #
44
+ # amount = Preconditions.assert_class(amount, BigDecimal)
45
+ def Preconditions.assert_class(value, klass)
46
+ Preconditions.check_not_null(value, "Value cannot be nil")
47
+ Preconditions.check_not_null(klass, "Klass cannot be nil")
48
+ Preconditions.check_state(value.is_a?(klass),
49
+ "Value is of type[#{value.class}] - class[#{klass}] is required. value[#{value.inspect.to_s}]")
50
+ value
51
+ end
52
+
53
+ def Preconditions.assert_class_or_nil(value, klass)
54
+ if !value.nil?
55
+ Preconditions.assert_class(value, klass)
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,37 @@
1
+ module SchemaEvolutionManager
2
+ # Adapter for what we need to remove dependency on RDoc which was removed in ruby 1.9
3
+ class RdocUsage
4
+
5
+ def RdocUsage.message
6
+ path = Library.normalize_path(RdocUsage.program_name)
7
+
8
+ lines = []
9
+
10
+ IO.readlines(path).each do |line|
11
+ if !line.match(/^\#/)
12
+ break
13
+ end
14
+ if line.match(/^\#\!/)
15
+ next
16
+ end
17
+ lines << line.sub(/^\#\s?/, '').sub(/\n$/, '')
18
+ end
19
+ lines << ""
20
+
21
+ lines.join("\n")
22
+ end
23
+
24
+ def RdocUsage.printAndExit(exit_code=0)
25
+ puts RdocUsage.message
26
+ exit(exit_code)
27
+ end
28
+
29
+ private
30
+ def RdocUsage.program_name
31
+ $0
32
+ end
33
+
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,19 @@
1
+ module SchemaEvolutionManager
2
+
3
+ class ScriptError < Exception
4
+
5
+ attr_reader :filename
6
+
7
+ def initialize(db, filename)
8
+ @db = Preconditions.assert_class(db, Db)
9
+ @filename = Preconditions.assert_class(filename, String)
10
+ end
11
+
12
+ def dml
13
+ sql_command = "insert into %s.%s (filename) values ('%s')" % [Db.schema_name, Scripts::SCRIPTS, filename]
14
+ "psql --command \"%s\" %s" % [sql_command, @db.url]
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,95 @@
1
+ module SchemaEvolutionManager
2
+
3
+ class Scripts
4
+
5
+ if !defined?(SCRIPTS)
6
+ SCRIPTS = "scripts"
7
+ BOOTSTRAP_SCRIPTS = "bootstrap_scripts"
8
+ VALID_TABLE_NAMES = [BOOTSTRAP_SCRIPTS, SCRIPTS]
9
+ end
10
+
11
+ # @param db Instance of Db class
12
+ # @param table_name Name of the table used to record which scripts
13
+ # we have processed. Will be one of 'scripts' or 'bootstrap_scripts'
14
+ def initialize(db, table_name)
15
+ @db = Preconditions.assert_class(db, Db)
16
+ @table_name = Preconditions.assert_class(table_name, String)
17
+ Preconditions.check_state(VALID_TABLE_NAMES.include?(@table_name),
18
+ "Invalid table name[%s]. Must be one of: %s" % [@table_name, VALID_TABLE_NAMES.join(", ")])
19
+ end
20
+
21
+ # Returns a sorted list of the full file paths to any sql scripts in
22
+ # the specified directory
23
+ def Scripts.all(dir)
24
+ Preconditions.assert_class(dir, String)
25
+
26
+ if File.directory?(dir)
27
+ Dir.glob("#{dir}/*.sql").sort
28
+ else
29
+ []
30
+ end
31
+ end
32
+
33
+ # For each sql script that needs to be applied to this database,
34
+ # yields a pair of |filename, fullpath| in proper order
35
+ #
36
+ # db = Db.new(host, user, name)
37
+ # scripts = Scripts.new(db)
38
+ # scripts.each_pending do |filename, path|
39
+ # puts filename
40
+ # end
41
+ def each_pending(dir)
42
+ files = {}
43
+ Scripts.all(dir).each do |path|
44
+ name = File.basename(path)
45
+ files[name] = path
46
+ end
47
+
48
+ scripts_previously_run(files.keys).each do |filename|
49
+ files.delete(filename)
50
+ end
51
+
52
+ files.keys.sort.each do |filename|
53
+ ## We have to recheck if this script is still pending. Some
54
+ ## upgrade scripts may modify the scripts table themselves. This
55
+ ## is actually useful in cases like when we migrated gilt from
56
+ ## util_schema => schema_evolution_manager schema
57
+ if !has_run?(filename)
58
+ yield filename, files[filename]
59
+ end
60
+ end
61
+ end
62
+
63
+ # True if this script has already been applied to the db. False
64
+ # otherwise.
65
+ def has_run?(filename)
66
+ if @db.schema_schema_evolution_manager_exists?
67
+ query = "select count(*) from %s.%s where filename = '%s'" % [Db.schema_name, @table_name, filename]
68
+ @db.psql_command(query).to_i > 0
69
+ else
70
+ false
71
+ end
72
+ end
73
+
74
+ # Inserts a record to indiciate that we have loaded the specified file.
75
+ def record_as_run!(filename)
76
+ Preconditions.check_state(filename.match(/^\d\d\d\d\d\d+\-\d\d\d\d\d\d\.sql$/),
77
+ "Invalid filename[#{filename}]. Must be like: 20120503-173242.sql")
78
+ command = "insert into %s.%s (filename) select '%s' where not exists (select 1 from %s.%s where filename = '%s')" % [Db.schema_name, @table_name, filename, Db.schema_name, @table_name, filename]
79
+ @db.psql_command(command)
80
+ end
81
+
82
+ private
83
+ # Fetch the list of scripts that have already been applied to this
84
+ # database.
85
+ def scripts_previously_run(scripts)
86
+ if scripts.empty? || !@db.schema_schema_evolution_manager_exists?
87
+ []
88
+ else
89
+ sql = "select filename from %s.%s where filename in (%s)" % [Db.schema_name, @table_name, "'" + scripts.join("', '") + "'"]
90
+ @db.psql_command(sql).strip.split
91
+ end
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,72 @@
1
+ module SchemaEvolutionManager
2
+
3
+ module SemInfo
4
+
5
+ def SemInfo.version(args=nil)
6
+ SchemaEvolutionManager::SemVersion::VERSION.dup
7
+ end
8
+
9
+ def SemInfo.tag(args)
10
+ valid = ['exists', 'latest', 'next']
11
+
12
+ subcommand = args.shift.to_s.strip
13
+
14
+ if subcommand == "exists"
15
+ tag = args.shift.to_s.strip
16
+ if tag.empty?
17
+ puts "ERROR: Missing tag."
18
+ exit(3)
19
+ elsif ::SchemaEvolutionManager::Library.tag_exists?(tag)
20
+ puts "true"
21
+ else
22
+ puts "false"
23
+ end
24
+
25
+ elsif subcommand == "latest"
26
+ if latest = ::SchemaEvolutionManager::SemInfo::Tag.latest
27
+ latest.to_version_string
28
+ else
29
+ nil
30
+ end
31
+
32
+ elsif subcommand == "next"
33
+ ::SchemaEvolutionManager::SemInfo::Tag.next(args).to_version_string
34
+
35
+ elsif subcommand.empty?
36
+ puts "ERROR: Missing tag subcommand. Must be one of: %s" % valid.join(", ")
37
+ exit(3)
38
+
39
+ else
40
+ puts "ERROR: Invalid tag subcommand[%s]. Must be one of: %s" % [subcommand, valid.join(", ")]
41
+ exit(4)
42
+ end
43
+ end
44
+
45
+ module Tag
46
+
47
+ def Tag.latest
48
+ Library.latest_tag || Version.new("0.0.0")
49
+ end
50
+
51
+ # @param component: One of major|minor|micro. Defaults to micro. Currently passed in as an array
52
+ def Tag.next(args=nil)
53
+ component = (args || []).first
54
+ valid = ['micro', 'minor', 'major']
55
+
56
+ if component.to_s.empty?
57
+ component = "micro"
58
+ end
59
+
60
+ if valid.include?(component)
61
+ latest.send("next_%s" % component)
62
+ else
63
+ puts "ERROR: Invalid component[%s]. Must be one of: %s" % [component, valid.join(", ")]
64
+ exit(4)
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,9 @@
1
+ module SchemaEvolutionManager
2
+
3
+ module SemVersion
4
+
5
+ VERSION = '0.9.24' # Automatically updated by util/create-release.rb
6
+
7
+ end
8
+
9
+ end
@@ -0,0 +1,39 @@
1
+ module SchemaEvolutionManager
2
+
3
+ # Simple template parsing using regular expressions to substitute values.
4
+ # See unit test for example usage at test/specs/lib/template_spec.rb
5
+ class Template
6
+
7
+ class Substitution
8
+
9
+ attr_reader :pattern, :value
10
+
11
+ def initialize(pattern, value)
12
+ @pattern = Preconditions.check_not_blank(pattern)
13
+ @value = Preconditions.check_not_blank(value)
14
+ end
15
+
16
+ end
17
+
18
+ def initialize
19
+ @subs = []
20
+ end
21
+
22
+ # add('first', 'Mike')
23
+ # Will replace all instances of '%%first%%' with value when you call parse
24
+ def add(pattern, value)
25
+ @subs << Substitution.new(pattern, value)
26
+ end
27
+
28
+ def parse(contents)
29
+ Preconditions.check_not_blank(contents)
30
+ string = contents.dup
31
+ @subs.each do |sub|
32
+ string = string.gsub(/%%#{sub.pattern}%%/, sub.value)
33
+ end
34
+ string
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,71 @@
1
+ module SchemaEvolutionManager
2
+
3
+ class Version
4
+
5
+ VERSION_FILE = File.join(Library.base_dir, "VERSION") unless defined?(VERSION_FILE)
6
+
7
+ attr_reader :major, :minor, :micro
8
+
9
+ def initialize(version_string)
10
+ Preconditions.check_not_blank(version_string, "version_string cannot be blank")
11
+ Library.assert_valid_tag(version_string)
12
+ pieces = version_string.split(".", 3)
13
+ @major = pieces[0].to_i
14
+ @minor = pieces[1].to_i
15
+ @micro = pieces[2].to_i
16
+ end
17
+
18
+ def to_version_string
19
+ "%s.%s.%s" % [major, minor, micro]
20
+ end
21
+
22
+ # Returns the next major version
23
+ def next_major
24
+ Version.new("%s.%s.%s" % [major+1, 0, 0])
25
+ end
26
+
27
+ # Returns the next minor version
28
+ def next_minor
29
+ Version.new("%s.%s.%s" % [major, minor+1, 0])
30
+ end
31
+
32
+ # Returns the next micro version
33
+ def next_micro
34
+ Version.new("%s.%s.%s" % [major, minor, micro+1])
35
+ end
36
+
37
+ def <=>(other)
38
+ Preconditions.assert_class(other, Version)
39
+ value = major <=> other.major
40
+ if value == 0
41
+ value = minor <=> other.minor
42
+ if value == 0
43
+ value = micro <=> other.micro
44
+ end
45
+ end
46
+ value
47
+ end
48
+
49
+ # Reads the current version (from the VERSION FILE), returning an
50
+ # instance of the Version class
51
+ def Version.read
52
+ Preconditions.check_state(File.exists?(VERSION_FILE), "Version file at path[%s] not found" % VERSION_FILE)
53
+ version = IO.read(VERSION_FILE).strip
54
+ Version.new(version)
55
+ end
56
+
57
+ def Version.write(version)
58
+ Preconditions.assert_class(version, Version)
59
+ File.open(VERSION_FILE, "w") do |out|
60
+ out << "%s\n" % version.to_version_string
61
+ end
62
+ end
63
+
64
+ def Version.is_valid?(version_string)
65
+ Preconditions.check_not_blank(version_string, "version_string cannot be blank")
66
+ version_string.match(/^\d+\.\d+\.\d+$/)
67
+ end
68
+
69
+ end
70
+
71
+ end