departure-76c9880 6.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +8 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +60 -0
- data/.travis.yml +30 -0
- data/CHANGELOG.md +192 -0
- data/CODE_OF_CONDUCT.md +50 -0
- data/Dockerfile +32 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +227 -0
- data/RELEASING.md +17 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/rspec +16 -0
- data/bin/setup +7 -0
- data/config.yml.erb +4 -0
- data/configuration.rb +16 -0
- data/departure.gemspec +35 -0
- data/docker-compose.yml +23 -0
- data/lib/active_record/connection_adapters/for_alter.rb +91 -0
- data/lib/active_record/connection_adapters/percona_adapter.rb +168 -0
- data/lib/departure.rb +43 -0
- data/lib/departure/alter_argument.rb +49 -0
- data/lib/departure/cli_generator.rb +84 -0
- data/lib/departure/command.rb +96 -0
- data/lib/departure/configuration.rb +20 -0
- data/lib/departure/connection_base.rb +9 -0
- data/lib/departure/connection_details.rb +96 -0
- data/lib/departure/dsn.rb +24 -0
- data/lib/departure/errors.rb +39 -0
- data/lib/departure/log_sanitizers/password_sanitizer.rb +22 -0
- data/lib/departure/logger.rb +42 -0
- data/lib/departure/logger_factory.rb +16 -0
- data/lib/departure/migration.rb +96 -0
- data/lib/departure/null_logger.rb +15 -0
- data/lib/departure/option.rb +75 -0
- data/lib/departure/railtie.rb +21 -0
- data/lib/departure/runner.rb +62 -0
- data/lib/departure/user_options.rb +44 -0
- data/lib/departure/version.rb +3 -0
- data/lib/lhm.rb +23 -0
- data/lib/lhm/adapter.rb +107 -0
- data/lib/lhm/column_with_sql.rb +96 -0
- data/lib/lhm/column_with_type.rb +29 -0
- data/test_database.rb +80 -0
- metadata +245 -0
data/RELEASING.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Releasing Departure
|
2
|
+
|
3
|
+
All releases come from the master branch. All other branches won't be maintained
|
4
|
+
and will receive bug fix releases only.
|
5
|
+
|
6
|
+
In order to give support to a new major Rails version, we'll branch off of
|
7
|
+
master, name it following the Rails repo convention, such as `v4.2`, and
|
8
|
+
we'll keep it open for bug fixes.
|
9
|
+
|
10
|
+
1. Update `lib/departure/version.rb` accordingly
|
11
|
+
2. Review the `CHANGELOG.md` and add a new section following the format
|
12
|
+
`[version] - YYYY-MM-DD`. We conform to the guidelines of
|
13
|
+
http://keepachangelog.com/
|
14
|
+
3. Commit the changes with the message `Prepare release VERSION`
|
15
|
+
4. Execute the release rake task as `bundle exec rake release`. It creates the
|
16
|
+
tag, builds and pushes the gem to Rubygems.
|
17
|
+
5. Announce it! :tada:
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
require './configuration'
|
5
|
+
require './test_database'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
task default: :spec
|
10
|
+
|
11
|
+
namespace :db do
|
12
|
+
desc 'Create the test database'
|
13
|
+
task :create do
|
14
|
+
config = Configuration.new
|
15
|
+
TestDatabase.new(config).setup_test_database
|
16
|
+
end
|
17
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'percona_migrator'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/rspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'pathname'
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/bin/setup
ADDED
data/config.yml.erb
ADDED
data/configuration.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
class Configuration
|
5
|
+
CONFIG_PATH = 'config.yml.erb'.freeze
|
6
|
+
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@config = YAML.load(ERB.new(File.read(CONFIG_PATH)).result).freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
config[key]
|
15
|
+
end
|
16
|
+
end
|
data/departure.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'departure/version'
|
7
|
+
|
8
|
+
# This environment variable is set on CI to facilitate testing with multiple
|
9
|
+
# versions of Rails.
|
10
|
+
RAILS_DEPENDENCY_VERSION = ENV.fetch('RAILS_VERSION', ['>= 5.2.0', '< 6.1'])
|
11
|
+
|
12
|
+
Gem::Specification.new do |spec|
|
13
|
+
spec.name = 'departure-76c9880'
|
14
|
+
spec.version = Departure::VERSION
|
15
|
+
spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas', 'Jorge Morante', 'Enrico Stano', 'Adrian Serafin', 'Kirk Haines', 'Guillermo Iguaran']
|
16
|
+
spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'nflamel@gmail.com', 'jorge.morante@redbooth.com', 'adrian@softmad.pl', 'wyhaines@gmail.com', 'guilleiguaran@gmail.com']
|
17
|
+
|
18
|
+
spec.summary = %q(pt-online-schema-change runner for ActiveRecord migrations)
|
19
|
+
spec.description = %q(Execute your ActiveRecord migrations with Percona's pt-online-schema-change. Formerly known as Percona Migrator.)
|
20
|
+
spec.homepage = 'https://github.com/departurerb/departure'
|
21
|
+
spec.license = 'MIT'
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'railties', *Array(RAILS_DEPENDENCY_VERSION)
|
27
|
+
spec.add_runtime_dependency 'activerecord', *Array(RAILS_DEPENDENCY_VERSION)
|
28
|
+
spec.add_runtime_dependency 'mysql2', '>= 0.4.0', '<= 0.5.3'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
|
32
|
+
spec.add_development_dependency 'rspec-its', '~> 1.2'
|
33
|
+
spec.add_development_dependency 'byebug', '~> 8.2', '>= 8.2.1'
|
34
|
+
spec.add_development_dependency 'climate_control', '~> 0.0.3'
|
35
|
+
end
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
version: '2'
|
2
|
+
services:
|
3
|
+
db:
|
4
|
+
image: mysql/mysql-server
|
5
|
+
ports:
|
6
|
+
- "3306:3306"
|
7
|
+
environment:
|
8
|
+
MYSQL_DATABASE: departure_test
|
9
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
10
|
+
MYSQL_ROOT_HOST: '%'
|
11
|
+
rails:
|
12
|
+
build: .
|
13
|
+
links:
|
14
|
+
- db
|
15
|
+
depends_on:
|
16
|
+
- db
|
17
|
+
tty: true
|
18
|
+
stdin_open: true
|
19
|
+
environment:
|
20
|
+
PERCONA_DB_USER: root
|
21
|
+
PERCONA_DB_HOST: db
|
22
|
+
PERCONA_DB_PASSWORD:
|
23
|
+
PERCONA_DB_NAME: departure_test
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'active_record/connection_adapters/mysql/schema_statements'
|
2
|
+
|
3
|
+
module ForAlterStatements
|
4
|
+
class << self
|
5
|
+
def included(_)
|
6
|
+
STDERR.puts 'Including for_alter statements'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def bulk_change_table(table_name, operations) #:nodoc:
|
11
|
+
sqls = operations.flat_map do |command, args|
|
12
|
+
table = args.shift
|
13
|
+
arguments = args
|
14
|
+
|
15
|
+
method = :"#{command}_for_alter"
|
16
|
+
|
17
|
+
raise "Unknown method called : #{method}(#{arguments.inspect})" unless respond_to?(method, true)
|
18
|
+
public_send(method, table, *arguments)
|
19
|
+
end.join(', ')
|
20
|
+
|
21
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def change_column_for_alter(table_name, column_name, type, options = {})
|
25
|
+
column = column_for(table_name, column_name)
|
26
|
+
type ||= column.sql_type
|
27
|
+
|
28
|
+
options = {
|
29
|
+
default: column.default,
|
30
|
+
null: column.null,
|
31
|
+
comment: column.comment
|
32
|
+
}.merge(options)
|
33
|
+
|
34
|
+
td = create_table_definition(table_name)
|
35
|
+
cd = td.new_column_definition(column.name, type, options)
|
36
|
+
schema_creation.accept(ActiveRecord::ConnectionAdapters::ChangeColumnDefinition.new(cd, column.name))
|
37
|
+
end
|
38
|
+
|
39
|
+
def rename_column_for_alter(table_name, column_name, new_column_name)
|
40
|
+
column = column_for(table_name, column_name)
|
41
|
+
options = {
|
42
|
+
default: column.default,
|
43
|
+
null: column.null,
|
44
|
+
auto_increment: column.auto_increment?
|
45
|
+
}
|
46
|
+
|
47
|
+
columns_sql = "SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}"
|
48
|
+
current_type = exec_query(columns_sql, 'SCHEMA').first['Type']
|
49
|
+
td = create_table_definition(table_name)
|
50
|
+
cd = td.new_column_definition(new_column_name, current_type, options)
|
51
|
+
schema_creation.accept(ActiveRecord::ConnectionAdapters::ChangeColumnDefinition.new(cd, column.name))
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_index_for_alter(table_name, column_name, options = {})
|
55
|
+
index_name, index_type, index_columns, _,
|
56
|
+
index_algorithm, index_using = add_index_options(table_name, column_name, options)
|
57
|
+
|
58
|
+
index_algorithm[0, 0] = ', ' if index_algorithm.present?
|
59
|
+
"ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def remove_index_for_alter(table_name, options = {})
|
63
|
+
index_name = index_name_for_remove(table_name, options)
|
64
|
+
"DROP INDEX #{quote_column_name(index_name)}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_timestamps_for_alter(table_name, options = {})
|
68
|
+
[
|
69
|
+
add_column_for_alter(table_name, :created_at, :datetime, options),
|
70
|
+
add_column_for_alter(table_name, :updated_at, :datetime, options)
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
def remove_timestamps_for_alter(table_name, _options = {})
|
75
|
+
[remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
|
76
|
+
end
|
77
|
+
|
78
|
+
def add_column_for_alter(table_name, column_name, type, options = {})
|
79
|
+
td = create_table_definition(table_name)
|
80
|
+
cd = td.new_column_definition(column_name, type, options)
|
81
|
+
schema_creation.accept(ActiveRecord::ConnectionAdapters::AddColumnDefinition.new(cd))
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove_column_for_alter(_table_name, column_name, _type = nil, _options = {})
|
85
|
+
"DROP COLUMN #{quote_column_name(column_name)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def remove_columns_for_alter(table_name, *column_names)
|
89
|
+
column_names.map { |column_name| remove_column_for_alter(table_name, column_name) }
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
2
|
+
require 'active_record/connection_adapters/statement_pool'
|
3
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
4
|
+
require 'departure'
|
5
|
+
require 'forwardable'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
module ConnectionHandling
|
9
|
+
# Establishes a connection to the database that's used by all Active
|
10
|
+
# Record objects.
|
11
|
+
def percona_connection(config)
|
12
|
+
config[:username] = 'root' if config[:username].nil?
|
13
|
+
mysql2_connection = mysql2_connection(config)
|
14
|
+
|
15
|
+
connection_details = Departure::ConnectionDetails.new(config)
|
16
|
+
verbose = ActiveRecord::Migration.verbose
|
17
|
+
sanitizers = [
|
18
|
+
Departure::LogSanitizers::PasswordSanitizer.new(connection_details)
|
19
|
+
]
|
20
|
+
percona_logger = Departure::LoggerFactory.build(sanitizers: sanitizers, verbose: verbose)
|
21
|
+
cli_generator = Departure::CliGenerator.new(connection_details)
|
22
|
+
|
23
|
+
runner = Departure::Runner.new(
|
24
|
+
percona_logger,
|
25
|
+
cli_generator,
|
26
|
+
mysql2_connection
|
27
|
+
)
|
28
|
+
|
29
|
+
connection_options = { mysql_adapter: mysql2_connection }
|
30
|
+
|
31
|
+
ConnectionAdapters::DepartureAdapter.new(
|
32
|
+
runner,
|
33
|
+
logger,
|
34
|
+
connection_options,
|
35
|
+
config
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module ConnectionAdapters
|
41
|
+
class DepartureAdapter < AbstractMysqlAdapter
|
42
|
+
class Column < ActiveRecord::ConnectionAdapters::MySQL::Column
|
43
|
+
def adapter
|
44
|
+
DepartureAdapter
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
|
49
|
+
def visit_DropForeignKey(name) # rubocop:disable Naming/MethodName
|
50
|
+
fk_name =
|
51
|
+
if name =~ /^__(.+)/
|
52
|
+
Regexp.last_match(1)
|
53
|
+
else
|
54
|
+
"_#{name}"
|
55
|
+
end
|
56
|
+
|
57
|
+
"DROP FOREIGN KEY #{fk_name}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
extend Forwardable
|
62
|
+
|
63
|
+
unless method_defined?(:change_column_for_alter)
|
64
|
+
include ForAlterStatements
|
65
|
+
end
|
66
|
+
|
67
|
+
ADAPTER_NAME = 'Percona'.freeze
|
68
|
+
|
69
|
+
def_delegators :mysql_adapter, :last_inserted_id, :each_hash, :set_field_encoding
|
70
|
+
|
71
|
+
def initialize(connection, _logger, connection_options, _config)
|
72
|
+
@mysql_adapter = connection_options[:mysql_adapter]
|
73
|
+
super
|
74
|
+
@prepared_statements = false
|
75
|
+
end
|
76
|
+
|
77
|
+
def exec_delete(sql, name, binds)
|
78
|
+
execute(to_sql(sql, binds), name)
|
79
|
+
@connection.affected_rows
|
80
|
+
end
|
81
|
+
alias exec_update exec_delete
|
82
|
+
|
83
|
+
def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) # rubocop:disable Lint/UnusedMethodArgument, Metrics/LineLength
|
84
|
+
execute(to_sql(sql, binds), name)
|
85
|
+
end
|
86
|
+
|
87
|
+
def exec_query(sql, name = 'SQL', _binds = [])
|
88
|
+
result = execute(sql, name)
|
89
|
+
ActiveRecord::Result.new(result.fields, result.to_a)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Executes a SELECT query and returns an array of rows. Each row is an
|
93
|
+
# array of field values.
|
94
|
+
|
95
|
+
def select_rows(arel, name = nil, binds = [])
|
96
|
+
select_all(arel, name, binds).rows
|
97
|
+
end
|
98
|
+
|
99
|
+
# Executes a SELECT query and returns an array of record hashes with the
|
100
|
+
# column names as keys and column values as values.
|
101
|
+
def select(sql, name = nil, binds = [])
|
102
|
+
exec_query(sql, name, binds)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns true, as this adapter supports migrations
|
106
|
+
def supports_migrations?
|
107
|
+
true
|
108
|
+
end
|
109
|
+
|
110
|
+
# rubocop:disable Metrics/ParameterLists
|
111
|
+
def new_column(field, default, type_metadata, null, table_name, default_function, collation, comment)
|
112
|
+
Column.new(field, default, type_metadata, null, table_name, default_function, collation, comment)
|
113
|
+
end
|
114
|
+
# rubocop:enable Metrics/ParameterLists
|
115
|
+
|
116
|
+
# Adds a new index to the table
|
117
|
+
#
|
118
|
+
# @param table_name [String, Symbol]
|
119
|
+
# @param column_name [String, Symbol]
|
120
|
+
# @param options [Hash] optional
|
121
|
+
def add_index(table_name, column_name, options = {})
|
122
|
+
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
|
123
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ADD #{index_type} INDEX #{quote_column_name(index_name)} (#{index_columns})#{index_options}" # rubocop:disable Metrics/LineLength
|
124
|
+
end
|
125
|
+
|
126
|
+
# Remove the given index from the table.
|
127
|
+
#
|
128
|
+
# @param table_name [String, Symbol]
|
129
|
+
# @param options [Hash] optional
|
130
|
+
def remove_index(table_name, options = {})
|
131
|
+
index_name = index_name_for_remove(table_name, options)
|
132
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} DROP INDEX #{quote_column_name(index_name)}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def schema_creation
|
136
|
+
SchemaCreation.new(self)
|
137
|
+
end
|
138
|
+
|
139
|
+
def change_table(table_name, _options = {})
|
140
|
+
recorder = ActiveRecord::Migration::CommandRecorder.new(self)
|
141
|
+
yield update_table_definition(table_name, recorder)
|
142
|
+
bulk_change_table(table_name, recorder.commands)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns the MySQL error number from the exception. The
|
146
|
+
# AbstractMysqlAdapter requires it to be implemented
|
147
|
+
def error_number(_exception); end
|
148
|
+
|
149
|
+
def full_version
|
150
|
+
if ActiveRecord::VERSION::MAJOR < 6
|
151
|
+
get_full_version
|
152
|
+
else
|
153
|
+
schema_cache.database_version.full_version_string
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# This is a method defined in Rails 6.0, and we have no control over the
|
158
|
+
# naming of this method.
|
159
|
+
def get_full_version # rubocop:disable Naming/AccessorMethodName
|
160
|
+
mysql_adapter.raw_connection.server_info[:version]
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
attr_reader :mysql_adapter
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/lib/departure.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/all'
|
3
|
+
|
4
|
+
require 'active_record/connection_adapters/for_alter'
|
5
|
+
|
6
|
+
require 'departure/version'
|
7
|
+
require 'departure/log_sanitizers/password_sanitizer'
|
8
|
+
require 'departure/runner'
|
9
|
+
require 'departure/cli_generator'
|
10
|
+
require 'departure/logger'
|
11
|
+
require 'departure/null_logger'
|
12
|
+
require 'departure/logger_factory'
|
13
|
+
require 'departure/configuration'
|
14
|
+
require 'departure/errors'
|
15
|
+
require 'departure/command'
|
16
|
+
require 'departure/connection_base'
|
17
|
+
require 'departure/migration'
|
18
|
+
|
19
|
+
require 'departure/railtie' if defined?(Rails)
|
20
|
+
|
21
|
+
# We need the OS not to buffer the IO to see pt-osc's output while migrating
|
22
|
+
$stdout.sync = true
|
23
|
+
|
24
|
+
ActiveSupport.on_load(:active_record) do
|
25
|
+
ActiveRecord::Migration.class_eval do
|
26
|
+
include Departure::Migration
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Departure
|
31
|
+
class << self
|
32
|
+
attr_accessor :configuration
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.configure
|
36
|
+
self.configuration ||= Configuration.new
|
37
|
+
yield(configuration)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.load
|
41
|
+
# No-op left for compatibility
|
42
|
+
end
|
43
|
+
end
|