upsert 2.9.9-universal-java-11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.standard.yml +1 -0
- data/.travis.yml +63 -0
- data/.yardopts +2 -0
- data/CHANGELOG +265 -0
- data/Gemfile +16 -0
- data/LICENSE +24 -0
- data/README.md +411 -0
- data/Rakefile +54 -0
- data/lib/upsert.rb +284 -0
- data/lib/upsert/active_record_upsert.rb +12 -0
- data/lib/upsert/binary.rb +8 -0
- data/lib/upsert/column_definition.rb +79 -0
- data/lib/upsert/column_definition/mysql.rb +24 -0
- data/lib/upsert/column_definition/postgresql.rb +66 -0
- data/lib/upsert/column_definition/sqlite3.rb +34 -0
- data/lib/upsert/connection.rb +37 -0
- data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +31 -0
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
- data/lib/upsert/connection/Java_OrgSqlite_Conn.rb +17 -0
- data/lib/upsert/connection/Mysql2_Client.rb +76 -0
- data/lib/upsert/connection/PG_Connection.rb +35 -0
- data/lib/upsert/connection/SQLite3_Database.rb +28 -0
- data/lib/upsert/connection/jdbc.rb +105 -0
- data/lib/upsert/connection/postgresql.rb +24 -0
- data/lib/upsert/connection/sqlite3.rb +19 -0
- data/lib/upsert/merge_function.rb +73 -0
- data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
- data/lib/upsert/merge_function/Java_OrgSqlite_Conn.rb +10 -0
- data/lib/upsert/merge_function/Mysql2_Client.rb +36 -0
- data/lib/upsert/merge_function/PG_Connection.rb +26 -0
- data/lib/upsert/merge_function/SQLite3_Database.rb +10 -0
- data/lib/upsert/merge_function/mysql.rb +66 -0
- data/lib/upsert/merge_function/postgresql.rb +365 -0
- data/lib/upsert/merge_function/sqlite3.rb +43 -0
- data/lib/upsert/row.rb +59 -0
- data/lib/upsert/version.rb +3 -0
- data/spec/active_record_upsert_spec.rb +26 -0
- data/spec/binary_spec.rb +21 -0
- data/spec/correctness_spec.rb +190 -0
- data/spec/database_functions_spec.rb +106 -0
- data/spec/database_spec.rb +121 -0
- data/spec/hstore_spec.rb +249 -0
- data/spec/jruby_spec.rb +9 -0
- data/spec/logger_spec.rb +52 -0
- data/spec/misc/get_postgres_reserved_words.rb +12 -0
- data/spec/misc/mysql_reserved.txt +226 -0
- data/spec/misc/pg_reserved.txt +742 -0
- data/spec/multibyte_spec.rb +27 -0
- data/spec/postgresql_spec.rb +94 -0
- data/spec/precision_spec.rb +11 -0
- data/spec/reserved_words_spec.rb +50 -0
- data/spec/sequel_spec.rb +57 -0
- data/spec/spec_helper.rb +417 -0
- data/spec/speed_spec.rb +44 -0
- data/spec/threaded_spec.rb +57 -0
- data/spec/timezones_spec.rb +58 -0
- data/spec/type_safety_spec.rb +12 -0
- data/travis/install_postgres.sh +18 -0
- data/travis/run_docker_db.sh +20 -0
- data/travis/tune_mysql.sh +7 -0
- data/upsert-java.gemspec +13 -0
- data/upsert.gemspec +11 -0
- data/upsert.gemspec.common +107 -0
- metadata +373 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
class Upsert
|
2
|
+
class ColumnDefinition
|
3
|
+
# @private
|
4
|
+
class Postgresql < ColumnDefinition
|
5
|
+
class << self
|
6
|
+
# activerecord-3.2.5/lib/active_record/connection_adapters/postgresql_adapter.rb#column_definitions
|
7
|
+
def all(connection, quoted_table_name)
|
8
|
+
res = connection.execute <<-EOS
|
9
|
+
SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS sql_type, d.adsrc AS default
|
10
|
+
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
11
|
+
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
12
|
+
WHERE a.attrelid = '#{quoted_table_name}'::regclass
|
13
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
14
|
+
EOS
|
15
|
+
res.map do |row|
|
16
|
+
new connection, row['name'], row['sql_type'], row['default']
|
17
|
+
end.sort_by do |cd|
|
18
|
+
cd.name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# NOTE not using this because it can't be indexed
|
24
|
+
# def equality(left, right)
|
25
|
+
# "#{left} IS NOT DISTINCT FROM #{right}"
|
26
|
+
# end
|
27
|
+
|
28
|
+
HSTORE_DETECTOR = /hstore/i
|
29
|
+
|
30
|
+
def initialize(*)
|
31
|
+
super
|
32
|
+
@hstore_query = !!(sql_type =~ HSTORE_DETECTOR)
|
33
|
+
end
|
34
|
+
|
35
|
+
def hstore?
|
36
|
+
@hstore_query
|
37
|
+
end
|
38
|
+
|
39
|
+
def arg_type
|
40
|
+
if hstore?
|
41
|
+
'text'
|
42
|
+
else
|
43
|
+
# JDBC uses prepared statements and properly sends date objects (which are otherwise escaped)
|
44
|
+
RUBY_PLATFORM == "java" ? sql_type : super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_setter_value
|
49
|
+
if hstore?
|
50
|
+
"#{quoted_setter_name}::hstore"
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_setter
|
57
|
+
if hstore?
|
58
|
+
# http://stackoverflow.com/questions/9317971/adding-a-key-to-an-empty-hstore-column
|
59
|
+
"#{quoted_name} = COALESCE(#{quoted_name}, hstore(array[]::varchar[])) || #{to_setter_value}"
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Upsert
|
2
|
+
class ColumnDefinition
|
3
|
+
# @private
|
4
|
+
class Sqlite3 < ColumnDefinition
|
5
|
+
class << self
|
6
|
+
def all(connection, quoted_table_name)
|
7
|
+
# activerecord-3.2.13/lib/active_record/connection_adapters/sqlite_adapter.rb
|
8
|
+
connection.execute("PRAGMA table_info(#{quoted_table_name})").map do |row|#, 'SCHEMA').to_hash
|
9
|
+
if connection.metal.respond_to?(:results_as_hash) and not connection.metal.results_as_hash
|
10
|
+
row = {'name' => row[1], 'type' => row[2], 'dflt_value' => row[4]}
|
11
|
+
end
|
12
|
+
default = case row["dflt_value"]
|
13
|
+
when /^null$/i
|
14
|
+
nil
|
15
|
+
when /^'(.*)'$/
|
16
|
+
$1.gsub(/''/, "'")
|
17
|
+
when /^"(.*)"$/
|
18
|
+
$1.gsub(/""/, '"')
|
19
|
+
else
|
20
|
+
row["dflt_value"]
|
21
|
+
end
|
22
|
+
new connection, row['name'], row['type'], default
|
23
|
+
end.sort_by do |cd|
|
24
|
+
cd.name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def equality(left, right)
|
30
|
+
"(#{left} IS #{right} OR (#{left} IS NULL AND #{right} IS NULL))"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Upsert
|
2
|
+
# @private
|
3
|
+
class Connection
|
4
|
+
attr_reader :controller
|
5
|
+
attr_reader :metal
|
6
|
+
|
7
|
+
def initialize(controller, metal)
|
8
|
+
@controller = controller
|
9
|
+
@metal = metal
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert_binary(bind_values)
|
13
|
+
bind_values.map do |v|
|
14
|
+
case v
|
15
|
+
when Upsert::Binary
|
16
|
+
binary v
|
17
|
+
else
|
18
|
+
v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def bind_value(v)
|
24
|
+
case v
|
25
|
+
when Time, DateTime
|
26
|
+
Upsert.utc_iso8601 v
|
27
|
+
when Date
|
28
|
+
v.strftime ISO8601_DATE
|
29
|
+
when Symbol
|
30
|
+
v.to_s
|
31
|
+
else
|
32
|
+
v
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'upsert/connection/jdbc'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class Connection
|
5
|
+
# @private
|
6
|
+
class Java_ComMysqlJdbc_JDBC4Connection < Connection
|
7
|
+
include Jdbc
|
8
|
+
|
9
|
+
def quote_ident(k)
|
10
|
+
if metal.useAnsiQuotedIdentifiers
|
11
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
12
|
+
else
|
13
|
+
# Escape backticks by doubling them. Ref http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
|
14
|
+
BACKTICK + k.to_s.gsub(BACKTICK, BACKTICK + BACKTICK) + BACKTICK
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def bind_value(v)
|
19
|
+
case v
|
20
|
+
when DateTime, Time
|
21
|
+
date = v.utc
|
22
|
+
java.time.LocalDateTime.of(date.year, date.month, date.day, date.hour, date.min, date.sec, date.nsec)
|
23
|
+
when Date
|
24
|
+
java.time.LocalDate.of(v.year, v.month, v.day)
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "jdbc"
|
2
|
+
require_relative "postgresql"
|
3
|
+
|
4
|
+
class Upsert
|
5
|
+
class Connection
|
6
|
+
# @private
|
7
|
+
class Java_OrgPostgresqlJdbc_PgConnection < Connection
|
8
|
+
include Jdbc
|
9
|
+
include Postgresql
|
10
|
+
|
11
|
+
def quote_ident(k)
|
12
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
13
|
+
end
|
14
|
+
|
15
|
+
def in_transaction?
|
16
|
+
# https://github.com/kares/activerecord-jdbc-adapter/commit/4d6e0e0c52d12b0166810dffc9f898141a23bee6
|
17
|
+
![0, 4].include?(metal.get_transaction_state)
|
18
|
+
end
|
19
|
+
|
20
|
+
def bind_value(v)
|
21
|
+
case v
|
22
|
+
when DateTime, Time
|
23
|
+
date = v.utc
|
24
|
+
java.time.LocalDateTime.of(date.year, date.month, date.day, date.hour, date.min, date.sec, date.nsec)
|
25
|
+
when Date
|
26
|
+
java.time.LocalDate.of(v.year, v.month, v.day)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'upsert/connection/jdbc'
|
2
|
+
require 'upsert/connection/sqlite3'
|
3
|
+
|
4
|
+
class Upsert
|
5
|
+
class Connection
|
6
|
+
# @private
|
7
|
+
class Java_OrgSqlite_Conn < Connection
|
8
|
+
include Jdbc
|
9
|
+
include Sqlite3
|
10
|
+
|
11
|
+
# ?
|
12
|
+
def quote_ident(k)
|
13
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class Upsert
|
2
|
+
class Connection
|
3
|
+
# @private
|
4
|
+
class Mysql2_Client < Connection
|
5
|
+
def execute(sql)
|
6
|
+
Upsert.logger.debug { %{[upsert] #{sql}} }
|
7
|
+
if results = metal.query(sql)
|
8
|
+
rows = []
|
9
|
+
results.each { |row| rows << row }
|
10
|
+
if rows[0].is_a? Array
|
11
|
+
# you don't know if mysql2 is going to give you an array or a hash... and you shouldn't specify, because it's sticky
|
12
|
+
fields = results.fields
|
13
|
+
rows.map { |row| Hash[fields.zip(row)] }
|
14
|
+
else
|
15
|
+
rows
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def quote_value(v)
|
21
|
+
case v
|
22
|
+
when NilClass
|
23
|
+
NULL_WORD
|
24
|
+
when Upsert::Binary
|
25
|
+
quote_binary v.value
|
26
|
+
when String
|
27
|
+
quote_string v
|
28
|
+
when TrueClass, FalseClass
|
29
|
+
quote_boolean v
|
30
|
+
when BigDecimal
|
31
|
+
quote_big_decimal v
|
32
|
+
when Numeric
|
33
|
+
v
|
34
|
+
when Symbol
|
35
|
+
quote_string v.to_s
|
36
|
+
when DateTime, Time
|
37
|
+
# mysql doesn't like it when you send timezone to a datetime
|
38
|
+
quote_string Upsert.utc_iso8601(v, false)
|
39
|
+
when Date
|
40
|
+
quote_date v
|
41
|
+
else
|
42
|
+
raise "not sure how to quote #{v.class}: #{v.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def quote_boolean(v)
|
47
|
+
v ? 'TRUE' : 'FALSE'
|
48
|
+
end
|
49
|
+
|
50
|
+
def quote_string(v)
|
51
|
+
SINGLE_QUOTE + metal.escape(v) + SINGLE_QUOTE
|
52
|
+
end
|
53
|
+
|
54
|
+
# This doubles the size of the representation.
|
55
|
+
def quote_binary(v)
|
56
|
+
X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
|
57
|
+
end
|
58
|
+
|
59
|
+
# put raw binary straight into sql
|
60
|
+
# might work if we could get the encoding issues fixed when joining together the values for the sql
|
61
|
+
# alias_method :quote_binary, :quote_string
|
62
|
+
|
63
|
+
def quote_date(v)
|
64
|
+
quote_string v.strftime(ISO8601_DATE)
|
65
|
+
end
|
66
|
+
|
67
|
+
def quote_ident(k)
|
68
|
+
BACKTICK + metal.escape(k.to_s) + BACKTICK
|
69
|
+
end
|
70
|
+
|
71
|
+
def quote_big_decimal(v)
|
72
|
+
v.to_s('F')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative "postgresql"
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class Connection
|
5
|
+
# @private
|
6
|
+
class PG_Connection < Connection
|
7
|
+
include Postgresql
|
8
|
+
|
9
|
+
def execute(sql, params = nil)
|
10
|
+
if params
|
11
|
+
# Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
12
|
+
# The following will blow up if you pass a value that cannot be automatically type-casted,
|
13
|
+
# such as passing a string to an integer field. You'll get an error something along the
|
14
|
+
# lines of: "invalid input syntax for <type>: <value>"
|
15
|
+
metal.exec sql, convert_binary(params)
|
16
|
+
else
|
17
|
+
Upsert.logger.debug { %{[upsert] #{sql}} }
|
18
|
+
metal.exec sql
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def quote_ident(k)
|
23
|
+
metal.quote_ident k.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def binary(v)
|
27
|
+
{ :value => v.value, :format => 1 }
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_transaction?
|
31
|
+
![PG::PQTRANS_IDLE, PG::PQTRANS_UNKNOWN].include?(metal.transaction_status)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'upsert/connection/sqlite3'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class Connection
|
5
|
+
# @private
|
6
|
+
class SQLite3_Database < Connection
|
7
|
+
include Sqlite3
|
8
|
+
|
9
|
+
def execute(sql, params = nil)
|
10
|
+
if params
|
11
|
+
Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
12
|
+
metal.execute sql, convert_binary(params)
|
13
|
+
else
|
14
|
+
Upsert.logger.debug { %{[upsert] #{sql}} }
|
15
|
+
metal.execute sql
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def quote_ident(k)
|
20
|
+
DOUBLE_QUOTE + SQLite3::Database.quote(k.to_s) + DOUBLE_QUOTE
|
21
|
+
end
|
22
|
+
|
23
|
+
def binary(v)
|
24
|
+
SQLite3::Blob.new v.value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class Upsert
|
2
|
+
class Connection
|
3
|
+
# @private
|
4
|
+
module Jdbc
|
5
|
+
# /Users/seamusabshere/.rvm/gems/jruby-head/gems/activerecord-jdbc-adapter-1.2.2.1/src/java/arjdbc/jdbc/RubyJdbcConnection.java
|
6
|
+
GETTER = {
|
7
|
+
java.sql.Types::CHAR => 'getString',
|
8
|
+
java.sql.Types::VARCHAR => 'getString',
|
9
|
+
java.sql.Types::OTHER => 'getString', # ?! i guess unicode text?
|
10
|
+
java.sql.Types::BINARY => 'getBlob',
|
11
|
+
java.sql.Types::LONGVARCHAR => 'getString',
|
12
|
+
java.sql.Types::BIGINT => 'getLong',
|
13
|
+
java.sql.Types::INTEGER => 'getInt',
|
14
|
+
java.sql.Types::REAL => "getLong",
|
15
|
+
java.sql.Types::ARRAY => ->(r, i){ r.getArray(i).array.to_ary }
|
16
|
+
}
|
17
|
+
java.sql.Types.constants.each do |type_name|
|
18
|
+
i = java.sql.Types.const_get type_name
|
19
|
+
unless GETTER.has_key?(i)
|
20
|
+
GETTER[i] = 'get' + type_name[0].upcase + type_name[1..-1].downcase
|
21
|
+
end
|
22
|
+
end
|
23
|
+
SETTER = Hash.new do |hash, k|
|
24
|
+
hash[k] = 'set' + k
|
25
|
+
end.merge(
|
26
|
+
'TrueClass' => 'setBoolean',
|
27
|
+
'FalseClass' => 'setBoolean',
|
28
|
+
'Fixnum' => 'setInt',
|
29
|
+
'Integer' => 'setInt'
|
30
|
+
)
|
31
|
+
|
32
|
+
def binary(v)
|
33
|
+
v.value.to_java_bytes.java_object
|
34
|
+
end
|
35
|
+
|
36
|
+
def execute(sql, params = nil)
|
37
|
+
has_result = if params
|
38
|
+
Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
39
|
+
setters = self.class.const_get(:SETTER)
|
40
|
+
statement = metal.prepareStatement sql
|
41
|
+
params.each_with_index do |v, i|
|
42
|
+
if v.is_a?(Fixnum) && v > 2_147_483_647
|
43
|
+
statement.setLong i+1, v
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
case v
|
48
|
+
when Upsert::Binary
|
49
|
+
statement.setBytes i+1, binary(v)
|
50
|
+
when Float, BigDecimal
|
51
|
+
statement.setBigDecimal i+1, java.math.BigDecimal.new(v.to_s)
|
52
|
+
when NilClass
|
53
|
+
# http://stackoverflow.com/questions/4243513/why-does-preparedstatement-setnull-requires-sqltype
|
54
|
+
statement.setObject i+1, nil
|
55
|
+
when java.time.LocalDateTime, java.time.Instant, java.time.LocalDate
|
56
|
+
statement.setObject i+1, v
|
57
|
+
else
|
58
|
+
setter = setters[v.class.name]
|
59
|
+
Upsert.logger.debug { "Setting [#{v.class}, #{v}] via #{setter}" }
|
60
|
+
statement.send setter, i+1, v
|
61
|
+
end
|
62
|
+
end
|
63
|
+
statement.execute
|
64
|
+
else
|
65
|
+
Upsert.logger.debug { %{[upsert] #{sql}} }
|
66
|
+
statement = metal.createStatement
|
67
|
+
statement.execute sql
|
68
|
+
end
|
69
|
+
if not has_result
|
70
|
+
statement.close
|
71
|
+
return
|
72
|
+
end
|
73
|
+
getters = self.class.const_get(:GETTER)
|
74
|
+
raw_result = statement.getResultSet
|
75
|
+
meta = raw_result.getMetaData
|
76
|
+
count = meta.getColumnCount
|
77
|
+
column_name_and_getter = (1..count).inject({}) do |memo, i|
|
78
|
+
memo[i] = [ meta.getColumnName(i), getters[meta.getColumnType(i)] ]
|
79
|
+
memo
|
80
|
+
end
|
81
|
+
result = []
|
82
|
+
while raw_result.next
|
83
|
+
row = {}
|
84
|
+
column_name_and_getter.each do |i, cg|
|
85
|
+
column_name, getter = cg
|
86
|
+
if getter == 'getNull'
|
87
|
+
row[column_name] = nil
|
88
|
+
elsif getter.respond_to?(:call)
|
89
|
+
row[column_name] = getter.call(raw_result, i)
|
90
|
+
else
|
91
|
+
row[column_name] = raw_result.send(getter, i)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
result << row
|
95
|
+
end
|
96
|
+
statement.close
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
def in_transaction?
|
101
|
+
raise "Not implemented"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|