schema-evolution-manager 0.9.24

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,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