amalgalite 0.10.1-x86-mingw32
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 +201 -0
- data/LICENSE +29 -0
- data/README +51 -0
- data/bin/amalgalite-pack +126 -0
- data/examples/a.rb +9 -0
- data/examples/blob.rb +88 -0
- data/examples/bootstrap.rb +36 -0
- data/examples/define_aggregate.rb +75 -0
- data/examples/define_function.rb +104 -0
- data/examples/gem-db.rb +94 -0
- data/examples/gems.db +0 -0
- data/examples/require_me.rb +11 -0
- data/examples/requires.rb +42 -0
- data/examples/schema-info.rb +34 -0
- data/ext/amalgalite/amalgalite3.c +290 -0
- data/ext/amalgalite/amalgalite3.h +151 -0
- data/ext/amalgalite/amalgalite3_blob.c +240 -0
- data/ext/amalgalite/amalgalite3_constants.c +221 -0
- data/ext/amalgalite/amalgalite3_database.c +1148 -0
- data/ext/amalgalite/amalgalite3_requires_bootstrap.c +210 -0
- data/ext/amalgalite/amalgalite3_statement.c +639 -0
- data/ext/amalgalite/extconf.rb +36 -0
- data/ext/amalgalite/gen_constants.rb +130 -0
- data/ext/amalgalite/sqlite3.c +106729 -0
- data/ext/amalgalite/sqlite3.h +5626 -0
- data/ext/amalgalite/sqlite3_options.h +4 -0
- data/ext/amalgalite/sqlite3ext.h +380 -0
- data/gemspec.rb +60 -0
- data/lib/amalgalite.rb +43 -0
- data/lib/amalgalite/1.8/amalgalite3.so +0 -0
- data/lib/amalgalite/1.9/amalgalite3.so +0 -0
- data/lib/amalgalite/aggregate.rb +67 -0
- data/lib/amalgalite/blob.rb +186 -0
- data/lib/amalgalite/boolean.rb +42 -0
- data/lib/amalgalite/busy_timeout.rb +47 -0
- data/lib/amalgalite/column.rb +97 -0
- data/lib/amalgalite/core_ext/kernel/require.rb +21 -0
- data/lib/amalgalite/database.rb +947 -0
- data/lib/amalgalite/function.rb +61 -0
- data/lib/amalgalite/index.rb +43 -0
- data/lib/amalgalite/packer.rb +226 -0
- data/lib/amalgalite/paths.rb +70 -0
- data/lib/amalgalite/profile_tap.rb +131 -0
- data/lib/amalgalite/progress_handler.rb +21 -0
- data/lib/amalgalite/requires.rb +120 -0
- data/lib/amalgalite/schema.rb +191 -0
- data/lib/amalgalite/sqlite3.rb +6 -0
- data/lib/amalgalite/sqlite3/constants.rb +80 -0
- data/lib/amalgalite/sqlite3/database/function.rb +48 -0
- data/lib/amalgalite/sqlite3/database/status.rb +68 -0
- data/lib/amalgalite/sqlite3/status.rb +60 -0
- data/lib/amalgalite/sqlite3/version.rb +37 -0
- data/lib/amalgalite/statement.rb +414 -0
- data/lib/amalgalite/table.rb +90 -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 +40 -0
- data/lib/amalgalite/type_maps/text_map.rb +22 -0
- data/lib/amalgalite/version.rb +37 -0
- data/lib/amalgalite/view.rb +26 -0
- data/spec/aggregate_spec.rb +169 -0
- data/spec/amalgalite_spec.rb +4 -0
- data/spec/blob_spec.rb +81 -0
- data/spec/boolean_spec.rb +23 -0
- data/spec/busy_handler.rb +165 -0
- data/spec/database_spec.rb +494 -0
- data/spec/default_map_spec.rb +87 -0
- data/spec/function_spec.rb +94 -0
- data/spec/integeration_spec.rb +111 -0
- data/spec/packer_spec.rb +60 -0
- data/spec/paths_spec.rb +28 -0
- data/spec/progress_handler_spec.rb +105 -0
- data/spec/requires_spec.rb +23 -0
- data/spec/rtree_spec.rb +71 -0
- data/spec/schema_spec.rb +120 -0
- data/spec/spec_helper.rb +27 -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 +53 -0
- data/spec/statement_spec.rb +161 -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 +15 -0
- data/tasks/announce.rake +43 -0
- data/tasks/config.rb +107 -0
- data/tasks/distribution.rake +77 -0
- data/tasks/documentation.rake +32 -0
- data/tasks/extension.rake +141 -0
- data/tasks/rspec.rake +33 -0
- data/tasks/rubyforge.rake +59 -0
- data/tasks/utils.rb +80 -0
- metadata +237 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
require 'set'
|
6
|
+
module Amalgalite
|
7
|
+
#
|
8
|
+
# a class representing the meta information about an SQLite table
|
9
|
+
#
|
10
|
+
class Table
|
11
|
+
# the schema object the table is associated with
|
12
|
+
attr_accessor :schema
|
13
|
+
|
14
|
+
# the table name
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
# the original sql that was used to create this table
|
18
|
+
attr_reader :sql
|
19
|
+
|
20
|
+
# hash of Index objects holding the meta informationa about the indexes
|
21
|
+
# on this table. The keys of the indexes variable is the index name
|
22
|
+
attr_accessor :indexes
|
23
|
+
|
24
|
+
# a hash of Column objects holding the meta information about the columns
|
25
|
+
# in this table. keys are the column names
|
26
|
+
attr_accessor :columns
|
27
|
+
|
28
|
+
def initialize( name, sql = nil )
|
29
|
+
@name = name
|
30
|
+
@sql = sql
|
31
|
+
@indexes = {}
|
32
|
+
@columns = {}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Is the table a temporary table or not
|
36
|
+
def temporary?
|
37
|
+
!sql
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# the Columns in original definition order
|
42
|
+
def columns_in_order
|
43
|
+
@columns.values.sort_by { |c| c.order }
|
44
|
+
end
|
45
|
+
|
46
|
+
# the column names in original definition order
|
47
|
+
def column_names
|
48
|
+
columns_in_order.map { |c| c.name }
|
49
|
+
end
|
50
|
+
|
51
|
+
# the columns that make up the primary key
|
52
|
+
def primary_key_columns
|
53
|
+
@columns.values.find_all { |c| c.primary_key? }
|
54
|
+
end
|
55
|
+
|
56
|
+
# the array of colmuns that make up the primary key of the table
|
57
|
+
# since a primary key has an index, we loop over all the indexes for the
|
58
|
+
# table and pick the first one that is unique, and all the columns in the
|
59
|
+
# index have primary_key? as true.
|
60
|
+
#
|
61
|
+
# we do this instead of just looking for the columns where primary key is
|
62
|
+
# true because we want the columns in primary key order
|
63
|
+
def primary_key
|
64
|
+
unless @primary_key
|
65
|
+
pk_column_names = Set.new( primary_key_columns.collect { |c| c.name } )
|
66
|
+
unique_indexes = indexes.values.find_all { |i| i.unique? }
|
67
|
+
|
68
|
+
pk_result = []
|
69
|
+
|
70
|
+
unique_indexes.each do |idx|
|
71
|
+
idx_column_names = Set.new( idx.columns.collect { |c| c.name } )
|
72
|
+
r = idx_column_names ^ pk_column_names
|
73
|
+
if r.size == 0 then
|
74
|
+
pk_result = idx.columns
|
75
|
+
break
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# no joy, see about just using all the columns that say the are primary
|
80
|
+
# keys
|
81
|
+
if pk_result.empty? then
|
82
|
+
pk_result = self.primary_key_columns
|
83
|
+
end
|
84
|
+
@primary_key = pk_result
|
85
|
+
end
|
86
|
+
return @primary_key
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'amalgalite/taps/io'
|
7
|
+
|
8
|
+
module Amalgalite::Taps
|
9
|
+
#
|
10
|
+
# Class provide an IO tap that can write to $stdout
|
11
|
+
#
|
12
|
+
class Stdout < ::Amalgalite::Taps::IO
|
13
|
+
def initialize
|
14
|
+
super( $stdout )
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# This class provide an IO tap that can write to $stderr
|
20
|
+
#
|
21
|
+
class Stderr < ::Amalgalite::Taps::IO
|
22
|
+
def initialize
|
23
|
+
super( $stderr )
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'amalgalite/profile_tap'
|
7
|
+
require 'stringio'
|
8
|
+
|
9
|
+
module Amalgalite
|
10
|
+
module Taps
|
11
|
+
#
|
12
|
+
# An IOTap is an easy way to send all top information to andy IO based
|
13
|
+
# object. Both profile and trace tap information can be captured
|
14
|
+
# This means you can send the events to STDOUT with:
|
15
|
+
#
|
16
|
+
# db.profile_tap = db.trace_tap = Amalgalite::Taps::Stdout.new
|
17
|
+
#
|
18
|
+
#
|
19
|
+
class IO
|
20
|
+
|
21
|
+
attr_reader :profile_tap
|
22
|
+
attr_reader :io
|
23
|
+
|
24
|
+
def initialize( io )
|
25
|
+
@io = io
|
26
|
+
@profile_tap = ProfileTap.new( self, 'output_profile_event' )
|
27
|
+
end
|
28
|
+
|
29
|
+
def trace( msg )
|
30
|
+
io.puts msg
|
31
|
+
end
|
32
|
+
|
33
|
+
# need a profile method, it routes through the profile tap which calls back
|
34
|
+
# to output_profile_event
|
35
|
+
def profile( msg, time )
|
36
|
+
@profile_tap.profile(msg, time)
|
37
|
+
end
|
38
|
+
|
39
|
+
def output_profile_event( msg, time )
|
40
|
+
io.puts "#{time} : #{msg}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def dump_profile
|
44
|
+
samplers.each_pair do |k,v|
|
45
|
+
io.puts v.to_s
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def samplers
|
50
|
+
profile_tap.samplers
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# This class provides an IO tap that writes to a StringIO. The result is
|
56
|
+
# available via .to_s or .string.
|
57
|
+
#
|
58
|
+
class StringIO < ::Amalgalite::Taps::IO
|
59
|
+
def initialize
|
60
|
+
@stringio = ::StringIO.new
|
61
|
+
super( @stringio )
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
@stringio.string
|
66
|
+
end
|
67
|
+
alias :string :to_s
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,35 @@
|
|
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 TraceTap receives tracing information from SQLite3. It receives the SQL
|
9
|
+
# statement being executed as a +msg+ just before the statement first begins
|
10
|
+
# executing.
|
11
|
+
#
|
12
|
+
# A TraceTap is a wrapper around another object and a method. The Tap object
|
13
|
+
# will receive the call to +trace+ and redirect that call to another object
|
14
|
+
# and method.
|
15
|
+
#
|
16
|
+
class TraceTap
|
17
|
+
|
18
|
+
attr_reader :delegate_obj
|
19
|
+
attr_reader :delegate_method
|
20
|
+
|
21
|
+
def initialize( wrapped_obj, send_to = 'trace' )
|
22
|
+
unless wrapped_obj.respond_to?( send_to )
|
23
|
+
raise Amalgalite::Error, "#{wrapped_obj.class.name} does not respond to #{send_to.to_s} "
|
24
|
+
end
|
25
|
+
|
26
|
+
@delegate_obj = wrapped_obj
|
27
|
+
@delegate_method = send_to
|
28
|
+
end
|
29
|
+
|
30
|
+
def trace( msg )
|
31
|
+
delegate_obj.send( delegate_method, msg )
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
module Amalgalite
|
6
|
+
##
|
7
|
+
# TypeMap defines the protocol used between Ruby and SQLite for mapping
|
8
|
+
# binding types, used in prepared statements; and result types, used in
|
9
|
+
# returning objects from a query.
|
10
|
+
#
|
11
|
+
#
|
12
|
+
class TypeMap
|
13
|
+
##
|
14
|
+
# :call-seq:
|
15
|
+
# map.bind_type_of( obj ) -> DataType constant
|
16
|
+
#
|
17
|
+
# bind_type_of is called during the Statement#bind process to convert the
|
18
|
+
# bind parameter to the appropriate SQLite types. This method MUST return
|
19
|
+
# one of the valid constants in the namespace
|
20
|
+
# Amalgalite::SQLite::Constants::DataType
|
21
|
+
#
|
22
|
+
def bind_type_of( obj )
|
23
|
+
raise NotImplementedError, "bind_type_of has not been implemented"
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# :call-seq:
|
28
|
+
# map.result_value_of( declared_type, value ) -> String
|
29
|
+
#
|
30
|
+
# result_value_of is called during the result processing of column values
|
31
|
+
# to convert an SQLite database value into the appropriate Ruby class.
|
32
|
+
#
|
33
|
+
# +declared_type+ is the string from the original CREATE TABLE statment
|
34
|
+
# from which the column value originates. It may also be nil if the origin
|
35
|
+
# column cannot be determined.
|
36
|
+
#
|
37
|
+
# +value+ is the SQLite value from the column as either a Ruby String,
|
38
|
+
# Integer, Float or Amalgalite::Blob.
|
39
|
+
#
|
40
|
+
# result_value should return the value that is to be put into the result set
|
41
|
+
# for the query. It may do nothing, or it may do massive amounts of
|
42
|
+
# conversion.
|
43
|
+
def result_value_of( delcared_type, value )
|
44
|
+
raise NotImplementedError, "result_value_of has not been implemented"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# The TypeMaps module holds all typemaps that ship with Amagalite. They
|
50
|
+
# currently are:
|
51
|
+
#
|
52
|
+
# DefaultMap:: does a 'best-guess' mapping to convert as many types as
|
53
|
+
# possible to known ruby classes from known SQL types.
|
54
|
+
# StorageMap:: converts to a limited set of classes directly based
|
55
|
+
# upon the SQLite storage types
|
56
|
+
# TextMap:: Everything is Text ... everything everything everything
|
57
|
+
#
|
58
|
+
module TypeMaps
|
59
|
+
end
|
60
|
+
end
|
61
|
+
require 'amalgalite/type_maps/default_map'
|
62
|
+
require 'amalgalite/type_maps/storage_map'
|
63
|
+
require 'amalgalite/type_maps/text_map'
|
@@ -0,0 +1,167 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 Jeremy Hinegardner
|
3
|
+
# All rights reserved. See LICENSE and/or COPYING for details.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'amalgalite/type_map'
|
7
|
+
require 'time'
|
8
|
+
require 'date'
|
9
|
+
|
10
|
+
module Amalgalite::TypeMaps
|
11
|
+
##
|
12
|
+
# An Amalgalite::TypeMap that does its best to convert between Ruby classes
|
13
|
+
# and known SQL data types.
|
14
|
+
#
|
15
|
+
# Upon instantiation, DefaultMap generates a conversion map to try to figure
|
16
|
+
# out the best way to convert between populate SQL 'types' and ruby classes
|
17
|
+
#
|
18
|
+
class DefaultMap
|
19
|
+
class << self
|
20
|
+
def methods_handling_sql_types # :nodoc:
|
21
|
+
@methods_handling_sql_types ||= {
|
22
|
+
'date' => %w[ date ],
|
23
|
+
'datetime' => %w[ datetime ],
|
24
|
+
'time' => %w[ timestamp time ],
|
25
|
+
'float' => %w[ double real numeric decimal ],
|
26
|
+
'integer' => %w[ integer tinyint smallint int int2 int4 int8 bigint serial bigserial ],
|
27
|
+
'string' => %w[ text char varchar character ],
|
28
|
+
'boolean' => %w[ bool boolean ],
|
29
|
+
'blob' => %w[ binary blob ],
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# say what method to call to convert an sql type to a ruby type
|
34
|
+
#
|
35
|
+
def sql_to_method( sql_type ) # :nodoc:
|
36
|
+
unless defined? @sql_to_method
|
37
|
+
@sql_to_method = {}
|
38
|
+
methods_handling_sql_types.each_pair do |method, sql_types|
|
39
|
+
sql_types.each { |t| @sql_to_method[t] = method }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
return_method = @sql_to_method[sql_type]
|
43
|
+
|
44
|
+
# the straight lookup didn't work, try iterating through the types and
|
45
|
+
# see what is found
|
46
|
+
unless return_method
|
47
|
+
@sql_to_method.each_pair do |sql, method|
|
48
|
+
if sql_type.index(sql) then
|
49
|
+
return_method = method
|
50
|
+
break
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
return return_method
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# A straight logical mapping (for me at least) of basic Ruby classes to SQLite types, if
|
63
|
+
# nothing can be found then default to TEXT.
|
64
|
+
#
|
65
|
+
def bind_type_of( obj )
|
66
|
+
case obj
|
67
|
+
when Float
|
68
|
+
::Amalgalite::SQLite3::Constants::DataType::FLOAT
|
69
|
+
when Fixnum
|
70
|
+
::Amalgalite::SQLite3::Constants::DataType::INTEGER
|
71
|
+
when NilClass
|
72
|
+
::Amalgalite::SQLite3::Constants::DataType::NULL
|
73
|
+
when ::Amalgalite::Blob
|
74
|
+
::Amalgalite::SQLite3::Constants::DataType::BLOB
|
75
|
+
else
|
76
|
+
::Amalgalite::SQLite3::Constants::DataType::TEXT
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Map the incoming value to an outgoing value. For some incoming values,
|
82
|
+
# there will be no change, but for some (i.e. Dates and Times) there is some
|
83
|
+
# conversion
|
84
|
+
#
|
85
|
+
def result_value_of( declared_type, value )
|
86
|
+
case value
|
87
|
+
when Numeric
|
88
|
+
return value
|
89
|
+
when NilClass
|
90
|
+
return value
|
91
|
+
when Amalgalite::Blob
|
92
|
+
return value
|
93
|
+
when String
|
94
|
+
if declared_type then
|
95
|
+
conversion_method = DefaultMap.sql_to_method( declared_type.downcase )
|
96
|
+
if conversion_method then
|
97
|
+
return send(conversion_method, value)
|
98
|
+
else
|
99
|
+
raise ::Amalgalite::Error, "Unable to convert SQL type of #{declared_type} to a Ruby class"
|
100
|
+
end
|
101
|
+
else
|
102
|
+
# unable to do any other conversion, just return what we have.
|
103
|
+
return value
|
104
|
+
end
|
105
|
+
else
|
106
|
+
raise ::Amalgalite::Error, "Unable to convert a class #{value.class.name} with value #{value.inspect}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# convert a string to a date
|
112
|
+
#
|
113
|
+
def date( str )
|
114
|
+
Date.parse( str )
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# convert a string to a datetime, if no timzone is found in the parsed
|
119
|
+
# string, set it to the local offset.
|
120
|
+
#
|
121
|
+
def datetime( str )
|
122
|
+
DateTime.parse( str )
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# convert a string to a Time
|
127
|
+
#
|
128
|
+
def time( str )
|
129
|
+
Time.parse( str )
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# convert a string to a Float
|
134
|
+
#
|
135
|
+
def float( str )
|
136
|
+
Float( str )
|
137
|
+
end
|
138
|
+
|
139
|
+
##
|
140
|
+
# convert an string to an Integer
|
141
|
+
#
|
142
|
+
def integer( str )
|
143
|
+
Float( str ).to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# convert a string to a String, yes redundant I know.
|
148
|
+
#
|
149
|
+
def string( str )
|
150
|
+
str
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# convert a string to true of false
|
155
|
+
#
|
156
|
+
def boolean( str )
|
157
|
+
::Amalgalite::Boolean.to_bool( str )
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# convert a string to a blob
|
162
|
+
#
|
163
|
+
def blob( str )
|
164
|
+
::Amalgalite::Blob.new( :string => str )
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|