sequel_core 1.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.
- data/CHANGELOG +1003 -0
- data/COPYING +18 -0
- data/README +81 -0
- data/Rakefile +176 -0
- data/bin/sequel +41 -0
- data/lib/sequel_core.rb +59 -0
- data/lib/sequel_core/adapters/adapter_skeleton.rb +68 -0
- data/lib/sequel_core/adapters/ado.rb +100 -0
- data/lib/sequel_core/adapters/db2.rb +158 -0
- data/lib/sequel_core/adapters/dbi.rb +126 -0
- data/lib/sequel_core/adapters/informix.rb +87 -0
- data/lib/sequel_core/adapters/jdbc.rb +108 -0
- data/lib/sequel_core/adapters/mysql.rb +269 -0
- data/lib/sequel_core/adapters/odbc.rb +145 -0
- data/lib/sequel_core/adapters/odbc_mssql.rb +93 -0
- data/lib/sequel_core/adapters/openbase.rb +90 -0
- data/lib/sequel_core/adapters/oracle.rb +99 -0
- data/lib/sequel_core/adapters/postgres.rb +519 -0
- data/lib/sequel_core/adapters/sqlite.rb +192 -0
- data/lib/sequel_core/array_keys.rb +296 -0
- data/lib/sequel_core/connection_pool.rb +152 -0
- data/lib/sequel_core/core_ext.rb +59 -0
- data/lib/sequel_core/core_sql.rb +191 -0
- data/lib/sequel_core/database.rb +433 -0
- data/lib/sequel_core/dataset.rb +409 -0
- data/lib/sequel_core/dataset/convenience.rb +321 -0
- data/lib/sequel_core/dataset/sequelizer.rb +354 -0
- data/lib/sequel_core/dataset/sql.rb +586 -0
- data/lib/sequel_core/exceptions.rb +45 -0
- data/lib/sequel_core/migration.rb +191 -0
- data/lib/sequel_core/model.rb +8 -0
- data/lib/sequel_core/pretty_table.rb +73 -0
- data/lib/sequel_core/schema.rb +8 -0
- data/lib/sequel_core/schema/schema_generator.rb +131 -0
- data/lib/sequel_core/schema/schema_sql.rb +131 -0
- data/lib/sequel_core/worker.rb +58 -0
- data/spec/adapters/informix_spec.rb +139 -0
- data/spec/adapters/mysql_spec.rb +330 -0
- data/spec/adapters/oracle_spec.rb +130 -0
- data/spec/adapters/postgres_spec.rb +189 -0
- data/spec/adapters/sqlite_spec.rb +345 -0
- data/spec/array_keys_spec.rb +679 -0
- data/spec/connection_pool_spec.rb +356 -0
- data/spec/core_ext_spec.rb +67 -0
- data/spec/core_sql_spec.rb +301 -0
- data/spec/database_spec.rb +812 -0
- data/spec/dataset_spec.rb +2381 -0
- data/spec/migration_spec.rb +261 -0
- data/spec/pretty_table_spec.rb +66 -0
- data/spec/rcov.opts +4 -0
- data/spec/schema_generator_spec.rb +86 -0
- data/spec/schema_spec.rb +230 -0
- data/spec/sequelizer_spec.rb +448 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/worker_spec.rb +96 -0
- metadata +162 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module SQLite
|
5
|
+
class Database < Sequel::Database
|
6
|
+
set_adapter_scheme :sqlite
|
7
|
+
|
8
|
+
def serial_primary_key_options
|
9
|
+
{:primary_key => true, :type => :integer, :auto_increment => true}
|
10
|
+
end
|
11
|
+
|
12
|
+
def connect
|
13
|
+
if @opts[:database].nil? || @opts[:database].empty?
|
14
|
+
@opts[:database] = ':memory:'
|
15
|
+
end
|
16
|
+
db = ::SQLite3::Database.new(@opts[:database])
|
17
|
+
db.type_translation = true
|
18
|
+
# fix for timestamp translation
|
19
|
+
db.translator.add_translator("timestamp") do |t, v|
|
20
|
+
v =~ /^\d+$/ ? Time.at(v.to_i) : Time.parse(v)
|
21
|
+
end
|
22
|
+
db
|
23
|
+
end
|
24
|
+
|
25
|
+
def disconnect
|
26
|
+
@pool.disconnect {|c| c.close}
|
27
|
+
end
|
28
|
+
|
29
|
+
def dataset(opts = nil)
|
30
|
+
SQLite::Dataset.new(self, opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
TABLES_FILTER = "type = 'table' AND NOT name = 'sqlite_sequence'"
|
34
|
+
|
35
|
+
def tables
|
36
|
+
self[:sqlite_master].filter(TABLES_FILTER).map {|r| r[:name].to_sym}
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute(sql)
|
40
|
+
@logger.info(sql) if @logger
|
41
|
+
@pool.hold {|conn| conn.execute_batch(sql); conn.changes}
|
42
|
+
end
|
43
|
+
|
44
|
+
def execute_insert(sql)
|
45
|
+
@logger.info(sql) if @logger
|
46
|
+
@pool.hold {|conn| conn.execute(sql); conn.last_insert_row_id}
|
47
|
+
end
|
48
|
+
|
49
|
+
def single_value(sql)
|
50
|
+
@logger.info(sql) if @logger
|
51
|
+
@pool.hold {|conn| conn.get_first_value(sql)}
|
52
|
+
end
|
53
|
+
|
54
|
+
def execute_select(sql, &block)
|
55
|
+
@logger.info(sql) if @logger
|
56
|
+
@pool.hold {|conn| conn.query(sql, &block)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def pragma_get(name)
|
60
|
+
single_value("PRAGMA #{name}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def pragma_set(name, value)
|
64
|
+
execute("PRAGMA #{name} = #{value}")
|
65
|
+
end
|
66
|
+
|
67
|
+
AUTO_VACUUM = {'0' => :none, '1' => :full, '2' => :incremental}.freeze
|
68
|
+
|
69
|
+
def auto_vacuum
|
70
|
+
AUTO_VACUUM[pragma_get(:auto_vacuum)]
|
71
|
+
end
|
72
|
+
|
73
|
+
def auto_vacuum=(value)
|
74
|
+
value = AUTO_VACUUM.index(value) || (raise Error, "Invalid value for auto_vacuum option. Please specify one of :none, :full, :incremental.")
|
75
|
+
pragma_set(:auto_vacuum, value)
|
76
|
+
end
|
77
|
+
|
78
|
+
SYNCHRONOUS = {'0' => :off, '1' => :normal, '2' => :full}.freeze
|
79
|
+
|
80
|
+
def synchronous
|
81
|
+
SYNCHRONOUS[pragma_get(:synchronous)]
|
82
|
+
end
|
83
|
+
|
84
|
+
def synchronous=(value)
|
85
|
+
value = SYNCHRONOUS.index(value) || (raise Error, "Invalid value for synchronous option. Please specify one of :off, :normal, :full.")
|
86
|
+
pragma_set(:synchronous, value)
|
87
|
+
end
|
88
|
+
|
89
|
+
TEMP_STORE = {'0' => :default, '1' => :file, '2' => :memory}.freeze
|
90
|
+
|
91
|
+
def temp_store
|
92
|
+
TEMP_STORE[pragma_get(:temp_store)]
|
93
|
+
end
|
94
|
+
|
95
|
+
def temp_store=(value)
|
96
|
+
value = TEMP_STORE.index(value) || (raise Error, "Invalid value for temp_store option. Please specify one of :default, :file, :memory.")
|
97
|
+
pragma_set(:temp_store, value)
|
98
|
+
end
|
99
|
+
|
100
|
+
def alter_table_sql(table, op)
|
101
|
+
case op[:op]
|
102
|
+
when :add_column
|
103
|
+
"ALTER TABLE #{table} ADD #{column_definition_sql(op)}"
|
104
|
+
else
|
105
|
+
raise Error, "Unsupported ALTER TABLE operation"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def transaction(&block)
|
110
|
+
@pool.hold do |conn|
|
111
|
+
if conn.transaction_active?
|
112
|
+
return yield(conn)
|
113
|
+
end
|
114
|
+
begin
|
115
|
+
result = nil
|
116
|
+
conn.transaction {result = yield(conn)}
|
117
|
+
result
|
118
|
+
rescue => e
|
119
|
+
raise e unless Error::Rollback === e
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Dataset < Sequel::Dataset
|
126
|
+
def literal(v)
|
127
|
+
case v
|
128
|
+
when Time
|
129
|
+
literal(v.iso8601)
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def insert_sql(*values)
|
136
|
+
if (values.size == 1) && values.first.is_a?(Sequel::Dataset)
|
137
|
+
"INSERT INTO #{@opts[:from]} #{values.first.sql};"
|
138
|
+
else
|
139
|
+
super(*values)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def fetch_rows(sql, &block)
|
144
|
+
@db.execute_select(sql) do |result|
|
145
|
+
@columns = result.columns.map {|c| c.to_sym}
|
146
|
+
column_count = @columns.size
|
147
|
+
result.each do |values|
|
148
|
+
row = {}
|
149
|
+
column_count.times {|i| row[@columns[i]] = values[i]}
|
150
|
+
block.call(row)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def array_tuples_fetch_rows(sql, &block)
|
156
|
+
@db.execute_select(sql) do |result|
|
157
|
+
@columns = result.columns.map {|c| c.to_sym}
|
158
|
+
result.each {|r| r.keys = @columns; block[r]}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def insert(*values)
|
163
|
+
@db.execute_insert insert_sql(*values)
|
164
|
+
end
|
165
|
+
|
166
|
+
def update(*args, &block)
|
167
|
+
@db.execute update_sql(*args, &block)
|
168
|
+
end
|
169
|
+
|
170
|
+
def delete(opts = nil)
|
171
|
+
# check if no filter is specified
|
172
|
+
unless (opts && opts[:where]) || @opts[:where]
|
173
|
+
@db.transaction do
|
174
|
+
unfiltered_count = count
|
175
|
+
@db.execute delete_sql(opts)
|
176
|
+
unfiltered_count
|
177
|
+
end
|
178
|
+
else
|
179
|
+
@db.execute delete_sql(opts)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
EXPLAIN = 'EXPLAIN %s'.freeze
|
184
|
+
|
185
|
+
def explain
|
186
|
+
res = []
|
187
|
+
@db.result_set(EXPLAIN % select_sql(opts), nil) {|r| res << r}
|
188
|
+
res
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# ArrayKeys provide support for accessing array elements by keys. ArrayKeys are
|
2
|
+
# based on the arrayfields gem by Ara Howard, and can be used as substitutes
|
3
|
+
# for fetching records tuples as Ruby hashes.
|
4
|
+
#
|
5
|
+
# The main advantage offered by ArrayKeys over hashes is that the values are
|
6
|
+
# always ordered according to the column order in the query. Another purported
|
7
|
+
# advantage is that they reduce the memory footprint, but this has turned out
|
8
|
+
# to be a false claim.
|
9
|
+
module ArrayKeys
|
10
|
+
# The KeySet module contains methods that extend an array of keys to return
|
11
|
+
# a key's position in the key set.
|
12
|
+
module KeySet
|
13
|
+
# Returns the key's position in the key set. Provides indifferent access
|
14
|
+
# for symbols and strings.
|
15
|
+
def key_pos(key)
|
16
|
+
@key_indexes ||= inject({}) {|h, k| h[k.to_sym] = h.size; h}
|
17
|
+
@key_indexes[key] || @key_indexes[key.to_sym] || @key_indexes[key.to_s]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds a key to the key set.
|
21
|
+
def add_key(key)
|
22
|
+
self << key
|
23
|
+
@key_indexes[key] = @key_indexes.size
|
24
|
+
end
|
25
|
+
|
26
|
+
# Removes a key from the key set by its index.
|
27
|
+
def del_key(idx)
|
28
|
+
delete_at(idx)
|
29
|
+
@key_indexes = nil # reset key indexes
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The KeyAccess provides a large part of the Hash API for arrays with keys.
|
34
|
+
module KeyAccess
|
35
|
+
# Returns a value referenced by an array index or a key.
|
36
|
+
def [](idx, *args)
|
37
|
+
if String === idx or Symbol === idx
|
38
|
+
(idx = @keys.key_pos(idx)) ? super(idx, *args) : nil
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets the value referenced by an array index or a key.
|
45
|
+
def []=(idx,*args)
|
46
|
+
if String === idx or Symbol === idx
|
47
|
+
idx = @keys.key_pos(idx) || @keys.add_key(idx.to_sym)
|
48
|
+
end
|
49
|
+
super(idx, *args)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Stores a value by index or key.
|
53
|
+
def store(k, v); self[k] = v; end
|
54
|
+
|
55
|
+
# Slices the array, and returns an array with its keys sliced accordingly.
|
56
|
+
def slice(*args)
|
57
|
+
s = super(*args)
|
58
|
+
s.keys = @keys.slice(*args)
|
59
|
+
s
|
60
|
+
end
|
61
|
+
|
62
|
+
# Converts the array into a hash.
|
63
|
+
def to_hash
|
64
|
+
h = {}
|
65
|
+
each_with_index {|v, i| h[@keys[i].to_sym] = v}
|
66
|
+
h
|
67
|
+
end
|
68
|
+
alias_method :to_h, :to_hash
|
69
|
+
|
70
|
+
# Iterates over each key-value pair in the array.
|
71
|
+
def each_pair
|
72
|
+
each_with_index {|v, i| yield @keys[i], v}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Iterates over the array's associated keys.
|
76
|
+
def each_key(&block)
|
77
|
+
@keys.each(&block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Iterates over the array's values.
|
81
|
+
def each_value(&block)
|
82
|
+
each(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Deletes a value by its key.
|
86
|
+
def delete(key, *args)
|
87
|
+
if (idx = @keys.key_pos(key))
|
88
|
+
delete_at(idx)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Deletes a value by its index.
|
93
|
+
def delete_at(idx)
|
94
|
+
super(idx)
|
95
|
+
@keys = @keys.clone
|
96
|
+
@keys.del_key(idx)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns true if the array's key set contains the given key.
|
100
|
+
def include?(k)
|
101
|
+
@keys.include?(k) || @keys.include?(k.to_sym) || @keys.include?(k.to_s)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if the array's key set contains the given key.
|
105
|
+
def has_key?(k)
|
106
|
+
@keys.include?(k) || @keys.include?(k.to_sym) || @keys.include?(k.to_s)
|
107
|
+
end
|
108
|
+
alias_method :member?, :has_key?
|
109
|
+
alias_method :key?, :has_key?
|
110
|
+
|
111
|
+
# Returns true if the array contains the given value.
|
112
|
+
def has_value?(k); orig_include?(k); end
|
113
|
+
alias_method :value?, :has_value?
|
114
|
+
|
115
|
+
# Fetches a value by its key and optionally passes it through the given
|
116
|
+
# block:
|
117
|
+
#
|
118
|
+
# row.fetch(:name) {|v| v.to_sym}
|
119
|
+
#
|
120
|
+
# You can also give a default value
|
121
|
+
#
|
122
|
+
# row.fetch(:name, 'untitled')
|
123
|
+
#
|
124
|
+
def fetch(k, *args, &block)
|
125
|
+
if idx = @keys.key_pos(k)
|
126
|
+
v = at idx
|
127
|
+
else
|
128
|
+
!args.empty? ? (v = args.first) : (raise IndexError, "key not found")
|
129
|
+
end
|
130
|
+
block ? block[v] : v
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns self.
|
134
|
+
def values
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Creates a copy of self with the same key set.
|
139
|
+
def dup
|
140
|
+
copy = super
|
141
|
+
copy.keys = @keys
|
142
|
+
copy
|
143
|
+
end
|
144
|
+
|
145
|
+
# Creates a copy of self with a copy of the key set.
|
146
|
+
def clone
|
147
|
+
copy = super
|
148
|
+
copy.keys = @keys.clone
|
149
|
+
copy
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns an array merged from self and the given array.
|
153
|
+
def merge(values, &block)
|
154
|
+
clone.merge!(values, &block)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Merges the given array with self, optionally passing the values from self
|
158
|
+
# through the given block:
|
159
|
+
#
|
160
|
+
# row.merge!(new_values) {|k, old, new| (k == :name) ? old : new}
|
161
|
+
#
|
162
|
+
def merge!(values, &block)
|
163
|
+
values.each_pair do |k, v|
|
164
|
+
self[k] = (has_key?(k) && block) ? block[k, self[k], v] : v
|
165
|
+
end
|
166
|
+
self
|
167
|
+
end
|
168
|
+
alias_method :update, :merge!
|
169
|
+
alias_method :update!, :merge!
|
170
|
+
end
|
171
|
+
|
172
|
+
# The ArrayExtensions module provides extensions for the Array class.
|
173
|
+
module ArrayExtensions
|
174
|
+
attr_reader :keys
|
175
|
+
|
176
|
+
# Sets the key set for the array. Once a key set has been set for an array,
|
177
|
+
# it is extended with the KeyAccess API
|
178
|
+
def keys=(keys)
|
179
|
+
extend ArrayKeys::KeyAccess if keys
|
180
|
+
@keys = keys.frozen? ? keys.dup : keys
|
181
|
+
unless @keys.respond_to?(:key_pos)
|
182
|
+
@keys.extend(ArrayKeys::KeySet)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
alias_method :columns, :keys
|
187
|
+
alias_method :columns=, :keys=
|
188
|
+
end
|
189
|
+
|
190
|
+
# The DatasetExtensions module provides extensions that modify
|
191
|
+
# a dataset to return Array tuples instead of Hash tuples.
|
192
|
+
module DatasetExtensions
|
193
|
+
# Fetches a dataset's records, converting each tuple into an array with
|
194
|
+
# keys.
|
195
|
+
def array_tuples_each(opts = nil, &block)
|
196
|
+
fetch_rows(select_sql(opts)) {|h| block[Array.from_hash(h)]}
|
197
|
+
end
|
198
|
+
|
199
|
+
# Provides the corresponding behavior to Sequel::Dataset#update_each_method,
|
200
|
+
# using array tuples.
|
201
|
+
def array_tuples_update_each_method
|
202
|
+
# warning: ugly code generation ahead
|
203
|
+
if @row_proc && @transform
|
204
|
+
class << self
|
205
|
+
def each(opts = nil, &block)
|
206
|
+
if opts && opts[:naked]
|
207
|
+
fetch_rows(select_sql(opts)) {|r| block[transform_load(Array.from_hash(r))]}
|
208
|
+
else
|
209
|
+
fetch_rows(select_sql(opts)) {|r| block[@row_proc[transform_load(Array.from_hash(r))]]}
|
210
|
+
end
|
211
|
+
self
|
212
|
+
end
|
213
|
+
end
|
214
|
+
elsif @row_proc
|
215
|
+
class << self
|
216
|
+
def each(opts = nil, &block)
|
217
|
+
if opts && opts[:naked]
|
218
|
+
fetch_rows(select_sql(opts)) {|r| block[Array.from_hash(r)]}
|
219
|
+
else
|
220
|
+
fetch_rows(select_sql(opts)) {|r| block[@row_proc[Array.from_hash(r)]]}
|
221
|
+
end
|
222
|
+
self
|
223
|
+
end
|
224
|
+
end
|
225
|
+
elsif @transform
|
226
|
+
class << self
|
227
|
+
def each(opts = nil, &block)
|
228
|
+
fetch_rows(select_sql(opts)) {|r| block[transform_load(Array.from_hash(r))]}
|
229
|
+
self
|
230
|
+
end
|
231
|
+
end
|
232
|
+
else
|
233
|
+
class << self
|
234
|
+
def each(opts = nil, &block)
|
235
|
+
fetch_rows(select_sql(opts)) {|r| block[Array.from_hash(r)]}
|
236
|
+
self
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Array extensions.
|
245
|
+
class Array
|
246
|
+
alias_method :orig_include?, :include?
|
247
|
+
|
248
|
+
include ArrayKeys::ArrayExtensions
|
249
|
+
|
250
|
+
# Converts a hash into an array with keys.
|
251
|
+
def self.from_hash(h)
|
252
|
+
a = []; a.keys = []
|
253
|
+
a.merge!(h)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
module Sequel
|
258
|
+
# Modifies all dataset classes to fetch records as arrays with keys. By
|
259
|
+
# default records are fetched as hashes.
|
260
|
+
def self.use_array_tuples
|
261
|
+
Dataset.dataset_classes.each do |c|
|
262
|
+
c.class_eval do
|
263
|
+
if method_defined?(:array_tuples_fetch_rows)
|
264
|
+
alias_method :hash_tuples_fetch_rows, :fetch_rows
|
265
|
+
alias_method :fetch_rows, :array_tuples_fetch_rows
|
266
|
+
else
|
267
|
+
alias_method :orig_each, :each
|
268
|
+
alias_method :orig_update_each_method, :update_each_method
|
269
|
+
include ArrayKeys::DatasetExtensions
|
270
|
+
alias_method :each, :array_tuples_each
|
271
|
+
alias_method :update_each_method, :array_tuples_update_each_method
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Modifies all dataset classes to fetch records as hashes.
|
278
|
+
def self.use_hash_tuples
|
279
|
+
Dataset.dataset_classes.each do |c|
|
280
|
+
c.class_eval do
|
281
|
+
if method_defined?(:hash_tuples_fetch_rows)
|
282
|
+
alias_method :fetch_rows, :hash_tuples_fetch_rows
|
283
|
+
else
|
284
|
+
if method_defined?(:orig_each)
|
285
|
+
alias_method :each, :orig_each
|
286
|
+
undef_method :orig_each
|
287
|
+
end
|
288
|
+
if method_defined?(:orig_update_each_method)
|
289
|
+
alias_method :update_each_method, :orig_update_each_method
|
290
|
+
undef_method :orig_update_each_method
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|