postshift 0.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +28 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/Appraisals +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +54 -0
- data/README.md +111 -0
- data/Rakefile +34 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +9 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/ar_5.0.gemfile +8 -0
- data/gemfiles/ar_5.0.gemfile.lock +70 -0
- data/gemfiles/ar_5.1.gemfile +8 -0
- data/gemfiles/ar_5.1.gemfile.lock +70 -0
- data/lib/active_record/connection_adapters/redshift/column.rb +7 -0
- data/lib/active_record/connection_adapters/redshift/referential_integrity.rb +11 -0
- data/lib/active_record/connection_adapters/redshift/schema_definitions.rb +43 -0
- data/lib/active_record/connection_adapters/redshift/schema_dumper.rb +25 -0
- data/lib/active_record/connection_adapters/redshift/schema_statements.rb +154 -0
- data/lib/active_record/connection_adapters/redshift/type_metadata.rb +18 -0
- data/lib/active_record/connection_adapters/redshift_adapter.rb +220 -0
- data/lib/postshift.rb +5 -0
- data/lib/postshift/version.rb +3 -0
- data/postshift.gemspec +37 -0
- metadata +199 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Redshift
|
4
|
+
if ActiveRecord.version < Gem::Version.new('5.1')
|
5
|
+
# All this to add 'encoding' to Structure
|
6
|
+
class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment, :encoding)
|
7
|
+
# From PostgreSQL to maintain compatability
|
8
|
+
attr_accessor :array
|
9
|
+
|
10
|
+
# From Abstract to maintain compatability
|
11
|
+
def primary_key?
|
12
|
+
primary_key || type.to_sym == :primary_key
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
|
17
|
+
include ActiveRecord::ConnectionAdapters::PostgreSQL::ColumnMethods
|
18
|
+
|
19
|
+
def new_column_definition(name, type, options) # :nodoc:
|
20
|
+
super.tap do |column|
|
21
|
+
column.encoding = options[:encoding]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def create_column_definition(name, type, options=nil)
|
28
|
+
ColumnDefinition.new name, type, options
|
29
|
+
end
|
30
|
+
end
|
31
|
+
else
|
32
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
|
33
|
+
def primary_key(name, type=:primary_key, **options)
|
34
|
+
ints = %i(integer bigint)
|
35
|
+
options[:auto_increment] ||= true if ints.include?(type) && !options.key?(:default)
|
36
|
+
type = :primary_key if ints.include?(type) && options.delete(:auto_increment) == true
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Redshift
|
4
|
+
module ColumnDumper
|
5
|
+
def column_spec_for_primary_key(column)
|
6
|
+
super.tap do |spec|
|
7
|
+
spec[:id] = ':primary_key' if column.sql_type == 'primary_key'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Adds +:encoding+ option to the default set
|
12
|
+
def prepare_column_options(column)
|
13
|
+
super.tap do |spec|
|
14
|
+
spec[:encoding] = "'#{column.sql_type_metadata.encoding}'" if column.sql_type_metadata.encoding.present?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds +:encoding+ as a valid migration key
|
19
|
+
def migration_keys
|
20
|
+
super + [:encoding]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Redshift
|
4
|
+
class SchemaCreation < PostgreSQL::SchemaCreation
|
5
|
+
private
|
6
|
+
|
7
|
+
def add_column_options!(sql, options)
|
8
|
+
sql = super
|
9
|
+
if (encoding = encoding_option(options)).present?
|
10
|
+
sql << " ENCODE #{encoding}"
|
11
|
+
end
|
12
|
+
sql
|
13
|
+
end
|
14
|
+
|
15
|
+
def encoding_option(options)
|
16
|
+
if ActiveRecord.version < Gem::Version.new('5.1')
|
17
|
+
options[:column].encoding
|
18
|
+
else
|
19
|
+
options[:encoding]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module SchemaStatements
|
25
|
+
# Create a new Redshift database. Options include <tt>:owner</tt> and <tt>:connection_limit</tt>
|
26
|
+
# Example:
|
27
|
+
# create_database config[:database], config
|
28
|
+
# create_database 'foo_development', encoding: 'unicode'
|
29
|
+
def create_database(name, options = {})
|
30
|
+
options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
|
31
|
+
|
32
|
+
option_string = options.inject("") do |memo, (key, value)|
|
33
|
+
memo += case key
|
34
|
+
when :owner
|
35
|
+
" OWNER = \"#{value}\""
|
36
|
+
when :connection_limit
|
37
|
+
" CONNECTION LIMIT = #{value}"
|
38
|
+
else
|
39
|
+
''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_table(table_name, comment: nil, **options)
|
47
|
+
options[:options] ||= ''
|
48
|
+
options[:options] += "DISTKEY(#{options.delete(:distkey)}) " if options.key?(:distkey)
|
49
|
+
options[:options] += "SORTKEY(#{options.delete(:sortkey)}) " if options.key?(:sortkey)
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def indexes(*)
|
54
|
+
[]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the list of all column definitions for a table.
|
58
|
+
def columns(table_name)
|
59
|
+
column_definitions(table_name.to_s).map do |column_name, type, default, notnull, oid, fmod, encoding|
|
60
|
+
default_value = extract_value_from_default(default)
|
61
|
+
type = determine_primary_key_type_conversion(type, default)
|
62
|
+
type_metadata = fetch_type_metadata(column_name, type, oid, fmod, encoding)
|
63
|
+
default_function = extract_default_function(default_value, default)
|
64
|
+
new_column(column_name, default_value, type_metadata, notnull == false, table_name, default_function)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def determine_primary_key_type_conversion(type, default)
|
69
|
+
return 'primary_key' if (type == 'integer' && default.to_s.starts_with?('"identity"'))
|
70
|
+
type
|
71
|
+
end
|
72
|
+
|
73
|
+
def new_column(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil) # :nodoc:
|
74
|
+
RedshiftColumn.new(name, default, sql_type_metadata, null, table_name, default_function)
|
75
|
+
end
|
76
|
+
|
77
|
+
def table_options(table_name) # :nodoc:
|
78
|
+
{}.tap do |options|
|
79
|
+
if (distkey = table_distkey(table_name)).present?
|
80
|
+
options[:distkey] = distkey
|
81
|
+
end
|
82
|
+
if (sortkey = table_sortkey(table_name)).present?
|
83
|
+
options[:sortkey] = sortkey
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def table_distkey(table_name) # :nodoc:
|
89
|
+
select_value("SELECT \"column\" FROM pg_table_def WHERE tablename = #{quote(table_name)} AND distkey = true")
|
90
|
+
end
|
91
|
+
|
92
|
+
def table_sortkey(table_name) # :nodoc:
|
93
|
+
columns = select_values("SELECT \"column\" FROM pg_table_def WHERE tablename = #{quote(table_name)} AND sortkey > 0 ORDER BY sortkey ASC")
|
94
|
+
columns.present? ? columns.join(', ') : nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns just a table's primary key
|
98
|
+
def primary_keys(table)
|
99
|
+
pks = query(<<-end_sql, 'SCHEMA')
|
100
|
+
SELECT DISTINCT attr.attname
|
101
|
+
FROM pg_attribute attr
|
102
|
+
INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
|
103
|
+
INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
|
104
|
+
WHERE cons.contype = 'p'
|
105
|
+
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
|
106
|
+
end_sql
|
107
|
+
pks.present? ? pks[0] : pks
|
108
|
+
end
|
109
|
+
|
110
|
+
# TODO: This entire method block for 't2.oid::regclass::text' to 't2.relname'
|
111
|
+
def foreign_keys(table_name)
|
112
|
+
fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA')
|
113
|
+
SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
|
114
|
+
FROM pg_constraint c
|
115
|
+
JOIN pg_class t1 ON c.conrelid = t1.oid
|
116
|
+
JOIN pg_class t2 ON c.confrelid = t2.oid
|
117
|
+
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
|
118
|
+
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
|
119
|
+
JOIN pg_namespace t3 ON c.connamespace = t3.oid
|
120
|
+
WHERE c.contype = 'f'
|
121
|
+
AND t1.relname = #{quote(table_name)}
|
122
|
+
AND t3.nspname = ANY (current_schemas(false))
|
123
|
+
ORDER BY c.conname
|
124
|
+
SQL
|
125
|
+
|
126
|
+
fk_info.map do |row|
|
127
|
+
options = {
|
128
|
+
column: row['column'],
|
129
|
+
name: row['name'],
|
130
|
+
primary_key: row['primary_key']
|
131
|
+
}
|
132
|
+
|
133
|
+
options[:on_delete] = extract_foreign_key_action(row['on_delete'])
|
134
|
+
options[:on_update] = extract_foreign_key_action(row['on_update'])
|
135
|
+
|
136
|
+
ForeignKeyDefinition.new(table_name, row['to_table'], options)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def fetch_type_metadata(column_name, sql_type, oid, fmod, encoding)
|
141
|
+
cast_type = get_oid_type(oid, fmod, column_name, sql_type)
|
142
|
+
simple_type = SqlTypeMetadata.new(
|
143
|
+
sql_type: sql_type,
|
144
|
+
type: cast_type.type,
|
145
|
+
limit: cast_type.limit,
|
146
|
+
precision: cast_type.precision,
|
147
|
+
scale: cast_type.scale,
|
148
|
+
)
|
149
|
+
RedshiftSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod, encoding: encoding)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class RedshiftSQLTypeMetadata < PostgreSQLTypeMetadata
|
4
|
+
attr_reader :encoding
|
5
|
+
|
6
|
+
def initialize(type_metadata, oid: nil, fmod: nil, encoding: nil)
|
7
|
+
super(type_metadata, oid: oid, fmod: fmod)
|
8
|
+
@encoding = encoding unless encoding == 'none'
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def attributes_for_hash
|
14
|
+
super << encoding
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/module/deprecation'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
6
|
+
|
7
|
+
require 'active_record/connection_adapters/redshift/column'
|
8
|
+
require 'active_record/connection_adapters/redshift/referential_integrity'
|
9
|
+
require 'active_record/connection_adapters/redshift/schema_definitions'
|
10
|
+
require 'active_record/connection_adapters/redshift/schema_dumper'
|
11
|
+
require 'active_record/connection_adapters/redshift/schema_statements'
|
12
|
+
require 'active_record/connection_adapters/redshift/type_metadata'
|
13
|
+
|
14
|
+
module ActiveRecord
|
15
|
+
module ConnectionHandling # :nodoc
|
16
|
+
RS_VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
|
17
|
+
:client_encoding, :options, :application_name, :fallback_application_name,
|
18
|
+
:keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
|
19
|
+
:tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
|
20
|
+
:sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
|
21
|
+
|
22
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
23
|
+
def redshift_connection(config)
|
24
|
+
conn_params = config.symbolize_keys
|
25
|
+
|
26
|
+
conn_params.delete_if { |_, v| v.nil? }
|
27
|
+
|
28
|
+
# Map ActiveRecords param names to PGs.
|
29
|
+
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
|
30
|
+
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
|
31
|
+
|
32
|
+
# Forward only valid config params to PGconn.connect.
|
33
|
+
conn_params.keep_if { |k, _| RS_VALID_CONN_PARAMS.include?(k) }
|
34
|
+
|
35
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
36
|
+
# so just pass a nil connection object for the time being.
|
37
|
+
ConnectionAdapters::RedshiftAdapter.new(nil, logger, conn_params, config)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ConnectionAdapters
|
42
|
+
class RedshiftAdapter < PostgreSQLAdapter
|
43
|
+
ADAPTER_NAME = 'Redshift'.freeze
|
44
|
+
|
45
|
+
NATIVE_DATABASE_TYPES = {
|
46
|
+
primary_key: 'integer identity primary key',
|
47
|
+
string: { name: 'varchar' },
|
48
|
+
text: { name: 'varchar' },
|
49
|
+
integer: { name: 'integer' },
|
50
|
+
float: { name: 'float' },
|
51
|
+
decimal: { name: 'decimal' },
|
52
|
+
datetime: { name: 'timestamp' },
|
53
|
+
time: { name: 'timestamptz' },
|
54
|
+
date: { name: 'date' },
|
55
|
+
bigint: { name: 'bigint' },
|
56
|
+
boolean: { name: 'boolean' },
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
include Redshift::ColumnDumper
|
60
|
+
include Redshift::ReferentialIntegrity
|
61
|
+
include Redshift::SchemaStatements
|
62
|
+
|
63
|
+
def schema_creation # :nodoc:
|
64
|
+
Redshift::SchemaCreation.new self
|
65
|
+
end
|
66
|
+
|
67
|
+
def supports_index_sort_order?
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def supports_partial_index?
|
72
|
+
false
|
73
|
+
end
|
74
|
+
|
75
|
+
def supports_expression_index?
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def supports_transaction_isolation?
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
def supports_json?
|
84
|
+
false
|
85
|
+
end
|
86
|
+
|
87
|
+
def supports_savepoints?
|
88
|
+
false
|
89
|
+
end
|
90
|
+
|
91
|
+
def native_database_types #:nodoc:
|
92
|
+
NATIVE_DATABASE_TYPES
|
93
|
+
end
|
94
|
+
|
95
|
+
def supports_extensions?
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
def use_insert_returning?
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def supports_advisory_locks?
|
104
|
+
false
|
105
|
+
end
|
106
|
+
|
107
|
+
def supports_ranges?
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def supports_materialized_views?
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
def postgresql_version
|
116
|
+
# Will pass all inernal version support checks
|
117
|
+
Float::INFINITY
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# TODO: Copied from PostgreSQL with minor registration changes. If broken out, could override segments, etc
|
123
|
+
def initialize_type_map(m) # :nodoc:
|
124
|
+
register_class_with_limit m, 'int2', Type::Integer
|
125
|
+
register_class_with_limit m, 'int4', Type::Integer
|
126
|
+
register_class_with_limit m, 'int8', Type::Integer
|
127
|
+
m.alias_type 'oid', 'int2'
|
128
|
+
m.register_type 'float4', Type::Float.new
|
129
|
+
m.alias_type 'float8', 'float4'
|
130
|
+
m.register_type 'text', Type::Text.new
|
131
|
+
register_class_with_limit m, 'varchar', Type::String
|
132
|
+
m.alias_type 'char', 'varchar'
|
133
|
+
m.alias_type 'name', 'varchar'
|
134
|
+
m.alias_type 'bpchar', 'varchar'
|
135
|
+
m.register_type 'bool', Type::Boolean.new
|
136
|
+
m.alias_type 'timestamptz', 'timestamp'
|
137
|
+
m.register_type 'date', Type::Date.new
|
138
|
+
|
139
|
+
m.register_type 'timestamp' do |_, _, sql_type|
|
140
|
+
precision = extract_precision(sql_type)
|
141
|
+
OID::DateTime.new(precision: precision)
|
142
|
+
end
|
143
|
+
|
144
|
+
m.register_type 'numeric' do |_, fmod, sql_type|
|
145
|
+
precision = extract_precision(sql_type)
|
146
|
+
scale = extract_scale(sql_type)
|
147
|
+
|
148
|
+
# The type for the numeric depends on the width of the field,
|
149
|
+
# so we'll do something special here.
|
150
|
+
#
|
151
|
+
# When dealing with decimal columns:
|
152
|
+
#
|
153
|
+
# places after decimal = fmod - 4 & 0xffff
|
154
|
+
# places before decimal = (fmod - 4) >> 16 & 0xffff
|
155
|
+
if fmod && (fmod - 4 & 0xffff).zero?
|
156
|
+
# FIXME: Remove this class, and the second argument to
|
157
|
+
# lookups on PG
|
158
|
+
Type::DecimalWithoutScale.new(precision: precision)
|
159
|
+
else
|
160
|
+
OID::Decimal.new(precision: precision, scale: scale)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def configure_connection
|
166
|
+
if @config[:encoding]
|
167
|
+
@connection.set_client_encoding(@config[:encoding])
|
168
|
+
end
|
169
|
+
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
170
|
+
|
171
|
+
# SET statements from :variables config hash
|
172
|
+
# http://www.postgresql.org/docs/8.3/static/sql-set.html
|
173
|
+
variables = @config[:variables] || {}
|
174
|
+
variables.map do |k, v|
|
175
|
+
if v == ':default' || v == :default
|
176
|
+
# Sets the value to the global or compile default
|
177
|
+
execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
|
178
|
+
elsif !v.nil?
|
179
|
+
execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns the list of a table's column names, data types, and default values.
|
185
|
+
#
|
186
|
+
# The underlying query is roughly:
|
187
|
+
# SELECT column.name, column.type, default.value
|
188
|
+
# FROM column LEFT JOIN default
|
189
|
+
# ON column.table_id = default.table_id
|
190
|
+
# AND column.num = default.column_num
|
191
|
+
# WHERE column.table_id = get_table_id('table_name')
|
192
|
+
# AND column.num > 0
|
193
|
+
# AND NOT column.is_dropped
|
194
|
+
# ORDER BY column.num
|
195
|
+
#
|
196
|
+
# If the table name is not prefixed with a schema, the database will
|
197
|
+
# take the first match from the schema search path.
|
198
|
+
#
|
199
|
+
# Query implementation notes:
|
200
|
+
# - format_type includes the column size constraint, e.g. varchar(50)
|
201
|
+
# - ::regclass is a function that gives the id for a table name
|
202
|
+
def column_definitions(table_name) # :nodoc:
|
203
|
+
query(<<-end_sql, 'SCHEMA')
|
204
|
+
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
|
205
|
+
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
|
206
|
+
format_encoding(a.attencodingtype::integer)
|
207
|
+
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
208
|
+
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
209
|
+
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
|
210
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
211
|
+
ORDER BY a.attnum
|
212
|
+
end_sql
|
213
|
+
end
|
214
|
+
|
215
|
+
def create_table_definition(*args) # :nodoc:
|
216
|
+
Redshift::TableDefinition.new(*args)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|