pg_migrate 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,34 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
19
+
20
+ # Intellij artifacts
21
+ .idea
22
+ *.iml
23
+
24
+ # RVM
25
+ .rvmrc
26
+
27
+ # VIM
28
+ *~
29
+
30
+ # rspec output directory
31
+ target
32
+
33
+ # local ruby symbolic link to fool IDE
34
+ ruby
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "lib/pg_migrate/templates"]
2
+ path = lib/pg_migrate/templates
3
+ url = git@github.com:sethcall/pg_migrate_templates.git
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pg_migrate.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'rspec', '2.11.0'
8
+ #gem 'files', :git => 'git@github.com:sethcall/files.git'
9
+ gem 'files', :path => '/Users/seth/workspace/files'
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pg_migrate (0.0.1)
5
+ logging (= 1.7.2)
6
+ pg (= 0.14.0)
7
+ thor (= 0.15.4)
8
+
9
+ PATH
10
+ remote: /Users/seth/workspace/files
11
+ specs:
12
+ files (0.2.1)
13
+
14
+ GEM
15
+ remote: https://rubygems.org/
16
+ specs:
17
+ diff-lcs (1.1.3)
18
+ little-plugger (1.1.3)
19
+ logging (1.7.2)
20
+ little-plugger (>= 1.1.3)
21
+ pg (0.14.0)
22
+ rspec (2.11.0)
23
+ rspec-core (~> 2.11.0)
24
+ rspec-expectations (~> 2.11.0)
25
+ rspec-mocks (~> 2.11.0)
26
+ rspec-core (2.11.0)
27
+ rspec-expectations (2.11.1)
28
+ diff-lcs (~> 1.1.3)
29
+ rspec-mocks (2.11.0)
30
+ thor (0.15.4)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ files!
37
+ pg_migrate!
38
+ rspec (= 2.11.0)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # PgMigrate
2
+
3
+ The ruby implementation of pg_migrate.
4
+
5
+ With this gem, there are two primary features:
6
+ * Manifest Builder - process a user's pg_migrate manifest into a stable set of SQL migration scripts
7
+ * Migrator - migrate a database, using a built pg_migrate manifest
8
+
9
+ ## Installation
10
+
11
+ **NOTE: this gem is not yet published**
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'pg_migrate'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install pg_migrate
24
+
25
+ ## Usage
26
+
27
+ TODO: Write usage instructions here
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/pg_migrate ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pg_migrate'
4
+
5
+ include PgMigrate
6
+
7
+ CommandLine.start
@@ -0,0 +1,158 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+ require 'find'
4
+ require 'erb'
5
+
6
+
7
+ module PgMigrate
8
+ # takes a unprocessed manifest directory, and adds before/after headers to each file
9
+ class Builder
10
+
11
+
12
+ attr_accessor :manifest_reader, :sql_reader
13
+
14
+
15
+ def initialize(manifest_reader, sql_reader)
16
+ @log = Logging.logger[self]
17
+ @manifest_reader = manifest_reader
18
+ @sql_reader = sql_reader
19
+ @template_dir = File.join(File.dirname(__FILE__), 'templates')
20
+ end
21
+
22
+
23
+ # input_dir is root path, contains file 'manifest' and 'migrations'
24
+ # output_dir will have a manifest and migrations folder, but processed
25
+ # force will create the output dir if needed, and *delete an existing directory* if it's in the way
26
+ def build(input_dir, output_dir, options={:force=>true})
27
+ input_dir = File.expand_path(input_dir)
28
+ output_dir = File.expand_path(output_dir)
29
+
30
+ if input_dir == output_dir
31
+ raise 'input_dir can not be same as output_dir: #{input_dir}'
32
+ end
33
+
34
+ @log.debug "building migration directory #{input_dir} and placing result at: #{output_dir}"
35
+
36
+ output = Pathname.new(output_dir)
37
+ if !output.exist?
38
+ if !options[:force]
39
+ raise "Output directory '#{output_dir}' does not exist. Create it or specify create_output_dir=true"
40
+ else
41
+ output.mkpath
42
+ end
43
+ else
44
+ # verify that it's is a directory
45
+ if !output.directory?
46
+ raise "output_dir #{output_dir} is a file; not a directory."
47
+ else
48
+ @log.debug("deleting & recreating existing output_dir #{output_dir}")
49
+ output.rmtree
50
+ output.mkpath
51
+ end
52
+ end
53
+
54
+ # manifest always goes over mostly as-is,
55
+ # just with a comment added at top indicating our version
56
+
57
+ input_manifest = File.join(input_dir, MANIFEST_FILENAME)
58
+ output_manifest = File.join(output_dir, MANIFEST_FILENAME)
59
+
60
+ File.open(output_manifest, 'w') do |fout|
61
+ fout.puts "#{BUILDER_VERSION_HEADER}pg_migrate_ruby-#{PgMigrate::VERSION}"
62
+ IO.readlines(input_manifest).each do |input|
63
+ fout.puts input
64
+ end
65
+ end
66
+
67
+ # in order array of manifest declarations
68
+ loaded_manifest = @manifest_reader.load_input_manifest(input_dir)
69
+ # hashed on migration name hash of manifest
70
+
71
+ loaded_manifest_hash = @manifest_reader.hash_loaded_manifest(loaded_manifest)
72
+ @manifest_reader.validate_migration_paths(input_dir, loaded_manifest)
73
+
74
+ build_up(input_dir, output_dir, loaded_manifest_hash)
75
+ end
76
+
77
+ def build_up(input_dir, output_dir, loaded_manifest_hash)
78
+ migrations_input = File.join(input_dir, UP_DIRNAME)
79
+ migrations_output = File.join(output_dir, UP_DIRNAME)
80
+
81
+ # iterate through files in input migrations path, wrapping files with transactions and other required bits
82
+
83
+ Find.find(migrations_input) do |path|
84
+ if path == ".."
85
+ Find.prune
86
+ else
87
+ @log.debug "building #{path}"
88
+
89
+ # create relative bit
90
+ relative_path = path[migrations_input.length..-1]
91
+
92
+ # create the filename correct for the input directory, for this file
93
+ migration_in_path = path
94
+
95
+ # create the filename correct for the output directory, for this file
96
+ migration_out_path = File.join(migrations_output, relative_path)
97
+
98
+ process_and_copy_up(migration_in_path, migration_out_path, relative_path, loaded_manifest_hash)
99
+ end
100
+ end
101
+
102
+ create_bootstrap_script(migrations_output)
103
+ end
104
+
105
+ # creates the 'pg_migrations table'
106
+ def create_bootstrap_script(migration_out_path)
107
+ run_template("bootstrap.erb", binding, File.join(migration_out_path, BOOTSTRAP_FILENAME))
108
+ end
109
+
110
+ def create_wrapped_up_migration(migration_in_filepath, migration_out_filepath, migration_def)
111
+ builder_version="pg_migrate_ruby-#{PgMigrate::VERSION}"
112
+ migration_content = nil
113
+ File.open(migration_in_filepath, 'r') {|reader| migration_content = reader.read }
114
+ run_template("up.erb", binding, File.join(migration_out_filepath))
115
+ end
116
+
117
+ # given an input template and binding, writes to an output file
118
+ def run_template(template, binding, output_filepath)
119
+ bootstrap_template = nil
120
+ File.open(File.join(@template_dir, template), 'r') do |reader|
121
+ bootstrap_template = reader.read
122
+ end
123
+
124
+
125
+ template = ERB.new(bootstrap_template, 0, "%<>")
126
+ content = template.result(binding)
127
+ File.open(output_filepath, 'w') do |writer|
128
+ writer.syswrite(content)
129
+ end
130
+ end
131
+
132
+ def process_and_copy_up(migration_in_path, migration_out_path, relative_path, loaded_manifest_hash)
133
+
134
+ if FileTest.directory?(migration_in_path)
135
+ # copy over directories
136
+ # find relative-to-migrations dir this path
137
+ FileUtils.mkdir(migration_out_path)
138
+ else
139
+ if migration_in_path.end_with?('.sql')
140
+ # if a .sql file, then copy & process
141
+
142
+ # create the the 'key' version of this name, which is basically the filepath
143
+ # of the .sql file relative without the leading '/' directory
144
+ manifest_name = relative_path[1..-1]
145
+
146
+ @log.debug("retrieving manifest definition for #{manifest_name}")
147
+
148
+ migration_def = loaded_manifest_hash[manifest_name]
149
+
150
+ create_wrapped_up_migration(migration_in_path, migration_out_path, migration_def)
151
+ else
152
+ # if not a .sql file, just copy it over
153
+ FileUtils.cp(migration_in_path, migration_out_path)
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,78 @@
1
+ module PgMigrate
2
+ class CommandLine < Thor
3
+
4
+ desc "up", "migrates the database forwards, applying migrations found in the source directory"
5
+ method_option :source, :aliases => "-s", :default => '.', :lazy_default => '.', :banner => 'input directory', :desc => "a pg_migrate built manifest. Should contain your processed manifest and up|down|test folders"
6
+ method_option :connopts, :aliases => "-o", :type => :hash, :required => true, :banner => "connection options", :desc => "database connection options used by gem 'pg': dbname|host|hostaddr|port|user|password|connection_timeout|options|sslmode|krbsrvname|gsslib|service"
7
+ method_option :verbose, :aliases => "-v", :type => :boolean, :default => false, :banner => "verbose", :desc=> "set to raise verbosity"
8
+
9
+ def up
10
+ bootstrap_logger(options[:verbose])
11
+
12
+ manifest_reader = ManifestReader.new
13
+ sql_reader = SqlReader.new
14
+
15
+ connopts = options[:connopts]
16
+ if !connopts[:port].nil?
17
+ connopts[:port] = connopts[:port].to_i
18
+ end
19
+
20
+ migrator = Migrator.new(manifest_reader, sql_reader, connopts)
21
+
22
+ begin
23
+ migrator.migrate(options[:source])
24
+ rescue Exception => e
25
+ if !options[:verbose]
26
+ # catch common exceptions and make pretty on command-line
27
+ if !e.message.index("ManifestReader: code=unloadable_manifest").nil?
28
+ puts "Unable to load manifest in source directory '#{options[:source]}' . Check -s|--source option and run again."
29
+ exit 1
30
+ else
31
+ raise e
32
+ end
33
+ else
34
+ raise e
35
+ end
36
+ end
37
+
38
+
39
+ end
40
+
41
+ desc "down", "not implemented"
42
+
43
+ def down
44
+ bootstrap_logger(options[:verbose])
45
+
46
+ raise 'Not implemented'
47
+ end
48
+
49
+ desc "build", "processes a pg_migrate source directory and places the result in the specified output directory"
50
+ method_option :source, :aliases => "-s", :default => '.', :lazy_default => '.', :banner => 'input directory', :desc => "the input directory containing a manifest file and up|down|test folders"
51
+ method_option :out, :aliases => "-o", :required => true, :banner => "output directory", :desc => "where the processed migrations will be placed"
52
+ method_option :force, :aliases => "-f", :default => false, :type => :boolean, :banner => "overwrite out", :desc => "if specified, the out directory will be created before processing occurs, replacing any existing directory"
53
+ method_option :verbose, :aliases => "-v", :type => :boolean, :default => false, :banner => "verbose", :desc=> "set to raise verbosity"
54
+
55
+ def build
56
+
57
+ bootstrap_logger(options[:verbose])
58
+
59
+ manifest_reader = ManifestReader.new
60
+ sql_reader = SqlReader.new
61
+ builder = Builder.new(manifest_reader, sql_reader)
62
+ builder.build(options[:source], options[:out], :force => options[:force])
63
+ end
64
+
65
+ no_tasks do
66
+ def bootstrap_logger(verbose)
67
+ # bootstrap logger
68
+ if verbose
69
+ Logging.logger.root.level = :debug
70
+ else
71
+ Logging.logger.root.level = :info
72
+ end
73
+
74
+ Logging.logger.root.appenders = Logging.appenders.stdout
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,49 @@
1
+ require 'yaml'
2
+
3
+ module PgMigrate
4
+
5
+ class ConfigParser
6
+
7
+ def self.rails(path, environment)
8
+
9
+ config = {}
10
+
11
+ rails_config = YAML.load_file(path)
12
+
13
+ if !rails_config.has_key?(environment)
14
+ raise "no environment #{environment} found in rails config file: #{path}"
15
+ end
16
+
17
+ rails_config = rails_config[environment]
18
+
19
+ # populate from rails YAML to PG
20
+
21
+ # required parameters 1st
22
+ if !rails_config.has_key?("database")
23
+ raise "no database key found in #{path} with environment #{environment}"
24
+ end
25
+
26
+ config[:dbname] = rails_config["database"]
27
+
28
+ if rails_config.has_key?("host")
29
+ config[:host] = rails_config["host"]
30
+ end
31
+
32
+ if rails_config.has_key?("port")
33
+ config[:port] = rails_config["port"]
34
+ end
35
+
36
+ if rails_config.has_key?("username")
37
+ config[:user] = rails_config["username"]
38
+ end
39
+
40
+ if rails_config.has_key?("password")
41
+ config[:password] = rails_config["password"]
42
+ end
43
+
44
+ return config
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,103 @@
1
+ require 'pathname'
2
+
3
+ module PgMigrate
4
+ class ManifestReader
5
+
6
+ def initialize
7
+ @log = Logging.logger[self]
8
+ end
9
+
10
+ # returns array of migration paths
11
+ def load_input_manifest(manifest_path)
12
+ manifest, version = load_manifest(manifest_path, false)
13
+ return manifest
14
+ end
15
+
16
+ # returns [array of migration paths, version]
17
+ def load_output_manifest(manifest_path)
18
+ return load_manifest(manifest_path, true)
19
+ end
20
+
21
+
22
+ # verify that the migration files exist
23
+ def validate_migration_paths(manifest_path, manifest)
24
+ # each item in the manifest should be a valid file
25
+ manifest.each do |item|
26
+ item_path = build_migration_path(manifest_path, item.name)
27
+ if !Pathname.new(item_path).exist?
28
+ raise "manifest reference #{item.name} does not exist at path #{item_path}"
29
+ end
30
+ end
31
+ end
32
+
33
+ # construct a migration file path location based on the manifest basedir and the name of the migration
34
+ def build_migration_path(manifest_path, migration_name)
35
+ File.join(manifest_path, UP_DIRNAME, "#{migration_name}")
36
+ end
37
+
38
+ def hash_loaded_manifest(loaded_manifest)
39
+ hash = {}
40
+ loaded_manifest.each do |manifest|
41
+ hash[manifest.name] = manifest
42
+ end
43
+ return hash
44
+ end
45
+
46
+ # read in the manifest, saving each migration declaration in order as they are found
47
+ private
48
+ def load_manifest(manifest_path, is_output)
49
+
50
+ manifest = []
51
+ version = nil
52
+
53
+ manifest_filepath = File.join(manifest_path, MANIFEST_FILENAME)
54
+
55
+ @log.debug "loading manifest from #{manifest_path}"
56
+
57
+ if !FileTest::exist?(manifest_filepath)
58
+ raise "ManifestReader: code=unloadable_manifest: manifest not found at #{manifest_path}"
59
+ end
60
+
61
+ # there should be a file called 'manifest' at this location
62
+ manifest_lines = IO.readlines(manifest_filepath)
63
+
64
+ ordinal = 0
65
+ manifest_lines.each_with_index do |line, index|
66
+ # ignore comments
67
+ migration_name = line.strip
68
+
69
+ @log.debug "processing line:#{index} #{line}"
70
+
71
+ # output files must have a version header as 1st line o file
72
+ if is_output
73
+ if index == 0
74
+ # the first line must be the version comment. if not, error out.
75
+ if migration_name.index(BUILDER_VERSION_HEADER) == 0 && migration_name.length > BUILDER_VERSION_HEADER.length
76
+ version = migration_name[BUILDER_VERSION_HEADER.length..-1]
77
+ @log.debug "manifest has builder_version #{version}"
78
+ else
79
+ raise "manifest invalid: missing/malformed version. expecting '# pg_migrate-VERSION' to begin first line '#{line}' of manifest file: '#{manifest_path}'"
80
+ end
81
+ end
82
+ end
83
+
84
+ if migration_name.empty? or migration_name.start_with?('#')
85
+ # ignored!
86
+ else
87
+ @log.debug "adding manifest #{migration_name} with ordinal #{ordinal}"
88
+ manifest.push(Migration.new(migration_name, ordinal, build_migration_path(manifest_path, migration_name)))
89
+ ordinal += 1
90
+ end
91
+
92
+ # the logic above wouldn't get upset with an empty manifest
93
+ if is_output
94
+ if version.nil?
95
+ raise "manifest invalid: empty"
96
+ end
97
+ end
98
+ end
99
+ return manifest, version
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,12 @@
1
+ module PgMigrate
2
+ class Migration
3
+ attr_accessor :name, :ordinal, :md5, :created, :production, :filepath
4
+
5
+ def initialize(name, ordinal, filepath)
6
+ @name = name
7
+ @ordinal = ordinal
8
+ @filepath = filepath
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ module PgMigrate
2
+
3
+ class Migrator
4
+
5
+ attr_accessor :conn, :connection_hash, :manifest_path, :manifest, :manifest_reader, :sql_reader
6
+
7
+ # options = gem 'pg' connection_hash options, or connstring => dbname=test port=5432, or pgconn => PG::Connection object
8
+ def initialize(manifest_reader, sql_reader, options = {})
9
+ @log = Logging.logger[self]
10
+ @connection_hash = options
11
+ @manifest = nil
12
+ @builder_version = nil
13
+ @manifest_reader = manifest_reader
14
+ @sql_reader = sql_reader
15
+ end
16
+
17
+ # 'migrate' attempt to migrate your database based on the contents of your built manifest
18
+ # The manifest_path argument should point to your manifest
19
+ # manifest_path = the directory containing your 'manifest' file, 'up' directory, 'down' directory, 'test' directory
20
+ # this method will throw an exception if anything goes wrong (such as bad SQL in the migrations themselves)
21
+
22
+ def migrate(manifest_path)
23
+ @manifest_path = manifest_path
24
+
25
+ if !@connection_hash[:pgconn].nil?
26
+ @conn = @connection_hash[:pgconn]
27
+ elsif !@connection_hash[:connstring].nil?
28
+ @conn = PG::Connection.open(@connection_hash[:connstring])
29
+ else
30
+ @conn = PG::Connection.open(@connection_hash)
31
+ end
32
+
33
+ # this is used to record the version of the 'migrator' in the pg_migrate table
34
+ @conn.exec("SET application_name = 'pg_migrate_ruby-#{PgMigrate::VERSION}'")
35
+
36
+ # load the manifest, and version of the builder that made it
37
+ process_manifest()
38
+
39
+ # execute the migrations
40
+ run_migrations()
41
+ end
42
+
43
+
44
+ # load the manifest's migration declarations, and validate that each migration points to a real file
45
+ def process_manifest
46
+ @manifest, @builder_version = @manifest_reader.load_output_manifest(@manifest_path)
47
+ @manifest_reader.validate_migration_paths(@manifest_path, @manifest)
48
+ end
49
+
50
+ # run all necessary migrations
51
+ def run_migrations
52
+
53
+ # run bootstrap before user migrations to prepare database
54
+ run_bootstrap
55
+
56
+ # loop through the manifest, executing migrations in turn
57
+ manifest.each_with_index do |migration, index|
58
+ execute_migration(migration.name, migration.filepath)
59
+ end
60
+
61
+ end
62
+
63
+ # executes the bootstrap method
64
+ def run_bootstrap
65
+ bootstrap = File.join(@manifest_path, UP_DIRNAME, BOOTSTRAP_FILENAME)
66
+ execute_migration('bootstrap.sql', bootstrap)
67
+ end
68
+
69
+ # execute a single migration by loading it's statements from file, and then executing each
70
+ def execute_migration(name, filepath)
71
+ @log.debug "executing migration #{filepath}"
72
+
73
+ statements = @sql_reader.load_migration(filepath)
74
+ if statements.length == 0
75
+ raise 'no statements found in migration #{migration_path}'
76
+ end
77
+ run_migration(name, statements)
78
+ end
79
+
80
+ # execute all the statements of a single migration
81
+ def run_migration(name, statements)
82
+
83
+ begin
84
+ statements.each do |statement|
85
+ conn.exec(statement).clear
86
+ end
87
+ rescue Exception => e
88
+ # we make a special allowance for one exception; it just means this migration
89
+ # has already occured, and we should just treat it like a continue
90
+ if e.message.index('pg_migrate: code=migration_exists').nil?
91
+ conn.exec("ROLLBACK")
92
+ raise e
93
+ else
94
+ conn.exec("ROLLBACK")
95
+ @log.info "migration #{name} already run"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+