presto-client 0.5.14 → 0.6.4
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 +5 -5
- data/.github/CODEOWNERS +1 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +18 -0
- data/.travis.yml +6 -6
- data/ChangeLog.md +160 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +23 -9
- data/Rakefile +4 -4
- data/lib/presto/client/faraday_client.rb +6 -1
- data/lib/presto/client/model_versions/303.rb +2574 -0
- data/lib/presto/client/model_versions/316.rb +2595 -0
- data/lib/presto/client/models.rb +3 -1
- data/lib/presto/client/query.rb +3 -5
- data/lib/presto/client/statement_client.rb +50 -36
- data/lib/presto/client/version.rb +1 -1
- data/modelgen/model_versions.rb +27 -3
- data/modelgen/modelgen.rb +16 -14
- data/modelgen/presto_models.rb +29 -9
- data/presto-client.gemspec +2 -1
- data/release.rb +56 -0
- data/spec/basic_query_spec.rb +82 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/statement_client_spec.rb +79 -18
- data/spec/tpch/q01.sql +21 -0
- data/spec/tpch/q02.sql +43 -0
- data/spec/tpch_query_spec.rb +41 -0
- metadata +33 -6
- data/ChangeLog +0 -171
data/lib/presto/client/models.rb
CHANGED
@@ -29,7 +29,9 @@ module Presto::Client
|
|
29
29
|
require 'presto/client/model_versions/0.173.rb'
|
30
30
|
require 'presto/client/model_versions/0.178.rb'
|
31
31
|
require 'presto/client/model_versions/0.205.rb'
|
32
|
+
require 'presto/client/model_versions/303.rb'
|
33
|
+
require 'presto/client/model_versions/316.rb'
|
32
34
|
|
33
|
-
Models = ModelVersions::
|
35
|
+
Models = ModelVersions::V316
|
34
36
|
|
35
37
|
end
|
data/lib/presto/client/query.rb
CHANGED
@@ -16,6 +16,7 @@
|
|
16
16
|
module Presto::Client
|
17
17
|
|
18
18
|
require 'faraday'
|
19
|
+
require 'faraday_middleware'
|
19
20
|
require 'presto/client/models'
|
20
21
|
require 'presto/client/errors'
|
21
22
|
require 'presto/client/faraday_client'
|
@@ -125,16 +126,13 @@ module Presto::Client
|
|
125
126
|
end
|
126
127
|
|
127
128
|
def close
|
128
|
-
@api.
|
129
|
+
@api.close
|
129
130
|
nil
|
130
131
|
end
|
131
132
|
|
132
133
|
def raise_if_failed
|
133
|
-
if @api.
|
134
|
+
if @api.client_aborted?
|
134
135
|
raise PrestoClientError, "Query aborted by user"
|
135
|
-
elsif @api.exception?
|
136
|
-
# query is gone
|
137
|
-
raise @api.exception
|
138
136
|
elsif @api.query_failed?
|
139
137
|
results = @api.current_results
|
140
138
|
error = results.error
|
@@ -31,8 +31,7 @@ module Presto::Client
|
|
31
31
|
|
32
32
|
@options = options
|
33
33
|
@query = query
|
34
|
-
@
|
35
|
-
@exception = nil
|
34
|
+
@state = :running
|
36
35
|
@retry_timeout = options[:retry_timeout] || 120
|
37
36
|
if model_version = @options[:model_version]
|
38
37
|
@models = ModelVersions.const_get("V#{model_version.gsub(".", "_")}")
|
@@ -76,7 +75,7 @@ module Presto::Client
|
|
76
75
|
|
77
76
|
# TODO error handling
|
78
77
|
if response.status != 200
|
79
|
-
|
78
|
+
exception! PrestoHttpError.new(response.status, "Failed to start query: #{response.body} (#{response.status})")
|
80
79
|
end
|
81
80
|
|
82
81
|
@results_headers = response.headers
|
@@ -91,14 +90,20 @@ module Presto::Client
|
|
91
90
|
!!@options[:debug]
|
92
91
|
end
|
93
92
|
|
94
|
-
def
|
95
|
-
@
|
93
|
+
def running?
|
94
|
+
@state == :running
|
96
95
|
end
|
97
96
|
|
98
|
-
|
97
|
+
def client_aborted?
|
98
|
+
@state == :client_aborted
|
99
|
+
end
|
100
|
+
|
101
|
+
def client_error?
|
102
|
+
@state == :client_error
|
103
|
+
end
|
99
104
|
|
100
|
-
def
|
101
|
-
@
|
105
|
+
def finished?
|
106
|
+
@state == :finished
|
102
107
|
end
|
103
108
|
|
104
109
|
def query_failed?
|
@@ -106,7 +111,7 @@ module Presto::Client
|
|
106
111
|
end
|
107
112
|
|
108
113
|
def query_succeeded?
|
109
|
-
@results.error == nil &&
|
114
|
+
@results.error == nil && finished?
|
110
115
|
end
|
111
116
|
|
112
117
|
def current_results
|
@@ -117,16 +122,29 @@ module Presto::Client
|
|
117
122
|
@results_headers
|
118
123
|
end
|
119
124
|
|
125
|
+
def query_id
|
126
|
+
@results.id
|
127
|
+
end
|
128
|
+
|
120
129
|
def has_next?
|
121
130
|
!!@results.next_uri
|
122
131
|
end
|
123
132
|
|
133
|
+
def exception!(e)
|
134
|
+
@state = :client_error
|
135
|
+
raise e
|
136
|
+
end
|
137
|
+
|
124
138
|
def advance
|
125
|
-
|
139
|
+
return false unless running?
|
140
|
+
|
141
|
+
unless has_next?
|
142
|
+
@state = :finished
|
126
143
|
return false
|
127
144
|
end
|
128
145
|
|
129
146
|
uri = @results.next_uri
|
147
|
+
|
130
148
|
response = faraday_get_with_retry(uri)
|
131
149
|
@results_headers = response.headers
|
132
150
|
@results = decode_model(uri, parse_body(response), @models::QueryResults)
|
@@ -150,8 +168,7 @@ module Presto::Client
|
|
150
168
|
if body.size > 1024 + 3
|
151
169
|
body = "#{body[0, 1024]}..."
|
152
170
|
end
|
153
|
-
|
154
|
-
raise @exception
|
171
|
+
exception! PrestoHttpError.new(500, "Presto API returned unexpected structure at #{uri}. Expected #{body_class} but got #{body}: #{e}")
|
155
172
|
end
|
156
173
|
end
|
157
174
|
|
@@ -166,8 +183,7 @@ module Presto::Client
|
|
166
183
|
JSON.parse(response.body, opts = JSON_OPTIONS)
|
167
184
|
end
|
168
185
|
rescue => e
|
169
|
-
|
170
|
-
raise @exception
|
186
|
+
exception! PrestoHttpError.new(500, "Presto API returned unexpected data format. #{e}")
|
171
187
|
end
|
172
188
|
end
|
173
189
|
|
@@ -184,8 +200,7 @@ module Presto::Client
|
|
184
200
|
# temporally error to retry
|
185
201
|
response = nil
|
186
202
|
rescue => e
|
187
|
-
|
188
|
-
raise @exception
|
203
|
+
exception! e
|
189
204
|
end
|
190
205
|
|
191
206
|
if response
|
@@ -195,8 +210,7 @@ module Presto::Client
|
|
195
210
|
|
196
211
|
if response.status != 503 # retry only if 503 Service Unavailable
|
197
212
|
# deterministic error
|
198
|
-
|
199
|
-
raise @exception
|
213
|
+
exception! PrestoHttpError.new(response.status, "Presto API error at #{uri} returned #{response.status}: #{response.body}")
|
200
214
|
end
|
201
215
|
end
|
202
216
|
|
@@ -204,18 +218,14 @@ module Presto::Client
|
|
204
218
|
|
205
219
|
attempts += 1
|
206
220
|
sleep attempts * 0.1
|
207
|
-
end while (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) < @retry_timeout &&
|
221
|
+
end while (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) < @retry_timeout && !client_aborted?
|
208
222
|
|
209
|
-
|
210
|
-
raise @exception
|
223
|
+
exception! PrestoHttpError.new(408, "Presto API error due to timeout")
|
211
224
|
end
|
212
225
|
|
213
226
|
def raise_if_timeout!
|
214
227
|
if @started_at
|
215
|
-
if
|
216
|
-
# query is already done
|
217
|
-
return
|
218
|
-
end
|
228
|
+
return if finished?
|
219
229
|
|
220
230
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
|
221
231
|
|
@@ -234,30 +244,34 @@ module Presto::Client
|
|
234
244
|
|
235
245
|
def raise_timeout_error!
|
236
246
|
if query_id = @results && @results.id
|
237
|
-
|
247
|
+
exception! PrestoQueryTimeoutError.new("Query #{query_id} timed out")
|
238
248
|
else
|
239
|
-
|
249
|
+
exception! PrestoQueryTimeoutError.new("Query timed out")
|
240
250
|
end
|
241
251
|
end
|
242
252
|
|
243
253
|
def cancel_leaf_stage
|
244
|
-
if uri = @results.
|
245
|
-
|
254
|
+
if uri = @results.partial_cancel_uri
|
255
|
+
@faraday.delete do |req|
|
246
256
|
req.url uri
|
247
257
|
end
|
248
|
-
return response.status / 100 == 2
|
249
258
|
end
|
250
|
-
return false
|
251
259
|
end
|
252
260
|
|
253
261
|
def close
|
254
|
-
return
|
262
|
+
return unless running?
|
255
263
|
|
256
|
-
|
257
|
-
|
258
|
-
|
264
|
+
@state = :client_aborted
|
265
|
+
|
266
|
+
begin
|
267
|
+
if uri = @results.next_uri
|
268
|
+
@faraday.delete do |req|
|
269
|
+
req.url uri
|
270
|
+
end
|
271
|
+
end
|
272
|
+
rescue => e
|
273
|
+
end
|
259
274
|
|
260
|
-
@closed = true
|
261
275
|
nil
|
262
276
|
end
|
263
277
|
end
|
data/modelgen/model_versions.rb
CHANGED
@@ -131,6 +131,7 @@ module Presto::Client::ModelVersions
|
|
131
131
|
when "apply" then ApplyNode
|
132
132
|
when "assignUniqueId" then AssignUniqueId
|
133
133
|
when "lateralJoin" then LateralJoinNode
|
134
|
+
when "statisticsWriterNode" then StatisticsWriterNode
|
134
135
|
end
|
135
136
|
if model_class
|
136
137
|
node = model_class.decode(hash)
|
@@ -197,9 +198,25 @@ module Presto::Client::ModelVersions
|
|
197
198
|
end
|
198
199
|
obj = allocate
|
199
200
|
model_class = case hash["@type"]
|
200
|
-
when "
|
201
|
-
when "
|
202
|
-
when "
|
201
|
+
when "CreateTarget" then CreateTarget
|
202
|
+
when "InsertTarget" then InsertTarget
|
203
|
+
when "DeleteTarget" then DeleteTarget
|
204
|
+
end
|
205
|
+
if model_class
|
206
|
+
model_class.decode(hash)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class << WriteStatisticsTarget =
|
212
|
+
Base.new(:type, :handle)
|
213
|
+
def decode(hash)
|
214
|
+
unless hash.is_a?(Hash)
|
215
|
+
raise TypeError, "Can't convert #{hash.class} to Hash"
|
216
|
+
end
|
217
|
+
obj = allocate
|
218
|
+
model_class = case hash["@type"]
|
219
|
+
when "WriteStatisticsHandle" then WriteStatisticsHandle
|
203
220
|
end
|
204
221
|
if model_class
|
205
222
|
model_class.decode(hash)
|
@@ -246,6 +263,13 @@ module Presto::Client::ModelVersions
|
|
246
263
|
end
|
247
264
|
end
|
248
265
|
|
266
|
+
class ResourceGroupId < Array
|
267
|
+
def initialize(array)
|
268
|
+
super()
|
269
|
+
concat(array)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
249
273
|
##
|
250
274
|
# Those model classes are automatically generated
|
251
275
|
#
|
data/modelgen/modelgen.rb
CHANGED
@@ -13,13 +13,13 @@ erb = ERB.new(File.read(template_path))
|
|
13
13
|
|
14
14
|
source_path = source_dir
|
15
15
|
|
16
|
-
predefined_simple_classes = %w[StageId TaskId Lifespan ConnectorSession]
|
17
|
-
predefined_models = %w[DistributionSnapshot PlanNode EquiJoinClause WriterTarget OperatorInfo HashCollisionsInfo]
|
16
|
+
predefined_simple_classes = %w[StageId TaskId Lifespan ConnectorSession ResourceGroupId]
|
17
|
+
predefined_models = %w[DistributionSnapshot PlanNode EquiJoinClause WriterTarget WriteStatisticsTarget OperatorInfo HashCollisionsInfo]
|
18
18
|
|
19
|
-
assume_primitive = %w[Object Type Long Symbol QueryId PlanNodeId PlanFragmentId MemoryPoolId TransactionId URI Duration DataSize DateTime ColumnHandle ConnectorTableHandle ConnectorOutputTableHandle ConnectorIndexHandle ConnectorColumnHandle ConnectorInsertTableHandle ConnectorTableLayoutHandle Expression FunctionCall TimeZoneKey Locale TypeSignature Frame TupleDomain<ColumnHandle> SerializableNativeValue ConnectorTransactionHandle OutputBufferId ConnectorPartitioningHandle NullableValue ConnectorId HostAddress JsonNode Node]
|
20
|
-
enum_types = %w[QueryState StageState TaskState QueueState PlanDistribution OutputPartitioning Step SortOrder BufferState NullPartitioning BlockedReason ParameterKind FunctionKind PartitionFunctionHandle Scope ErrorType DistributionType PipelineExecutionStrategy JoinType]
|
19
|
+
assume_primitive = %w[Object Type Long Symbol QueryId PlanNodeId PlanFragmentId MemoryPoolId TransactionId URI Duration DataSize DateTime ColumnHandle ConnectorTableHandle ConnectorOutputTableHandle ConnectorIndexHandle ConnectorColumnHandle ConnectorInsertTableHandle ConnectorTableLayoutHandle Expression FunctionCall TimeZoneKey Locale TypeSignature Frame TupleDomain<ColumnHandle> SerializableNativeValue ConnectorTransactionHandle OutputBufferId ConnectorPartitioningHandle NullableValue ConnectorId HostAddress JsonNode Node CatalogName QualifiedObjectName]
|
20
|
+
enum_types = %w[QueryState StageState TaskState QueueState PlanDistribution OutputPartitioning Step SortOrder BufferState NullPartitioning BlockedReason ParameterKind FunctionKind PartitionFunctionHandle Scope ErrorType DistributionType PipelineExecutionStrategy JoinType ExchangeNode.Type ColumnStatisticType TableStatisticType StageExecutionStrategy SemanticErrorCode]
|
21
21
|
|
22
|
-
root_models = %w[QueryResults QueryInfo] + %w[
|
22
|
+
root_models = %w[QueryResults QueryInfo BasicQueryInfo] + %w[
|
23
23
|
OutputNode
|
24
24
|
ProjectNode
|
25
25
|
TableScanNode
|
@@ -42,7 +42,6 @@ IndexJoinNode
|
|
42
42
|
IndexSourceNode
|
43
43
|
TableWriterNode
|
44
44
|
DeleteNode
|
45
|
-
MetadataDeleteNode
|
46
45
|
TableFinishNode
|
47
46
|
UnnestNode
|
48
47
|
ExchangeNode
|
@@ -54,6 +53,7 @@ ExplainAnalyzeNode
|
|
54
53
|
ApplyNode
|
55
54
|
AssignUniqueId
|
56
55
|
LateralJoinNode
|
56
|
+
StatisticsWriterNode
|
57
57
|
] + %w[
|
58
58
|
ExchangeClientStatus
|
59
59
|
LocalExchangeBufferInfo
|
@@ -62,7 +62,8 @@ SplitOperatorInfo
|
|
62
62
|
PartitionedOutputInfo
|
63
63
|
JoinOperatorInfo
|
64
64
|
WindowInfo
|
65
|
-
TableWriterInfo
|
65
|
+
TableWriterInfo
|
66
|
+
]
|
66
67
|
|
67
68
|
name_mapping = Hash[*%w[
|
68
69
|
StatementStats StageStats ClientStageStats
|
@@ -71,13 +72,14 @@ QueryResults Column ClientColumn
|
|
71
72
|
].each_slice(3).map { |x, y, z| [[x,y], z] }.flatten(1)]
|
72
73
|
|
73
74
|
path_mapping = Hash[*%w[
|
74
|
-
ClientColumn presto-client/src/main/java/
|
75
|
-
ClientStageStats presto-client/src/main/java/
|
76
|
-
Column presto-main/src/main/java/
|
77
|
-
QueryStats presto-main/src/main/java/
|
78
|
-
StageStats presto-main/src/main/java/
|
79
|
-
PartitionedOutputInfo presto-main/src/main/java/
|
80
|
-
TableWriterInfo presto-main/src/main/java/
|
75
|
+
ClientColumn presto-client/src/main/java/io/prestosql/client/Column.java
|
76
|
+
ClientStageStats presto-client/src/main/java/io/prestosql/client/StageStats.java
|
77
|
+
Column presto-main/src/main/java/io/prestosql/execution/Column.java
|
78
|
+
QueryStats presto-main/src/main/java/io/prestosql/execution/QueryStats.java
|
79
|
+
StageStats presto-main/src/main/java/io/prestosql/execution/StageStats.java
|
80
|
+
PartitionedOutputInfo presto-main/src/main/java/io/prestosql/operator/PartitionedOutputOperator.java
|
81
|
+
TableWriterInfo presto-main/src/main/java/io/prestosql/operator/TableWriterOperator.java
|
82
|
+
TableInfo presto-main/src/main/java/io/prestosql/execution/TableInfo.java
|
81
83
|
].map.with_index { |v,i| i % 2 == 0 ? v : (source_path + "/" + v) }]
|
82
84
|
|
83
85
|
# model => [ [key,nullable,type], ... ]
|
data/modelgen/presto_models.rb
CHANGED
@@ -3,13 +3,13 @@ module PrestoModels
|
|
3
3
|
require 'find'
|
4
4
|
require 'stringio'
|
5
5
|
|
6
|
-
PRIMITIVE_TYPES = %w[String boolean long int short byte double float Integer]
|
6
|
+
PRIMITIVE_TYPES = %w[String boolean long int short byte double float Integer Double Boolean]
|
7
7
|
ARRAY_PRIMITIVE_TYPES = PRIMITIVE_TYPES.map { |t| "#{t}[]" }
|
8
8
|
|
9
9
|
class Model < Struct.new(:name, :fields)
|
10
10
|
end
|
11
11
|
|
12
|
-
class Field < Struct.new(:key, :nullable, :array, :map, :type, :base_type, :map_value_base_type)
|
12
|
+
class Field < Struct.new(:key, :nullable, :array, :map, :type, :base_type, :map_value_base_type, :base_type_alias)
|
13
13
|
alias_method :nullable?, :nullable
|
14
14
|
alias_method :array?, :array
|
15
15
|
alias_method :map?, :map
|
@@ -47,10 +47,12 @@ module PrestoModels
|
|
47
47
|
|
48
48
|
private
|
49
49
|
|
50
|
-
PROPERTY_PATTERN = /@JsonProperty\(\"(\w+)\"\)\s+(@Nullable\s+)?([\w\<\>\[\]\,\s]+)\s+\w+/
|
50
|
+
PROPERTY_PATTERN = /@JsonProperty\(\"(\w+)\"\)\s+(@Nullable\s+)?([\w\<\>\[\]\,\s\.]+)\s+\w+/
|
51
51
|
CREATOR_PATTERN = /@JsonCreator[\s]+public[\s]+(static\s+)?(\w+)[\w\s]*\((?:\s*#{PROPERTY_PATTERN}\s*,?)+\)/
|
52
|
+
GENERIC_PATTERN = /(\w+)\<(\w+)\>/
|
52
53
|
|
53
|
-
def analyze_fields(model_name, creator_block)
|
54
|
+
def analyze_fields(model_name, creator_block, generic: nil)
|
55
|
+
model_name = "#{model_name}_#{generic}" if generic
|
54
56
|
extra = @extra_fields[model_name] || []
|
55
57
|
fields = creator_block.scan(PROPERTY_PATTERN).concat(extra).map do |key,nullable,type|
|
56
58
|
map = false
|
@@ -63,7 +65,7 @@ module PrestoModels
|
|
63
65
|
base_type = m[1]
|
64
66
|
map_value_base_type = m[2]
|
65
67
|
map = true
|
66
|
-
elsif m = /Optional<([\w\[\]]+)>/.match(type)
|
68
|
+
elsif m = /Optional<([\w\[\]\<\>]+)>/.match(type)
|
67
69
|
base_type = m[1]
|
68
70
|
nullable = true
|
69
71
|
elsif m = /OptionalInt/.match(type)
|
@@ -72,6 +74,9 @@ module PrestoModels
|
|
72
74
|
elsif m = /OptionalLong/.match(type)
|
73
75
|
base_type = 'Long'
|
74
76
|
nullable = true
|
77
|
+
elsif m = /OptionalDouble/.match(type)
|
78
|
+
base_type = 'Double'
|
79
|
+
nullable = true
|
75
80
|
elsif type =~ /\w+/
|
76
81
|
base_type = type
|
77
82
|
else
|
@@ -79,7 +84,16 @@ module PrestoModels
|
|
79
84
|
end
|
80
85
|
base_type = @name_mapping[[model_name, base_type]] || base_type
|
81
86
|
map_value_base_type = @name_mapping[[model_name, map_value_base_type]] || map_value_base_type
|
82
|
-
|
87
|
+
|
88
|
+
if generic
|
89
|
+
base_type = generic if base_type == 'T'
|
90
|
+
map_value_base_type = generic if map_value_base_type == 'T'
|
91
|
+
end
|
92
|
+
if m = GENERIC_PATTERN.match(base_type)
|
93
|
+
base_type_alias = "#{m[1]}_#{m[2]}"
|
94
|
+
end
|
95
|
+
|
96
|
+
Field.new(key, !!nullable, array, map, type, base_type, map_value_base_type, base_type_alias)
|
83
97
|
end
|
84
98
|
|
85
99
|
@models[model_name] = Model.new(model_name, fields)
|
@@ -92,9 +106,15 @@ module PrestoModels
|
|
92
106
|
return fields
|
93
107
|
end
|
94
108
|
|
95
|
-
def analyze_model(model_name, parent_model
|
109
|
+
def analyze_model(model_name, parent_model= nil, generic: nil)
|
96
110
|
return if @models[model_name] || @ignore_types.include?(model_name)
|
97
111
|
|
112
|
+
if m = GENERIC_PATTERN.match(model_name)
|
113
|
+
analyze_model(m[1], generic: m[2])
|
114
|
+
analyze_model(m[2])
|
115
|
+
return
|
116
|
+
end
|
117
|
+
|
98
118
|
path = find_class_file(model_name, parent_model)
|
99
119
|
java = File.read(path)
|
100
120
|
|
@@ -114,7 +134,7 @@ module PrestoModels
|
|
114
134
|
fields = analyze_fields(inner_model_name, m[0])
|
115
135
|
end
|
116
136
|
|
117
|
-
fields = analyze_fields(model_name, body)
|
137
|
+
fields = analyze_fields(model_name, body, generic: generic)
|
118
138
|
|
119
139
|
rescue => e
|
120
140
|
puts "Skipping model #{parent_model}/#{model_name}: #{e}"
|
@@ -214,7 +234,7 @@ module PrestoModels
|
|
214
234
|
elem_expr = convert_expression(field.base_type, field.base_type, "h")
|
215
235
|
expr << "hash[\"#{field.key}\"].map {|h| #{elem_expr} }"
|
216
236
|
else
|
217
|
-
expr << convert_expression(field.type, field.base_type, "hash[\"#{field.key}\"]")
|
237
|
+
expr << convert_expression(field.type, field.base_type_alias || field.base_type, "hash[\"#{field.key}\"]")
|
218
238
|
end
|
219
239
|
end
|
220
240
|
|