sequel 0.4.4.1 → 0.4.4.2
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/CHANGELOG +10 -0
- data/Rakefile +161 -159
- data/lib/sequel.rb +14 -10
- data/lib/sequel/adapters/adapter_skeleton.rb +2 -1
- data/lib/sequel/adapters/ado.rb +2 -1
- data/lib/sequel/adapters/db2.rb +5 -3
- data/lib/sequel/adapters/dbi.rb +2 -1
- data/lib/sequel/adapters/informix.rb +2 -1
- data/lib/sequel/adapters/jdbc.rb +3 -2
- data/lib/sequel/adapters/mysql.rb +268 -264
- data/lib/sequel/adapters/odbc.rb +7 -2
- data/lib/sequel/adapters/odbc_mssql.rb +1 -1
- data/lib/sequel/adapters/openbase.rb +2 -1
- data/lib/sequel/adapters/oracle.rb +2 -1
- data/lib/sequel/adapters/postgres.rb +32 -16
- data/lib/sequel/adapters/sqlite.rb +7 -6
- data/lib/sequel/array_keys.rb +295 -295
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core_sql.rb +14 -5
- data/lib/sequel/database.rb +4 -4
- data/lib/sequel/dataset.rb +12 -10
- data/lib/sequel/dataset/convenience.rb +10 -8
- data/lib/sequel/dataset/sequelizer.rb +19 -16
- data/lib/sequel/dataset/sql.rb +43 -30
- data/lib/sequel/exceptions.rb +45 -0
- data/lib/sequel/migration.rb +7 -5
- data/lib/sequel/model.rb +1 -1
- data/lib/sequel/model/base.rb +3 -3
- data/lib/sequel/model/hooks.rb +0 -4
- data/lib/sequel/model/record.rb +9 -9
- data/lib/sequel/model/relations.rb +2 -2
- data/lib/sequel/pretty_table.rb +6 -3
- data/lib/sequel/schema/schema_sql.rb +11 -6
- data/lib/sequel/worker.rb +8 -7
- data/spec/adapters/sqlite_spec.rb +3 -3
- data/spec/array_keys_spec.rb +543 -543
- data/spec/connection_pool_spec.rb +6 -3
- data/spec/database_spec.rb +4 -4
- data/spec/dataset_spec.rb +25 -25
- data/spec/migration_spec.rb +1 -1
- data/spec/model_spec.rb +16 -16
- data/spec/sequelizer_spec.rb +7 -7
- data/spec/spec.opts +8 -0
- metadata +5 -5
- data/lib/sequel/error.rb +0 -22
data/lib/sequel/core_sql.rb
CHANGED
@@ -47,7 +47,12 @@ class String
|
|
47
47
|
|
48
48
|
# Converts a string into a Time object.
|
49
49
|
def to_time
|
50
|
-
|
50
|
+
begin
|
51
|
+
Time.parse(self)
|
52
|
+
rescue Exception => e
|
53
|
+
raise Error::InvalidValue, "Invalid time value '#{self}' (#{e.message})"
|
54
|
+
end
|
55
|
+
# Why does Time.parse('0000-00-00') bork and not return nil or some such?
|
51
56
|
end
|
52
57
|
end
|
53
58
|
|
@@ -163,10 +168,14 @@ class Symbol
|
|
163
168
|
#
|
164
169
|
def to_column_ref(ds)
|
165
170
|
case s = to_s
|
166
|
-
when COLUMN_REF_RE1
|
167
|
-
|
168
|
-
when
|
169
|
-
|
171
|
+
when COLUMN_REF_RE1
|
172
|
+
"#{$1}.#{ds.quote_column_ref($2)} AS #{ds.quote_column_ref($3)}"
|
173
|
+
when COLUMN_REF_RE2
|
174
|
+
"#{ds.quote_column_ref($1)} AS #{ds.quote_column_ref($2)}"
|
175
|
+
when COLUMN_REF_RE3
|
176
|
+
"#{$1}.#{ds.quote_column_ref($2)}"
|
177
|
+
else
|
178
|
+
ds.quote_column_ref(s)
|
170
179
|
end
|
171
180
|
end
|
172
181
|
|
data/lib/sequel/database.rb
CHANGED
@@ -135,9 +135,9 @@ module Sequel
|
|
135
135
|
(String === args.first) ? fetch(*args) : from(*args)
|
136
136
|
end
|
137
137
|
|
138
|
-
# Raises a
|
138
|
+
# Raises a Sequel::Error::NotImplemented. This method is overriden in descendants.
|
139
139
|
def execute(sql)
|
140
|
-
raise NotImplementedError
|
140
|
+
raise NotImplementedError, "#execute should be overriden by adapters"
|
141
141
|
end
|
142
142
|
|
143
143
|
# Executes the supplied SQL statement. The SQL can be supplied as a string
|
@@ -272,7 +272,7 @@ module Sequel
|
|
272
272
|
result
|
273
273
|
rescue => e
|
274
274
|
conn.execute(SQL_ROLLBACK)
|
275
|
-
raise e unless
|
275
|
+
raise e unless Error::Rollback === e
|
276
276
|
ensure
|
277
277
|
@transactions.delete(Thread.current)
|
278
278
|
end
|
@@ -318,7 +318,7 @@ module Sequel
|
|
318
318
|
require File.join(File.dirname(__FILE__), "adapters/#{scheme}")
|
319
319
|
c = @@adapters[scheme.to_sym]
|
320
320
|
end
|
321
|
-
raise
|
321
|
+
raise Error::InvalidDatabaseScheme, "Invalid database scheme" unless c
|
322
322
|
c
|
323
323
|
end
|
324
324
|
|
data/lib/sequel/dataset.rb
CHANGED
@@ -226,28 +226,28 @@ module Sequel
|
|
226
226
|
def set_model(key, *args)
|
227
227
|
# pattern matching
|
228
228
|
case key
|
229
|
-
when nil
|
229
|
+
when nil # set_model(nil) => no
|
230
230
|
# no argument provided, so the dataset is denuded
|
231
231
|
@opts.merge!(:naked => true, :models => nil, :polymorphic_key => nil)
|
232
232
|
remove_row_proc
|
233
233
|
# extend_with_stock_each
|
234
|
-
when Class
|
234
|
+
when Class
|
235
235
|
# isomorphic model
|
236
236
|
@opts.merge!(:naked => nil, :models => {nil => key}, :polymorphic_key => nil)
|
237
237
|
set_row_proc {|h| key.new(h, *args)}
|
238
238
|
extend_with_destroy
|
239
|
-
when Symbol
|
239
|
+
when Symbol
|
240
240
|
# polymorphic model
|
241
|
-
hash = args.shift || raise(
|
241
|
+
hash = args.shift || raise(ArgumentError, "No class hash supplied for polymorphic model")
|
242
242
|
@opts.merge!(:naked => true, :models => hash, :polymorphic_key => key)
|
243
243
|
set_row_proc do |h|
|
244
244
|
c = hash[h[key]] || hash[nil] || \
|
245
|
-
raise(
|
245
|
+
raise(Error, "No matching model class for record (#{polymorphic_key} => #{h[polymorphic_key].inspect})")
|
246
246
|
c.new(h, *args)
|
247
247
|
end
|
248
248
|
extend_with_destroy
|
249
249
|
else
|
250
|
-
raise
|
250
|
+
raise ArgumentError, "Invalid model specified"
|
251
251
|
end
|
252
252
|
self
|
253
253
|
end
|
@@ -300,13 +300,13 @@ module Sequel
|
|
300
300
|
@transform = t
|
301
301
|
t.each do |k, v|
|
302
302
|
case v
|
303
|
-
when Array
|
303
|
+
when Array
|
304
304
|
if (v.size != 2) || !v.first.is_a?(Proc) && !v.last.is_a?(Proc)
|
305
|
-
raise
|
305
|
+
raise Error::InvalidTransform, "Invalid transform specified"
|
306
306
|
end
|
307
307
|
else
|
308
308
|
unless v = STOCK_TRANSFORMS[v]
|
309
|
-
raise
|
309
|
+
raise Error::InvalidTransform, "Invalid transform specified"
|
310
310
|
else
|
311
311
|
t[k] = v
|
312
312
|
end
|
@@ -384,7 +384,9 @@ module Sequel
|
|
384
384
|
def extend_with_destroy
|
385
385
|
unless respond_to?(:destroy)
|
386
386
|
meta_def(:destroy) do
|
387
|
-
|
387
|
+
unless @opts[:models]
|
388
|
+
raise Error, "No model associated with this dataset"
|
389
|
+
end
|
388
390
|
count = 0
|
389
391
|
@db.transaction {each {|r| count += 1; r.destroy}}
|
390
392
|
count
|
@@ -38,8 +38,10 @@ module Sequel
|
|
38
38
|
end
|
39
39
|
args = args.empty? ? 1 : (args.size == 1) ? args.first : args
|
40
40
|
case args
|
41
|
-
when 1
|
42
|
-
|
41
|
+
when 1
|
42
|
+
single_record(:limit => 1)
|
43
|
+
when Fixnum
|
44
|
+
limit(args).all
|
43
45
|
else
|
44
46
|
filter(args, &block).single_record(:limit => 1)
|
45
47
|
end
|
@@ -59,13 +61,13 @@ module Sequel
|
|
59
61
|
# record is returned. Otherwise an array is returned with the last
|
60
62
|
# <i>num</i> records.
|
61
63
|
def last(*args)
|
62
|
-
raise
|
64
|
+
raise Error, 'No order specified' unless
|
63
65
|
@opts[:order] || (opts && opts[:order])
|
64
66
|
|
65
67
|
args = args.empty? ? 1 : (args.size == 1) ? args.first : args
|
66
68
|
|
67
69
|
case args
|
68
|
-
when Fixnum
|
70
|
+
when Fixnum
|
69
71
|
l = {:limit => args}
|
70
72
|
opts = {:order => invert_order(@opts[:order])}. \
|
71
73
|
merge(opts ? opts.merge(l) : l)
|
@@ -238,10 +240,10 @@ module Sequel
|
|
238
240
|
end
|
239
241
|
|
240
242
|
module QueryBlockCopy #:nodoc:
|
241
|
-
def each(*args); raise
|
242
|
-
def insert(*args); raise
|
243
|
-
def update(*args); raise
|
244
|
-
def delete(*args); raise
|
243
|
+
def each(*args); raise Error, "#each cannot be invoked inside a query block."; end
|
244
|
+
def insert(*args); raise Error, "#insert cannot be invoked inside a query block."; end
|
245
|
+
def update(*args); raise Error, "#update cannot be invoked inside a query block."; end
|
246
|
+
def delete(*args); raise Error, "#delete cannot be invoked inside a query block."; end
|
245
247
|
|
246
248
|
def clone_merge(opts)
|
247
249
|
@opts.merge!(opts)
|
@@ -38,17 +38,17 @@ class Sequel::Dataset
|
|
38
38
|
# "(id = 3)"
|
39
39
|
def compare_expr(l, r)
|
40
40
|
case r
|
41
|
-
when Range
|
41
|
+
when Range
|
42
42
|
r.exclude_end? ? \
|
43
43
|
"(#{literal(l)} >= #{literal(r.begin)} AND #{literal(l)} < #{literal(r.end)})" : \
|
44
44
|
"(#{literal(l)} >= #{literal(r.begin)} AND #{literal(l)} <= #{literal(r.end)})"
|
45
|
-
when Array
|
45
|
+
when Array
|
46
46
|
"(#{literal(l)} IN (#{literal(r)}))"
|
47
|
-
when Sequel::Dataset
|
47
|
+
when Sequel::Dataset
|
48
48
|
"(#{literal(l)} IN (#{r.sql}))"
|
49
|
-
when NilClass
|
49
|
+
when NilClass
|
50
50
|
"(#{literal(l)} IS NULL)"
|
51
|
-
when Regexp
|
51
|
+
when Regexp
|
52
52
|
match_expr(l, r)
|
53
53
|
else
|
54
54
|
"(#{literal(l)} = #{literal(r)})"
|
@@ -60,10 +60,10 @@ class Sequel::Dataset
|
|
60
60
|
# can override this method to provide support for regular expressions.
|
61
61
|
def match_expr(l, r)
|
62
62
|
case r
|
63
|
-
when String
|
63
|
+
when String
|
64
64
|
"(#{literal(l)} LIKE #{literal(r)})"
|
65
65
|
else
|
66
|
-
raise
|
66
|
+
raise Sequel::Error, "Unsupported match pattern class (#{r.class})."
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
@@ -250,9 +250,9 @@ class Sequel::Dataset
|
|
250
250
|
vcall_expr(e, b, opts)
|
251
251
|
when :ivar, :cvar, :dvar, :const, :gvar # local ref
|
252
252
|
eval(e[1].to_s, b)
|
253
|
-
when :nth_ref
|
253
|
+
when :nth_ref
|
254
254
|
eval("$#{e[1]}", b)
|
255
|
-
when :lvar
|
255
|
+
when :lvar # local context
|
256
256
|
if e[1] == :block
|
257
257
|
pr = eval(e[1].to_s, b)
|
258
258
|
"#{proc_to_sql(pr)}"
|
@@ -267,9 +267,12 @@ class Sequel::Dataset
|
|
267
267
|
eval_expr(e[1], b, opts)...eval_expr(e[2], b, opts)
|
268
268
|
when :colon2 # qualified constant ref
|
269
269
|
eval_expr(e[1], b, opts).const_get(e[2])
|
270
|
-
when :false
|
271
|
-
|
272
|
-
when :
|
270
|
+
when :false
|
271
|
+
false
|
272
|
+
when :true
|
273
|
+
true
|
274
|
+
when :nil
|
275
|
+
nil
|
273
276
|
when :array
|
274
277
|
# array
|
275
278
|
e[1..-1].map {|i| eval_expr(i, b, opts)}
|
@@ -284,11 +287,11 @@ class Sequel::Dataset
|
|
284
287
|
# assignment
|
285
288
|
l = e[1]
|
286
289
|
r = eval_expr(e[2], b, opts)
|
287
|
-
raise
|
290
|
+
raise Sequel::Error::InvalidExpression, "#{l} = #{r}. Did you mean :#{l} == #{r}?"
|
288
291
|
when :if, :dstr
|
289
292
|
ext_expr(e, b, opts)
|
290
293
|
else
|
291
|
-
raise
|
294
|
+
raise Sequel::Error::InvalidExpression, "Invalid expression tree: #{e.inspect}"
|
292
295
|
end
|
293
296
|
end
|
294
297
|
|
@@ -338,7 +341,7 @@ begin
|
|
338
341
|
rescue Exception
|
339
342
|
module Sequel::Dataset::Sequelizer
|
340
343
|
def proc_to_sql(proc)
|
341
|
-
raise
|
344
|
+
raise Sequel::Error, "You must have the ParseTree gem installed in order to use block filters."
|
342
345
|
end
|
343
346
|
end
|
344
347
|
end
|
@@ -348,7 +351,7 @@ begin
|
|
348
351
|
rescue Exception
|
349
352
|
module Sequel::Dataset::Sequelizer
|
350
353
|
def ext_expr(e)
|
351
|
-
raise
|
354
|
+
raise Sequel::Error, "You must have the Ruby2Ruby gem installed in order to use this block filter."
|
352
355
|
end
|
353
356
|
end
|
354
357
|
end
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -45,15 +45,15 @@ module Sequel
|
|
45
45
|
# Converts an array of sources names into into a comma separated list.
|
46
46
|
def source_list(source)
|
47
47
|
if source.nil? || source.empty?
|
48
|
-
raise
|
48
|
+
raise Error, 'No source specified for query'
|
49
49
|
end
|
50
50
|
auto_alias_count = 0
|
51
51
|
m = source.map do |i|
|
52
52
|
case i
|
53
|
-
when Dataset
|
53
|
+
when Dataset
|
54
54
|
auto_alias_count += 1
|
55
55
|
i.to_table_reference(auto_alias_count)
|
56
|
-
when Hash
|
56
|
+
when Hash
|
57
57
|
i.map {|k, v| "#{k.is_a?(Dataset) ? k.to_table_reference : k} #{v}"}.
|
58
58
|
join(COMMA_SEPARATOR)
|
59
59
|
else
|
@@ -84,21 +84,34 @@ module Sequel
|
|
84
84
|
# If an unsupported object is given, an exception is raised.
|
85
85
|
def literal(v)
|
86
86
|
case v
|
87
|
-
when LiteralString
|
88
|
-
|
89
|
-
when
|
90
|
-
|
91
|
-
when
|
92
|
-
|
93
|
-
when
|
94
|
-
|
95
|
-
when
|
96
|
-
|
97
|
-
when
|
98
|
-
|
99
|
-
when
|
87
|
+
when LiteralString
|
88
|
+
v
|
89
|
+
when String
|
90
|
+
"'#{v.gsub(/'/, "''")}'"
|
91
|
+
when Integer, Float
|
92
|
+
v.to_s
|
93
|
+
when BigDecimal
|
94
|
+
v.to_s("F")
|
95
|
+
when NilClass
|
96
|
+
NULL
|
97
|
+
when TrueClass
|
98
|
+
TRUE
|
99
|
+
when FalseClass
|
100
|
+
FALSE
|
101
|
+
when Symbol
|
102
|
+
v.to_column_ref(self)
|
103
|
+
when Sequel::SQL::Expression
|
104
|
+
v.to_s(self)
|
105
|
+
when Array
|
106
|
+
v.empty? ? NULL : v.map {|i| literal(i)}.join(COMMA_SEPARATOR)
|
107
|
+
when Time
|
108
|
+
v.strftime(TIMESTAMP_FORMAT)
|
109
|
+
when Date
|
110
|
+
v.strftime(DATE_FORMAT)
|
111
|
+
when Dataset
|
112
|
+
"(#{v.sql})"
|
100
113
|
else
|
101
|
-
raise
|
114
|
+
raise Error, "can't express #{v.inspect} as a SQL literal"
|
102
115
|
end
|
103
116
|
end
|
104
117
|
|
@@ -109,12 +122,12 @@ module Sequel
|
|
109
122
|
# generated clause will be enclosed in a set of parentheses.
|
110
123
|
def expression_list(expr, parenthesize = false)
|
111
124
|
case expr
|
112
|
-
when Hash
|
125
|
+
when Hash
|
113
126
|
parenthesize = false if expr.size == 1
|
114
127
|
fmt = expr.map {|i| compare_expr(i[0], i[1])}.join(AND_SEPARATOR)
|
115
|
-
when Array
|
128
|
+
when Array
|
116
129
|
fmt = expr.shift.gsub(QUESTION_MARK) {literal(expr.shift)}
|
117
|
-
when Proc
|
130
|
+
when Proc
|
118
131
|
fmt = proc_to_sql(expr)
|
119
132
|
else
|
120
133
|
# if the expression is compound, it should be parenthesized in order for
|
@@ -204,7 +217,7 @@ module Sequel
|
|
204
217
|
clause = (@opts[:group] ? :having : :where)
|
205
218
|
cond = cond.first if cond.size == 1
|
206
219
|
if cond === true || cond === false
|
207
|
-
raise
|
220
|
+
raise Error::InvalidFilter, "Invalid filter specified. Did you mean to supply a block?"
|
208
221
|
end
|
209
222
|
parenthesize = !(cond.is_a?(Hash) || cond.is_a?(Array))
|
210
223
|
filter = cond.is_a?(Hash) && cond
|
@@ -228,7 +241,7 @@ module Sequel
|
|
228
241
|
r = expression_list(block || cond, parenthesize)
|
229
242
|
clone_merge(clause => "#{l} OR #{r}")
|
230
243
|
else
|
231
|
-
raise
|
244
|
+
raise Error::NoExistingFilter, "No existing filter found."
|
232
245
|
end
|
233
246
|
end
|
234
247
|
|
@@ -238,7 +251,7 @@ module Sequel
|
|
238
251
|
def and(*cond, &block)
|
239
252
|
clause = (@opts[:group] ? :having : :where)
|
240
253
|
unless @opts[clause]
|
241
|
-
raise
|
254
|
+
raise Error::NoExistingFilter, "No existing filter found."
|
242
255
|
end
|
243
256
|
filter(*cond, &block)
|
244
257
|
end
|
@@ -265,7 +278,7 @@ module Sequel
|
|
265
278
|
# if the dataset has been grouped. See also #filter.
|
266
279
|
def where(*cond, &block)
|
267
280
|
if @opts[:group]
|
268
|
-
raise
|
281
|
+
raise Error, "Can't specify a WHERE clause once the dataset has been grouped"
|
269
282
|
else
|
270
283
|
filter(*cond, &block)
|
271
284
|
end
|
@@ -275,7 +288,7 @@ module Sequel
|
|
275
288
|
# if the dataset has not been grouped. See also #filter
|
276
289
|
def having(*cond, &block)
|
277
290
|
unless @opts[:group]
|
278
|
-
raise
|
291
|
+
raise Error, "Can only specify a HAVING clause on a grouped dataset"
|
279
292
|
else
|
280
293
|
filter(*cond, &block)
|
281
294
|
end
|
@@ -310,7 +323,7 @@ module Sequel
|
|
310
323
|
def join_expr(type, table, expr)
|
311
324
|
join_type = JOIN_TYPES[type || :inner]
|
312
325
|
unless join_type
|
313
|
-
raise
|
326
|
+
raise Error::InvalidJoinType, "Invalid join type: #{type}"
|
314
327
|
end
|
315
328
|
|
316
329
|
join_conditions = {}
|
@@ -471,9 +484,9 @@ module Sequel
|
|
471
484
|
opts = opts ? @opts.merge(opts) : @opts
|
472
485
|
|
473
486
|
if opts[:group]
|
474
|
-
raise
|
487
|
+
raise Error::InvalidOperation, "A grouped dataset cannot be updated"
|
475
488
|
elsif (opts[:from].size > 1) or opts[:join]
|
476
|
-
raise
|
489
|
+
raise Error::InvalidOperation, "A joined dataset cannot be updated"
|
477
490
|
end
|
478
491
|
|
479
492
|
sql = "UPDATE #{@opts[:from]} SET "
|
@@ -507,9 +520,9 @@ module Sequel
|
|
507
520
|
opts = opts ? @opts.merge(opts) : @opts
|
508
521
|
|
509
522
|
if opts[:group]
|
510
|
-
raise
|
523
|
+
raise Error::InvalidOperation, "Grouped datasets cannot be deleted from"
|
511
524
|
elsif opts[:from].is_a?(Array) && opts[:from].size > 1
|
512
|
-
raise
|
525
|
+
raise Error::InvalidOperation, "Joined datasets cannot be deleted from"
|
513
526
|
end
|
514
527
|
|
515
528
|
sql = "DELETE FROM #{opts[:from]}"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Sequel
|
2
|
+
# Represents an error raised in Sequel code.
|
3
|
+
class Error < StandardError
|
4
|
+
|
5
|
+
# Rollback is a special error used to rollback a transactions.
|
6
|
+
# A transaction block will catch this error and wont pass further up the stack.
|
7
|
+
class Rollback < Error ; end
|
8
|
+
|
9
|
+
class InvalidDatabaseScheme < Error; end
|
10
|
+
|
11
|
+
# Represents an invalid value stored in the database.
|
12
|
+
class InvalidValue < Error ; end
|
13
|
+
|
14
|
+
# Represents an Invalid transform.
|
15
|
+
class InvalidTransform < Error ; end
|
16
|
+
|
17
|
+
# Represents an Invalid filter.
|
18
|
+
class InvalidFilter < Error ; end
|
19
|
+
|
20
|
+
class InvalidExpression < Error; end
|
21
|
+
|
22
|
+
# Represents an attempt to performing filter operations when no filter has been specified yet.
|
23
|
+
class NoExistingFilter < Error ; end
|
24
|
+
|
25
|
+
# Represents an invalid join type.
|
26
|
+
class InvalidJoinType < Error ; end
|
27
|
+
|
28
|
+
class WorkerStop < RuntimeError ; end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Object extensions
|
34
|
+
class Object
|
35
|
+
# Cancels the current transaction without an error:
|
36
|
+
#
|
37
|
+
# DB.tranaction do
|
38
|
+
# ...
|
39
|
+
# rollback! if failed_to_contact_client
|
40
|
+
# ...
|
41
|
+
# end
|
42
|
+
def rollback!
|
43
|
+
raise Sequel::Error::Rollback
|
44
|
+
end
|
45
|
+
end
|