pod4 0.7.2 → 0.8.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 +4 -4
- data/.hgignore +1 -0
- data/.hgtags +1 -0
- data/Gemfile +3 -1
- data/Rakefile +17 -5
- data/lib/pod4/pg_interface.rb +81 -57
- data/lib/pod4/sequel_interface.rb +61 -6
- data/lib/pod4/sql_helper.rb +229 -0
- data/lib/pod4/tds_interface.rb +65 -50
- data/lib/pod4/version.rb +1 -1
- data/md/roadmap.md +81 -25
- data/spec/common/basic_model_spec.rb +5 -0
- data/spec/common/model_spec.rb +49 -7
- data/spec/common/nebulous_interface_spec.rb +2 -0
- data/spec/common/sequel_interface_pg_spec.rb +511 -0
- data/spec/common/sql_helper_spec.rb +299 -0
- data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +525 -0
- data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +520 -0
- data/spec/mri/pg_interface_spec.rb +140 -16
- data/spec/mri/sequel_interface_spec.rb +146 -13
- data/spec/mri/tds_interface_spec.rb +82 -12
- metadata +11 -4
- data/spec/jruby/pg_interface_spec.rb +0 -469
@@ -0,0 +1,511 @@
|
|
1
|
+
#
|
2
|
+
# Supplemental test for Sequel -- tests using PG gem in MRI, jeremyevans-postgres-pg under jruby?
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'pod4/sequel_interface'
|
6
|
+
|
7
|
+
require 'sequel'
|
8
|
+
require 'date'
|
9
|
+
require 'time'
|
10
|
+
require 'bigdecimal'
|
11
|
+
|
12
|
+
|
13
|
+
class TestSequelInterfacePg < SequelInterface
|
14
|
+
set_table :customer
|
15
|
+
set_id_fld :id
|
16
|
+
end
|
17
|
+
|
18
|
+
class SchemaSequelInterfacePg < SequelInterface
|
19
|
+
set_schema :public
|
20
|
+
set_table :customer
|
21
|
+
set_id_fld :id
|
22
|
+
end
|
23
|
+
|
24
|
+
class ProdSequelInterfacePg < SequelInterface
|
25
|
+
set_table :product
|
26
|
+
set_id_fld :code
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
describe TestSequelInterfacePg do
|
33
|
+
|
34
|
+
let(:data) do
|
35
|
+
d = []
|
36
|
+
d << { name: 'Barney',
|
37
|
+
level: 1.23,
|
38
|
+
day: Date.parse("2016-01-01"),
|
39
|
+
timestamp: Time.parse('2015-01-01 12:11'),
|
40
|
+
qty: BigDecimal.new("1.24"),
|
41
|
+
price: BigDecimal.new("1.24") }
|
42
|
+
|
43
|
+
d << { name: 'Fred',
|
44
|
+
level: 2.34,
|
45
|
+
day: Date.parse("2016-02-02"),
|
46
|
+
timestamp: Time.parse('2015-01-02 12:22'),
|
47
|
+
qty: BigDecimal.new("2.35"),
|
48
|
+
price: BigDecimal.new("2.35") }
|
49
|
+
|
50
|
+
d << { name: 'Betty',
|
51
|
+
level: 3.45,
|
52
|
+
day: Date.parse("2016-03-03"),
|
53
|
+
timestamp: Time.parse('2015-01-03 12:33'),
|
54
|
+
qty: BigDecimal.new("3.46"),
|
55
|
+
price: BigDecimal.new("3.46") }
|
56
|
+
|
57
|
+
d
|
58
|
+
end
|
59
|
+
|
60
|
+
def fill_data(ifce)
|
61
|
+
data.each{|r| ifce.create(r) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def fill_product_data(ifce)
|
65
|
+
ifce.create( {code: "foo", name: "bar"} )
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
let (:db) do
|
70
|
+
db = Sequel.connect('postgres://pod4test:pod4test@centos7andy/pod4_test?search_path=public')
|
71
|
+
|
72
|
+
db.run %Q|
|
73
|
+
drop table if exists customer;
|
74
|
+
drop table if exists product;
|
75
|
+
|
76
|
+
create table customer (
|
77
|
+
id serial primary key,
|
78
|
+
name text,
|
79
|
+
level real null,
|
80
|
+
day date null,
|
81
|
+
timestamp timestamp null,
|
82
|
+
price money null,
|
83
|
+
qty numeric null );
|
84
|
+
|
85
|
+
create table product (
|
86
|
+
code text,
|
87
|
+
name text );|
|
88
|
+
|
89
|
+
db
|
90
|
+
end
|
91
|
+
|
92
|
+
let(:interface) { TestSequelInterfacePg.new(db) }
|
93
|
+
let(:prod_interface) { ProdSequelInterfacePg.new(db) }
|
94
|
+
|
95
|
+
|
96
|
+
before do
|
97
|
+
interface.execute %Q|
|
98
|
+
truncate table customer restart identity;
|
99
|
+
truncate table product;|
|
100
|
+
|
101
|
+
fill_data(interface)
|
102
|
+
end
|
103
|
+
|
104
|
+
after do
|
105
|
+
db.disconnect
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
|
110
|
+
|
111
|
+
describe '#quoted_table' do
|
112
|
+
|
113
|
+
it 'returns just the table when the schema is not set' do
|
114
|
+
expect( interface.quoted_table.downcase ).to eq( %Q|"customer"| )
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'returns the schema plus table when the schema is set' do
|
118
|
+
ifce = SchemaSequelInterfacePg.new(db)
|
119
|
+
expect( ifce.quoted_table.downcase ).to eq( %|"public"."customer"| )
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
##
|
124
|
+
|
125
|
+
|
126
|
+
describe '#create' do
|
127
|
+
|
128
|
+
let(:hash) { {name: 'Bam-Bam', qty: 4.44} }
|
129
|
+
let(:ot) { Octothorpe.new(name: 'Wilma', qty: 5.55) }
|
130
|
+
|
131
|
+
it 'raises a Pod4::DatabaseError if anything goes wrong' do
|
132
|
+
expect{ interface.create(one: 'two') }.to raise_exception DatabaseError
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'creates the record when given a hash' do
|
136
|
+
# kinda impossible to seperate these two tests
|
137
|
+
id = interface.create(hash)
|
138
|
+
|
139
|
+
expect( id ).not_to be_nil
|
140
|
+
expect{ interface.read(id) }.not_to raise_exception
|
141
|
+
expect( interface.read(id).to_h ).to include hash
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'creates the record when given an Octothorpe' do
|
145
|
+
id = interface.create(ot)
|
146
|
+
|
147
|
+
expect{ interface.read(id) }.not_to raise_exception
|
148
|
+
expect( interface.read(id).to_h ).to include ot.to_h
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'does not freak out if the hash has symbol values' do
|
152
|
+
# Which, Sequel does
|
153
|
+
expect{ interface.create(name: :Booboo) }.not_to raise_exception
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'shouldnt have a problem with record values of nil' do
|
157
|
+
record = {name: 'Ranger', price: nil}
|
158
|
+
expect{ interface.create(record) }.not_to raise_exception
|
159
|
+
id = interface.create(record)
|
160
|
+
expect( interface.read(id).to_h ).to include(record)
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'shouldnt have a problem with strings containing special characters' do
|
164
|
+
record = {name: "T'Challa[]", price: nil}
|
165
|
+
expect{ interface.create(record) }.not_to raise_exception
|
166
|
+
id = interface.create(record)
|
167
|
+
expect( interface.read(id).to_h ).to include(record)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'shouldn\'t have a problem with non-integer keys' do
|
171
|
+
hash = {code: "foo", name: "bar"}
|
172
|
+
id = prod_interface.create( Octothorpe.new(hash) )
|
173
|
+
|
174
|
+
expect( id ).to eq "foo"
|
175
|
+
expect{ prod_interface.read("foo") }.not_to raise_exception
|
176
|
+
expect( prod_interface.read("foo").to_h ).to include hash
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
##
|
181
|
+
|
182
|
+
|
183
|
+
describe '#read' do
|
184
|
+
|
185
|
+
it 'returns the record for the id as an Octothorpe' do
|
186
|
+
expect( interface.read(2).to_h ).to include(name: 'Fred', qty: 2.35)
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'raises a Pod4::CantContinue if the ID is bad' do
|
190
|
+
expect{ interface.read(:foo) }.to raise_exception CantContinue
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'returns an empty Octothorpe if no record matches the ID' do
|
194
|
+
expect{ interface.read(99) }.not_to raise_exception
|
195
|
+
expect( interface.read(99) ).to be_a_kind_of Octothorpe
|
196
|
+
expect( interface.read(99) ).to be_empty
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'returns real fields as Float' do
|
200
|
+
level = interface.read(1).>>.level
|
201
|
+
|
202
|
+
expect( level ).to be_a_kind_of Float
|
203
|
+
expect( level ).to be_within(0.001).of( data.first[:level] )
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'returns date fields as Date' do
|
207
|
+
date = interface.read(1).>>.day
|
208
|
+
|
209
|
+
expect( date ).to be_a_kind_of Date
|
210
|
+
expect( date ).to eq data.first[:day]
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'returns datetime fields as Time' do
|
214
|
+
timestamp = interface.read(1).>>.timestamp
|
215
|
+
|
216
|
+
expect( timestamp ).to be_a_kind_of Time
|
217
|
+
expect( timestamp ).to eq data.first[:timestamp]
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'returns numeric fields as BigDecimal' do
|
221
|
+
qty = interface.read(1).>>.qty
|
222
|
+
|
223
|
+
expect( qty ).to be_a_kind_of BigDecimal
|
224
|
+
expect( qty ).to eq data.first[:qty]
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'returns money fields as bigdecimal' do
|
228
|
+
pending "Sequel/PG returns a string for Money type"
|
229
|
+
|
230
|
+
price = interface.read(1).>>.price
|
231
|
+
|
232
|
+
expect( price ).to be_a_kind_of BigDecimal
|
233
|
+
expect( price ).to eq data.first[:price]
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'shouldn\'t have a problem with non-integer keys' do
|
237
|
+
# this is a 100% overlap with the create test above...
|
238
|
+
fill_product_data(prod_interface)
|
239
|
+
|
240
|
+
expect{ prod_interface.read("foo") }.not_to raise_exception
|
241
|
+
expect( prod_interface.read("foo").to_h ).to include(code: "foo", name: "bar")
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
##
|
246
|
+
|
247
|
+
|
248
|
+
|
249
|
+
describe '#list' do
|
250
|
+
|
251
|
+
it 'returns an array of Octothorpes that match the records' do
|
252
|
+
arr = interface.list.map {|ot| x = ot.to_h}
|
253
|
+
|
254
|
+
expect( arr.size ).to eq(data.size)
|
255
|
+
|
256
|
+
data.each do |d|
|
257
|
+
r = arr.find{|x| x[:name] == d[:name] }
|
258
|
+
expect( r ).not_to be_nil
|
259
|
+
expect( r[:level] ).to be_within(0.001).of( d[:level] )
|
260
|
+
expect( r[:day] ).to eq d[:day]
|
261
|
+
expect( r[:timestamp] ).to eq d[:timestamp]
|
262
|
+
expect( r[:qty] ).to eq d[:qty]
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'returns a subset of records based on the selection parameter' do
|
268
|
+
expect( interface.list(name: 'Fred').size ).to eq 1
|
269
|
+
|
270
|
+
expect( interface.list(name: 'Betty').first.to_h ).
|
271
|
+
to include(name: 'Betty', qty: 3.46)
|
272
|
+
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'returns an empty Array if nothing matches' do
|
276
|
+
expect( interface.list(name: 'Yogi') ).to eq([])
|
277
|
+
end
|
278
|
+
|
279
|
+
it 'raises DatabaseError if the selection criteria is nonsensical' do
|
280
|
+
expect{ interface.list('foo') }.to raise_exception Pod4::DatabaseError
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'returns an empty array if there is no data' do
|
284
|
+
interface.list.each {|x| interface.delete(x[interface.id_fld]) }
|
285
|
+
expect( interface.list ).to eq([])
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'does not freak out if the hash has symbol values' do
|
289
|
+
# Which, Sequel does
|
290
|
+
expect{ interface.list(name: :Barney) }.not_to raise_exception
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
end
|
295
|
+
##
|
296
|
+
|
297
|
+
|
298
|
+
describe '#update' do
|
299
|
+
|
300
|
+
let(:id) { interface.list.first[:id] }
|
301
|
+
|
302
|
+
it 'updates the record at ID with record parameter' do
|
303
|
+
record = {name: 'Booboo', qty: 99.99}
|
304
|
+
interface.update(id, record)
|
305
|
+
|
306
|
+
booboo = interface.read(id)
|
307
|
+
expect( booboo.>>.name ).to eq( record[:name] )
|
308
|
+
expect( booboo.>>.qty.to_f ).to eq( record[:qty] )
|
309
|
+
end
|
310
|
+
|
311
|
+
it 'raises a CantContinue if anything weird happens with the ID' do
|
312
|
+
expect{ interface.update(99, name: 'Booboo') }.
|
313
|
+
to raise_exception CantContinue
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
it 'raises a DatabaseError if anything weird happensi with the record' do
|
318
|
+
expect{ interface.update(id, smarts: 'more') }.
|
319
|
+
to raise_exception DatabaseError
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'does not freak out if the hash has symbol values' do
|
324
|
+
# Which, Sequel does
|
325
|
+
expect{ interface.update(id, name: :Booboo) }.not_to raise_exception
|
326
|
+
end
|
327
|
+
|
328
|
+
it 'shouldn\'t have a problem with record values of nil' do
|
329
|
+
record = {name: 'Ranger', qty: nil}
|
330
|
+
expect{ interface.update(id, record) }.not_to raise_exception
|
331
|
+
expect( interface.read(id).to_h ).to include(record)
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'shouldn\'t have a problem with strings containing special characters' do
|
335
|
+
record = {name: "T'Challa[]", qty: nil}
|
336
|
+
expect{ interface.update(id, record) }.not_to raise_exception
|
337
|
+
expect( interface.read(id).to_h ).to include(record)
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'shouldn\'t have a problem with non-integer keys' do
|
341
|
+
fill_product_data(prod_interface)
|
342
|
+
expect{ prod_interface.update("foo", name: "baz") }.not_to raise_error
|
343
|
+
expect( prod_interface.read("foo").to_h[:name] ).to eq "baz"
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
##
|
348
|
+
|
349
|
+
|
350
|
+
describe '#delete' do
|
351
|
+
|
352
|
+
def list_contains(ifce, id)
|
353
|
+
ifce.list.find {|x| x[ifce.id_fld] == id }
|
354
|
+
end
|
355
|
+
|
356
|
+
let(:id) { interface.list.first[:id] }
|
357
|
+
|
358
|
+
it 'raises CantContinue if anything hinky happens with the ID' do
|
359
|
+
expect{ interface.delete(:foo) }.to raise_exception CantContinue
|
360
|
+
expect{ interface.delete(99) }.to raise_exception CantContinue
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'makes the record at ID go away' do
|
364
|
+
expect( list_contains(interface, id) ).to be_truthy
|
365
|
+
interface.delete(id)
|
366
|
+
expect( list_contains(interface, id) ).to be_falsy
|
367
|
+
end
|
368
|
+
|
369
|
+
it 'shouldn\'t have a problem with non-integer keys' do
|
370
|
+
fill_product_data(prod_interface)
|
371
|
+
expect( list_contains(prod_interface, "foo") ).to be_truthy
|
372
|
+
prod_interface.delete("foo")
|
373
|
+
expect( list_contains(prod_interface, "foo") ).to be_falsy
|
374
|
+
end
|
375
|
+
|
376
|
+
end
|
377
|
+
##
|
378
|
+
|
379
|
+
|
380
|
+
describe '#execute' do
|
381
|
+
|
382
|
+
let(:sql) { 'delete from customer where qty < 2.0;' }
|
383
|
+
|
384
|
+
it 'requires an SQL string' do
|
385
|
+
expect{ interface.execute }.to raise_exception ArgumentError
|
386
|
+
expect{ interface.execute(nil) }.to raise_exception ArgumentError
|
387
|
+
expect{ interface.execute(14) }.to raise_exception ArgumentError
|
388
|
+
end
|
389
|
+
|
390
|
+
it 'raises some sort of Pod4 error if it runs into problems' do
|
391
|
+
expect{ interface.execute('delete from not_a_table') }.
|
392
|
+
to raise_exception Pod4Error
|
393
|
+
|
394
|
+
end
|
395
|
+
|
396
|
+
it 'executes the string' do
|
397
|
+
expect{ interface.execute(sql) }.not_to raise_exception
|
398
|
+
expect( interface.list.size ).to eq(data.size - 1)
|
399
|
+
expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
|
400
|
+
end
|
401
|
+
|
402
|
+
end
|
403
|
+
##
|
404
|
+
|
405
|
+
|
406
|
+
describe '#select' do
|
407
|
+
|
408
|
+
it 'requires an SQL string' do
|
409
|
+
expect{ interface.select }.to raise_exception ArgumentError
|
410
|
+
expect{ interface.select(nil) }.to raise_exception ArgumentError
|
411
|
+
expect{ interface.select(14) }.to raise_exception ArgumentError
|
412
|
+
end
|
413
|
+
|
414
|
+
it 'raises some sort of Pod4 error if it runs into problems' do
|
415
|
+
expect{ interface.select('select * from not_a_table') }.
|
416
|
+
to raise_exception Pod4Error
|
417
|
+
|
418
|
+
end
|
419
|
+
|
420
|
+
it 'returns the result of the sql' do
|
421
|
+
sql1 = 'select name from customer where qty < 2.0;'
|
422
|
+
sql2 = 'select name from customer where qty < 0.0;'
|
423
|
+
|
424
|
+
expect{ interface.select(sql1) }.not_to raise_exception
|
425
|
+
expect( interface.select(sql1) ).to eq( [{name: 'Barney'}] )
|
426
|
+
expect( interface.select(sql2) ).to eq( [] )
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'works if you pass a non-select' do
|
430
|
+
# By which I mean: still executes the SQL; returns []
|
431
|
+
sql = 'delete from customer where qty < 2.0;'
|
432
|
+
ret = interface.select(sql)
|
433
|
+
|
434
|
+
expect( interface.list.size ).to eq(data.size - 1)
|
435
|
+
expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
|
436
|
+
expect( ret ).to eq( [] )
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
440
|
+
##
|
441
|
+
|
442
|
+
|
443
|
+
describe "#executep" do
|
444
|
+
# For the time being lets assume that Sequel does its job and the three modes we are calling
|
445
|
+
# actually work
|
446
|
+
|
447
|
+
let(:sql) { 'delete from customer where qty < ?;' }
|
448
|
+
|
449
|
+
it 'requires an SQL string and a mode' do
|
450
|
+
expect{ interface.executep }.to raise_exception ArgumentError
|
451
|
+
expect{ interface.executep(nil) }.to raise_exception ArgumentError
|
452
|
+
expect{ interface.executep(14, :update) }.to raise_exception ArgumentError
|
453
|
+
expect{ interface.executep(14, :update, 2) }.to raise_exception ArgumentError
|
454
|
+
end
|
455
|
+
|
456
|
+
it 'requires the mode to be valid' do
|
457
|
+
expect{ interface.executep(sql, :foo, 2) }.to raise_exception ArgumentError
|
458
|
+
end
|
459
|
+
|
460
|
+
it 'raises some sort of Pod4 error if it runs into problems' do
|
461
|
+
expect{ interface.executep('delete from not_a_table where thingy = ?', :delete, 14) }.
|
462
|
+
to raise_exception Pod4Error
|
463
|
+
|
464
|
+
end
|
465
|
+
|
466
|
+
it 'executes the string' do
|
467
|
+
expect{ interface.executep(sql, :delete, 2.0) }.not_to raise_exception
|
468
|
+
expect( interface.list.size ).to eq(data.size - 1)
|
469
|
+
expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
|
470
|
+
end
|
471
|
+
|
472
|
+
end
|
473
|
+
|
474
|
+
|
475
|
+
describe "#selectp" do
|
476
|
+
|
477
|
+
it 'requires an SQL string' do
|
478
|
+
expect{ interface.selectp }.to raise_exception ArgumentError
|
479
|
+
expect{ interface.selectp(nil) }.to raise_exception ArgumentError
|
480
|
+
expect{ interface.selectp(14) }.to raise_exception ArgumentError
|
481
|
+
end
|
482
|
+
|
483
|
+
it 'raises some sort of Pod4 error if it runs into problems' do
|
484
|
+
expect{ interface.selectp('select * from not_a_table where thingy = ?', 14) }.
|
485
|
+
to raise_exception Pod4Error
|
486
|
+
|
487
|
+
end
|
488
|
+
|
489
|
+
it 'returns the result of the sql' do
|
490
|
+
sql = 'select name from customer where qty < ?;'
|
491
|
+
|
492
|
+
expect{ interface.selectp(sql, 2.0) }.not_to raise_exception
|
493
|
+
expect( interface.selectp(sql, 2.0) ).to eq( [{name: 'Barney'}] )
|
494
|
+
expect( interface.selectp(sql, 0.0) ).to eq( [] )
|
495
|
+
end
|
496
|
+
|
497
|
+
it 'works if you pass a non-select' do
|
498
|
+
# By which I mean: still executes the SQL; returns []
|
499
|
+
sql = 'delete from customer where qty < ?;'
|
500
|
+
ret = interface.selectp(sql, 2.0)
|
501
|
+
|
502
|
+
expect( interface.list.size ).to eq(data.size - 1)
|
503
|
+
expect( interface.list.map{|r| r[:name] } ).not_to include 'Barney'
|
504
|
+
expect( ret ).to eq( [] )
|
505
|
+
end
|
506
|
+
|
507
|
+
end
|
508
|
+
|
509
|
+
|
510
|
+
end
|
511
|
+
|