amalgalite 0.4.2-x86-mswin32-60
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/HISTORY +81 -0
- data/LICENSE +29 -0
- data/README +40 -0
- data/bin/amalgalite-pack-into-db +155 -0
- data/examples/a.rb +9 -0
- data/examples/blob.rb +105 -0
- data/examples/bootstrap.rb +36 -0
- data/examples/gem-db.rb +94 -0
- data/examples/requires.rb +54 -0
- data/examples/schema-info.rb +34 -0
- data/ext/amalgalite3.c +201 -0
- data/ext/amalgalite3.h +121 -0
- data/ext/amalgalite3_blob.c +241 -0
- data/ext/amalgalite3_constants.c +221 -0
- data/ext/amalgalite3_database.c +550 -0
- data/ext/amalgalite3_requires_bootstrap.c +210 -0
- data/ext/amalgalite3_statement.c +628 -0
- data/ext/extconf.rb +19 -0
- data/ext/gen_constants.rb +130 -0
- data/ext/rbconfig-mingw.rb +178 -0
- data/ext/sqlite3.c +97092 -0
- data/ext/sqlite3.h +6364 -0
- data/ext/sqlite3_options.h +4 -0
- data/ext/sqlite3ext.h +372 -0
- data/gemspec.rb +55 -0
- data/lib/amalgalite.rb +33 -0
- data/lib/amalgalite/blob.rb +186 -0
- data/lib/amalgalite/boolean.rb +42 -0
- data/lib/amalgalite/column.rb +86 -0
- data/lib/amalgalite/core_ext/kernel/require.rb +14 -0
- data/lib/amalgalite/database.rb +514 -0
- data/lib/amalgalite/index.rb +43 -0
- data/lib/amalgalite/paths.rb +70 -0
- data/lib/amalgalite/profile_tap.rb +130 -0
- data/lib/amalgalite/requires.rb +112 -0
- data/lib/amalgalite/schema.rb +115 -0
- data/lib/amalgalite/sqlite3.rb +6 -0
- data/lib/amalgalite/sqlite3/constants.rb +82 -0
- data/lib/amalgalite/sqlite3/database/status.rb +69 -0
- data/lib/amalgalite/sqlite3/status.rb +61 -0
- data/lib/amalgalite/sqlite3/version.rb +38 -0
- data/lib/amalgalite/statement.rb +394 -0
- data/lib/amalgalite/table.rb +36 -0
- data/lib/amalgalite/taps.rb +2 -0
- data/lib/amalgalite/taps/console.rb +27 -0
- data/lib/amalgalite/taps/io.rb +71 -0
- data/lib/amalgalite/trace_tap.rb +35 -0
- data/lib/amalgalite/type_map.rb +63 -0
- data/lib/amalgalite/type_maps/default_map.rb +167 -0
- data/lib/amalgalite/type_maps/storage_map.rb +41 -0
- data/lib/amalgalite/type_maps/text_map.rb +23 -0
- data/lib/amalgalite/version.rb +37 -0
- data/lib/amalgalite/view.rb +26 -0
- data/lib/amalgalite3.so +0 -0
- data/spec/amalgalite_spec.rb +4 -0
- data/spec/blob_spec.rb +81 -0
- data/spec/boolean_spec.rb +23 -0
- data/spec/database_spec.rb +238 -0
- data/spec/default_map_spec.rb +87 -0
- data/spec/integeration_spec.rb +111 -0
- data/spec/paths_spec.rb +28 -0
- data/spec/schema_spec.rb +60 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/sqlite3/constants_spec.rb +65 -0
- data/spec/sqlite3/database_status_spec.rb +36 -0
- data/spec/sqlite3/status_spec.rb +18 -0
- data/spec/sqlite3/version_spec.rb +14 -0
- data/spec/sqlite3_spec.rb +23 -0
- data/spec/statement_spec.rb +134 -0
- data/spec/storage_map_spec.rb +41 -0
- data/spec/tap_spec.rb +59 -0
- data/spec/text_map_spec.rb +23 -0
- data/spec/type_map_spec.rb +17 -0
- data/spec/version_spec.rb +9 -0
- data/tasks/announce.rake +39 -0
- data/tasks/config.rb +110 -0
- data/tasks/distribution.rake +53 -0
- data/tasks/documentation.rake +33 -0
- data/tasks/extension.rake +100 -0
- data/tasks/rspec.rake +32 -0
- data/tasks/rubyforge.rake +59 -0
- data/tasks/utils.rb +80 -0
- metadata +192 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module Amalgalite
|
7
|
+
#
|
8
|
+
# a class representing the meta information about an SQLite index
|
9
|
+
#
|
10
|
+
class Index
|
11
|
+
# the name of the index
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# the sql statement that created the index
|
15
|
+
attr_reader :sql
|
16
|
+
|
17
|
+
# the table the index is for
|
18
|
+
attr_accessor :table
|
19
|
+
|
20
|
+
# the columns that make up this index, in index order
|
21
|
+
attr_accessor :columns
|
22
|
+
|
23
|
+
# sqlite sequence number of the index
|
24
|
+
attr_accessor :sequence_number
|
25
|
+
|
26
|
+
# is the index unique
|
27
|
+
attr_writer :unique
|
28
|
+
|
29
|
+
def initialize( name, sql, table )
|
30
|
+
@name = name
|
31
|
+
@sql = sql
|
32
|
+
@table = table
|
33
|
+
@columns = []
|
34
|
+
@sequence_number = nil
|
35
|
+
@unique = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def unique?
|
39
|
+
return @unique
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
module Amalgalite
|
6
|
+
#
|
7
|
+
# Paths contains helpful methods to determine paths of files inside the
|
8
|
+
# Amalgalite library
|
9
|
+
#
|
10
|
+
module Paths
|
11
|
+
#
|
12
|
+
# The root directory of the project is considered to be the parent directory
|
13
|
+
# of the 'lib' directory.
|
14
|
+
#
|
15
|
+
# returns:: [String] The full expanded path of the parent directory of 'lib'
|
16
|
+
# going up the path from the current file. Trailing
|
17
|
+
# File::SEPARATOR is guaranteed.
|
18
|
+
#
|
19
|
+
def self.root_dir
|
20
|
+
unless @root_dir
|
21
|
+
path_parts = ::File.expand_path(__FILE__).split(::File::SEPARATOR)
|
22
|
+
lib_index = path_parts.rindex("lib")
|
23
|
+
@root_dir = path_parts[0...lib_index].join(::File::SEPARATOR) + ::File::SEPARATOR
|
24
|
+
end
|
25
|
+
return @root_dir
|
26
|
+
end
|
27
|
+
|
28
|
+
# returns:: [String] The full expanded path of the +config+ directory
|
29
|
+
# below _root_dir_. All parameters passed in are joined onto the
|
30
|
+
# result. Trailing File::SEPARATOR is guaranteed if _args_ are
|
31
|
+
# *not* present.
|
32
|
+
#
|
33
|
+
def self.config_path(*args)
|
34
|
+
self.sub_path("config", *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns:: [String] The full expanded path of the +data+ directory below
|
38
|
+
# _root_dir_. All parameters passed in are joined onto the
|
39
|
+
# result. Trailing File::SEPARATOR is guaranteed if
|
40
|
+
# _*args_ are *not* present.
|
41
|
+
#
|
42
|
+
def self.data_path(*args)
|
43
|
+
self.sub_path("data", *args)
|
44
|
+
end
|
45
|
+
|
46
|
+
# returns:: [String] The full expanded path of the +lib+ directory below
|
47
|
+
# _root_dir_. All parameters passed in are joined onto the
|
48
|
+
# result. Trailing File::SEPARATOR is guaranteed if
|
49
|
+
# _*args_ are *not* present.
|
50
|
+
#
|
51
|
+
def self.lib_path(*args)
|
52
|
+
self.sub_path("lib", *args)
|
53
|
+
end
|
54
|
+
|
55
|
+
# returns:: [String] The full expanded path of the +ext+ directory below
|
56
|
+
# _root_dir_. All parameters passed in are joined onto the
|
57
|
+
# result. Trailing File::SEPARATOR is guaranteed if
|
58
|
+
# _*args_ are *not* present.
|
59
|
+
#
|
60
|
+
def self.ext_path(*args)
|
61
|
+
self.sub_path("ext", *args)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.sub_path(sub,*args)
|
65
|
+
sp = ::File.join(root_dir, sub) + File::SEPARATOR
|
66
|
+
sp = ::File.join(sp, *args) if args
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module Amalgalite
|
7
|
+
#
|
8
|
+
# A ProfileSampler is a sampler of profile times. It aggregates up profile
|
9
|
+
# events that happen for the same source. It is based upon the RFuzz::Sampler
|
10
|
+
# class from the rfuzz gem
|
11
|
+
#
|
12
|
+
class ProfileSampler
|
13
|
+
#
|
14
|
+
# create a new sampler with the given name
|
15
|
+
#
|
16
|
+
def initialize( name )
|
17
|
+
@name = name
|
18
|
+
reset!
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# reset the internal state so it may be used again
|
23
|
+
#
|
24
|
+
def reset!
|
25
|
+
@sum = 0.0
|
26
|
+
@sumsq = 0.0
|
27
|
+
@n = 0
|
28
|
+
@min = 0.0
|
29
|
+
@max = 0.0
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# add a sample to the calculations
|
34
|
+
#
|
35
|
+
def sample( value )
|
36
|
+
@sum += value
|
37
|
+
@sumsq += (value * value)
|
38
|
+
if @n == 0 then
|
39
|
+
@min = @max = value
|
40
|
+
else
|
41
|
+
@min = value if value < @min
|
42
|
+
@max = value if value > @max
|
43
|
+
end
|
44
|
+
@n += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# return the mean of the data
|
49
|
+
#
|
50
|
+
def mean
|
51
|
+
@sum / @n
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# returns the standard deviation of the data
|
56
|
+
#
|
57
|
+
def stddev
|
58
|
+
begin
|
59
|
+
Math.sqrt( (@sumsq - ( @sum * @sum / @n)) / (@n-1) )
|
60
|
+
rescue Errno::EDOM
|
61
|
+
return 0.0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# return all the values as an array
|
67
|
+
#
|
68
|
+
def to_a
|
69
|
+
[ @name, @sum, @sumsq, @n, mean, stddev, @min, @max ]
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# return all the values as a hash
|
74
|
+
#
|
75
|
+
def to_h
|
76
|
+
{ 'name' => @name, 'n' => @n,
|
77
|
+
'sum' => @sum, 'sumsq' => @sumsq, 'mean' => mean,
|
78
|
+
'stddev' => stddev, 'min' => @min, 'max' => @max }
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# return a string containing the sampler summary
|
83
|
+
#
|
84
|
+
def to_s
|
85
|
+
"[%s] => sum: %d, sumsq: %d, n: %d, mean: %0.6f, stddev: %0.6f, min: %d, max: %d" % to_a
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# A Profile Tap recives +profile+ events from SQLite which involve the number of
|
92
|
+
# nanoseconds in wall-clock time it took for a particular thing to happen. In
|
93
|
+
# general this +thing+ is an SQL statement.
|
94
|
+
#
|
95
|
+
# It has a well known +profile+ method which when invoked will write the event
|
96
|
+
# to a delegate object.
|
97
|
+
#
|
98
|
+
#
|
99
|
+
class ProfileTap
|
100
|
+
|
101
|
+
attr_reader :samplers
|
102
|
+
|
103
|
+
#
|
104
|
+
# Create a new ProfileTap object that wraps the given object and calls the
|
105
|
+
# method named in +send_to+ ever time a profile event happens.
|
106
|
+
#
|
107
|
+
def initialize( wrapped_obj, send_to = 'profile' )
|
108
|
+
unless wrapped_obj.respond_to?( send_to )
|
109
|
+
raise Amalgalite::Error, "#{wrapped_obj.class.name} does not respond to #{send_to.to_s} "
|
110
|
+
end
|
111
|
+
|
112
|
+
@delegate_obj = wrapped_obj
|
113
|
+
@delegate_method = send_to
|
114
|
+
@samplers = {}
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Record the profile information and send the delegate object the msg and
|
119
|
+
# time information.
|
120
|
+
#
|
121
|
+
def profile( msg, time )
|
122
|
+
unless sampler = @samplers[msg]
|
123
|
+
msg = msg.gsub(/\s+/,' ')
|
124
|
+
sampler = @samplers[msg] = ProfileSampler.new( msg )
|
125
|
+
end
|
126
|
+
sampler.sample( time )
|
127
|
+
@delegate_obj.send( @delegate_method, msg, time )
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'amalgalite'
|
2
|
+
|
3
|
+
module Amalgalite
|
4
|
+
#
|
5
|
+
# Requires encapsulates requiring itesm from the database
|
6
|
+
class Requires
|
7
|
+
class << self
|
8
|
+
def load_path_db_connections
|
9
|
+
@load_path_db_connections ||= {}
|
10
|
+
end
|
11
|
+
def load_path
|
12
|
+
@load_path ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def db_connection_to( dbfile_name )
|
16
|
+
unless connection = load_path_db_connections[ dbfile_name ]
|
17
|
+
puts "loading file #{dbfile_name}"
|
18
|
+
connection = ::Amalgalite::Database.new( dbfile_name )
|
19
|
+
load_path_db_connections[dbfile_name] = connection
|
20
|
+
end
|
21
|
+
return connection
|
22
|
+
end
|
23
|
+
|
24
|
+
def require( filename )
|
25
|
+
load_path.each { |lp| lp.require( filename ) }
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# return the files in their dependency order for use for packing into a
|
30
|
+
# database
|
31
|
+
#
|
32
|
+
def require_order
|
33
|
+
@require_roder ||= %w[
|
34
|
+
amalgalite.rb
|
35
|
+
amalgalite/blob.rb
|
36
|
+
amalgalite/boolean.rb
|
37
|
+
amalgalite/column.rb
|
38
|
+
amalgalite/statement.rb
|
39
|
+
amalgalite/trace_tap.rb
|
40
|
+
amalgalite/profile_tap.rb
|
41
|
+
amalgalite/type_map.rb
|
42
|
+
amalgalite/type_maps/storage_map.rb
|
43
|
+
amalgalite/type_maps/text_map.rb
|
44
|
+
amalgalite/type_maps/default_map.rb
|
45
|
+
amalgalite/database.rb
|
46
|
+
amalgalite/index.rb
|
47
|
+
amalgalite/paths.rb
|
48
|
+
amalgalite/table.rb
|
49
|
+
amalgalite/view.rb
|
50
|
+
amalgalite/schema.rb
|
51
|
+
amalgalite/version.rb
|
52
|
+
amalgalite/sqlite3/version.rb
|
53
|
+
amalgalite/sqlite3/constants.rb
|
54
|
+
amalgalite/sqlite3.rb
|
55
|
+
amalgalite/taps/io.rb
|
56
|
+
amalgalite/taps/console.rb
|
57
|
+
amalgalite/taps.rb
|
58
|
+
amalgalite/core_ext/kernel/require.rb
|
59
|
+
amalgalite/requires.rb
|
60
|
+
]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :dbfile_name
|
65
|
+
attr_reader :table_name
|
66
|
+
attr_reader :filename_column
|
67
|
+
attr_reader :contents_column
|
68
|
+
attr_reader :db_connection
|
69
|
+
|
70
|
+
def initialize( opts = {} )
|
71
|
+
@dbfile_name = opts[:dbfile_name] || "lib.db"
|
72
|
+
@table_name = opts[:table_name] || "rubylibs"
|
73
|
+
@filename_column = opts[:filename_column] || "filename"
|
74
|
+
@contents_column = opts[:contents_column] || "contents"
|
75
|
+
@db_connection = Requires.db_connection_to( dbfile_name )
|
76
|
+
Requires.load_path << self
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# return the sql to find the file contents for a file in this requires
|
81
|
+
#
|
82
|
+
def sql
|
83
|
+
@sql ||= "SELECT #{filename_column}, #{contents_column} FROM #{table_name} WHERE #{filename_column} = ?"
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# require a file in this database table. This will check and see if the
|
88
|
+
# file is already required. If it isn't it will select the contents
|
89
|
+
# associated with the row identified by the filename and eval those contents
|
90
|
+
# within the context of TOPLEVEL_BINDING. The filename is then appended to
|
91
|
+
# $".
|
92
|
+
#
|
93
|
+
# if the file was required then true is returned, otherwise false
|
94
|
+
#
|
95
|
+
def require( filename )
|
96
|
+
if $".include?( filename ) then
|
97
|
+
return false
|
98
|
+
else
|
99
|
+
begin
|
100
|
+
rows = db_connection.execute(sql, filename)
|
101
|
+
row = rows.first
|
102
|
+
eval( row[contents_column].to_s, TOPLEVEL_BINDING)
|
103
|
+
$" << row[filename_column]
|
104
|
+
rescue => e
|
105
|
+
raise LoadError, "Failure loading #{filename} from #{dbfile_name} : #{e}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
require 'amalgalite/core_ext/kernel/require'
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'amalgalite/table'
|
7
|
+
require 'amalgalite/index'
|
8
|
+
require 'amalgalite/column'
|
9
|
+
require 'amalgalite/view'
|
10
|
+
|
11
|
+
module Amalgalite
|
12
|
+
#
|
13
|
+
# An object view of the schema in the SQLite database. If the schema changes
|
14
|
+
# after this class is created, it has no knowledge of that.
|
15
|
+
#
|
16
|
+
class Schema
|
17
|
+
|
18
|
+
attr_reader :catalog
|
19
|
+
attr_reader :schema
|
20
|
+
attr_reader :tables
|
21
|
+
attr_reader :views
|
22
|
+
attr_reader :db
|
23
|
+
|
24
|
+
#
|
25
|
+
# Create a new instance of Schema
|
26
|
+
#
|
27
|
+
def initialize( db, catalog = 'main', schema = 'sqlite')
|
28
|
+
@db = db
|
29
|
+
@catalog = catalog
|
30
|
+
@schema = schema
|
31
|
+
|
32
|
+
load_schema!
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# load the schema from the database
|
37
|
+
def load_schema!
|
38
|
+
load_tables
|
39
|
+
load_views
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# load all the tables
|
44
|
+
#
|
45
|
+
def load_tables
|
46
|
+
@tables = {}
|
47
|
+
@db.execute("SELECT tbl_name, sql FROM sqlite_master WHERE type = 'table'") do |table_info|
|
48
|
+
table = Amalgalite::Table.new( table_info['tbl_name'], table_info['sql'] )
|
49
|
+
table.columns = load_columns( table )
|
50
|
+
table.schema = self
|
51
|
+
table.indexes = load_indexes( table )
|
52
|
+
|
53
|
+
@tables[table.name] = table
|
54
|
+
end
|
55
|
+
|
56
|
+
@tables
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# load all the indexes for a particular table
|
61
|
+
#
|
62
|
+
def load_indexes( table )
|
63
|
+
indexes = {}
|
64
|
+
|
65
|
+
@db.prepare("SELECT name, sql FROM sqlite_master WHERE type ='index' and tbl_name = $name") do |idx_stmt|
|
66
|
+
idx_stmt.execute( "$name" => table.name) do |idx_info|
|
67
|
+
indexes[idx_info['name']] = Amalgalite::Index.new( idx_info['name'], idx_info['sql'], table )
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
@db.execute("PRAGMA index_list( #{table.name} );") do |idx_list|
|
72
|
+
idx = indexes[idx_list['name']]
|
73
|
+
|
74
|
+
idx.sequence_number = idx_list['seq']
|
75
|
+
idx.unique = Boolean.to_bool( idx_list['unique'] )
|
76
|
+
|
77
|
+
@db.execute("PRAGMA index_info( #{idx.name} );") do |col_info|
|
78
|
+
idx.columns << table.columns[col_info['name']]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
return indexes
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# load all the columns for a particular table
|
86
|
+
#
|
87
|
+
def load_columns( table )
|
88
|
+
cols = {}
|
89
|
+
@db.execute("PRAGMA table_info(#{table.name})") do |row|
|
90
|
+
col = Amalgalite::Column.new( "main", table.name, row['name'] )
|
91
|
+
|
92
|
+
col.default_value = row['dflt_value']
|
93
|
+
@db.api.table_column_metadata( "main", table.name, col.name ).each_pair do |key, value|
|
94
|
+
col.send("#{key}=", value)
|
95
|
+
end
|
96
|
+
col.schema = self
|
97
|
+
cols[col.name] = col
|
98
|
+
end
|
99
|
+
cols
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# load all the views for the database
|
104
|
+
#
|
105
|
+
def load_views
|
106
|
+
@views = {}
|
107
|
+
@db.execute("SELECT name, sql FROM sqlite_master WHERE type = 'view'") do |view_info|
|
108
|
+
view = Amalgalite::View.new( view_info['name'], view_info['sql'] )
|
109
|
+
view.schema = self
|
110
|
+
@views[view.name] = view
|
111
|
+
end
|
112
|
+
@views
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|