aq1018-sqlserver-2000-2008-adpater 0.0.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 +55 -0
- data/MIT-LICENSE +20 -0
- data/README +0 -0
- data/RUNNING_UNIT_TESTS +60 -0
- data/Rakefile +44 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +984 -0
- data/lib/core_ext/active_record.rb +93 -0
- data/lib/core_ext/dbi.rb +87 -0
- data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
- data/test/cases/adapter_test_sqlserver.rb +560 -0
- data/test/cases/basics_test_sqlserver.rb +21 -0
- data/test/cases/calculations_test_sqlserver.rb +20 -0
- data/test/cases/column_test_sqlserver.rb +246 -0
- data/test/cases/connection_test_sqlserver.rb +103 -0
- data/test/cases/eager_association_test_sqlserver.rb +22 -0
- data/test/cases/inheritance_test_sqlserver.rb +28 -0
- data/test/cases/migration_test_sqlserver.rb +57 -0
- data/test/cases/offset_and_limit_test_sqlserver.rb +89 -0
- data/test/cases/pessimistic_locking_test_sqlserver.rb +100 -0
- data/test/cases/query_cache_test_sqlserver.rb +24 -0
- data/test/cases/schema_dumper_test_sqlserver.rb +61 -0
- data/test/cases/specific_schema_test_sqlserver.rb +25 -0
- data/test/cases/sqlserver_helper.rb +100 -0
- data/test/cases/table_name_test_sqlserver.rb +22 -0
- data/test/cases/unicode_test_sqlserver.rb +44 -0
- data/test/connections/native_sqlserver/connection.rb +23 -0
- data/test/connections/native_sqlserver_odbc/connection.rb +27 -0
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
- data/test/schema/sqlserver_specific_schema.rb +77 -0
- metadata +85 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/developer'
|
3
|
+
|
4
|
+
class BasicsTestSqlserver < ActiveRecord::TestCase
|
5
|
+
end
|
6
|
+
|
7
|
+
class BasicsTest < ActiveRecord::TestCase
|
8
|
+
|
9
|
+
COERCED_TESTS = [:test_read_attributes_before_type_cast_on_datetime]
|
10
|
+
|
11
|
+
include SqlserverCoercedTest
|
12
|
+
|
13
|
+
fixtures :developers
|
14
|
+
|
15
|
+
def test_coerced_test_read_attributes_before_type_cast_on_datetime
|
16
|
+
developer = Developer.find(:first)
|
17
|
+
assert_equal developer.created_at.to_s(:db)+'.000' , developer.attributes_before_type_cast["created_at"]
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/company'
|
3
|
+
|
4
|
+
class CalculationsTestSqlserver < ActiveRecord::TestCase
|
5
|
+
end
|
6
|
+
|
7
|
+
class CalculationsTest < ActiveRecord::TestCase
|
8
|
+
|
9
|
+
COERCED_TESTS = [:test_should_sum_expression]
|
10
|
+
|
11
|
+
include SqlserverCoercedTest
|
12
|
+
|
13
|
+
fixtures :accounts
|
14
|
+
|
15
|
+
def test_coerced_test_should_sum_expression
|
16
|
+
assert_equal 636, Account.sum("2 * credit_limit")
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/binary'
|
3
|
+
|
4
|
+
class ColumnTestSqlserver < ActiveRecord::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@connection = ActiveRecord::Base.connection
|
8
|
+
@column_klass = ActiveRecord::ConnectionAdapters::SQLServerColumn
|
9
|
+
end
|
10
|
+
|
11
|
+
should 'return real_number as float' do
|
12
|
+
assert_equal :float, TableWithRealColumn.columns_hash["real_number"].type
|
13
|
+
end
|
14
|
+
|
15
|
+
should 'know its #table_name and #table_klass' do
|
16
|
+
Topic.columns.each do |column|
|
17
|
+
assert_equal 'topics', column.table_name, "This column #{column.inspect} did not know it's #table_name"
|
18
|
+
assert_equal Topic, column.table_klass, "This column #{column.inspect} did not know it's #table_klass"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'return correct null, limit, and default for Topic' do
|
23
|
+
tch = Topic.columns_hash
|
24
|
+
assert_equal false, tch['id'].null
|
25
|
+
assert_equal true, tch['title'].null
|
26
|
+
assert_equal 255, tch['author_name'].limit
|
27
|
+
assert_equal true, tch['approved'].default
|
28
|
+
assert_equal 0, tch['replies_count'].default
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'For binary columns' do
|
32
|
+
|
33
|
+
setup do
|
34
|
+
@binary_string = "GIF89a\001\000\001\000\200\000\000\377\377\377\000\000\000!\371\004\000\000\000\000\000,\000\000\000\000\001\000\001\000\000\002\002D\001\000;"
|
35
|
+
@saved_bdata = Binary.create!(:data => @binary_string)
|
36
|
+
end
|
37
|
+
|
38
|
+
should 'read and write binary data equally' do
|
39
|
+
assert_equal @binary_string, Binary.find(@saved_bdata).data
|
40
|
+
end
|
41
|
+
|
42
|
+
should 'have correct attributes' do
|
43
|
+
column = Binary.columns_hash['data']
|
44
|
+
assert_equal :binary, column.type
|
45
|
+
assert_equal @connection.native_binary_database_type, column.sql_type
|
46
|
+
assert_equal nil, column.limit
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'quote data for sqlserver with literal 0x prefix' do
|
50
|
+
# See the output of the stored procedure: 'exec sp_datatype_info'
|
51
|
+
sqlserver_encoded_bdata = "0x47494638396101000100800000ffffff00000021f90400000000002c00000000010001000002024401003b"
|
52
|
+
assert_equal sqlserver_encoded_bdata, @column_klass.string_to_binary(@binary_string)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'For string columns' do
|
58
|
+
|
59
|
+
setup do
|
60
|
+
@char = SqlServerString.columns_hash['char']
|
61
|
+
@char10 = SqlServerString.columns_hash['char_10']
|
62
|
+
@varcharmax = SqlServerString.columns_hash['varchar_max']
|
63
|
+
@varcharmax10 = SqlServerString.columns_hash['varchar_max_10']
|
64
|
+
end
|
65
|
+
|
66
|
+
should 'have correct simplified types' do
|
67
|
+
assert_equal :string, @char.type
|
68
|
+
assert_equal :string, @char10.type
|
69
|
+
if sqlserver_2005?
|
70
|
+
assert_equal :string, @varcharmax.type
|
71
|
+
assert_equal :string, @varcharmax10.type
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
should 'have correct #sql_type per schema definition' do
|
76
|
+
assert_equal 'char(1)', @char.sql_type, 'Specifing a char type with no limit is 1 by SQL Server standards.'
|
77
|
+
assert_equal 'char(10)', @char10.sql_type, @char10.inspect
|
78
|
+
if sqlserver_2005?
|
79
|
+
assert_equal 'varchar(max)', @varcharmax.sql_type, 'A -1 limit should be converted to max (max) type.'
|
80
|
+
assert_equal 'varchar(max)', @varcharmax10.sql_type, 'A -1 limit should be converted to max (max) type.'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
should 'have correct #limit per schema definition' do
|
85
|
+
assert_equal 1, @char.limit
|
86
|
+
assert_equal 10, @char10.limit
|
87
|
+
if sqlserver_2005?
|
88
|
+
assert_equal nil, @varcharmax.limit, 'Limits on max types are moot and we should let rails know that.'
|
89
|
+
assert_equal nil, @varcharmax10.limit, 'Limits on max types are moot and we should let rails know that.'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
context 'For all national/unicode columns' do
|
97
|
+
|
98
|
+
setup do
|
99
|
+
@nchar = SqlServerUnicode.columns_hash['nchar']
|
100
|
+
@nvarchar = SqlServerUnicode.columns_hash['nvarchar']
|
101
|
+
@ntext = SqlServerUnicode.columns_hash['ntext']
|
102
|
+
@ntext10 = SqlServerUnicode.columns_hash['ntext_10']
|
103
|
+
@nchar10 = SqlServerUnicode.columns_hash['nchar_10']
|
104
|
+
@nvarchar100 = SqlServerUnicode.columns_hash['nvarchar_100']
|
105
|
+
@nvarcharmax = SqlServerUnicode.columns_hash['nvarchar_max']
|
106
|
+
@nvarcharmax10 = SqlServerUnicode.columns_hash['nvarchar_max_10']
|
107
|
+
end
|
108
|
+
|
109
|
+
should 'all respond true to #is_utf8?' do
|
110
|
+
SqlServerUnicode.columns_hash.except('id').values.each do |column|
|
111
|
+
assert column.is_utf8?, "This column #{column.inspect} should have been a unicode column."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
should 'have correct simplified types' do
|
116
|
+
assert_equal :string, @nchar.type
|
117
|
+
assert_equal :string, @nvarchar.type
|
118
|
+
assert_equal :text, @ntext.type
|
119
|
+
assert_equal :text, @ntext10.type
|
120
|
+
assert_equal :string, @nchar10.type
|
121
|
+
assert_equal :string, @nvarchar100.type
|
122
|
+
if sqlserver_2005?
|
123
|
+
assert_equal :string, @nvarcharmax.type
|
124
|
+
assert_equal :string, @nvarcharmax10.type
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
should 'have correct #sql_type per schema definition' do
|
129
|
+
assert_equal 'nchar(1)', @nchar.sql_type, 'Specifing a nchar type with no limit is 1 by SQL Server standards.'
|
130
|
+
assert_equal 'nvarchar(255)', @nvarchar.sql_type, 'Default nvarchar limit is 255.'
|
131
|
+
assert_equal 'ntext', @ntext.sql_type, 'Nice and clean ntext, limit means nothing here.'
|
132
|
+
assert_equal 'ntext', @ntext10.sql_type, 'Even a next with a limit of 10 specified will mean nothing.'
|
133
|
+
assert_equal 'nchar(10)', @nchar10.sql_type, 'An nchar with a limit of 10 needs to have it show up here.'
|
134
|
+
assert_equal 'nvarchar(100)', @nvarchar100.sql_type, 'An nvarchar with a specified limit of 100 needs to show it.'
|
135
|
+
if sqlserver_2005?
|
136
|
+
assert_equal 'nvarchar(max)', @nvarcharmax.sql_type, 'A -1 limit should be converted to max (max) type.'
|
137
|
+
assert_equal 'nvarchar(max)', @nvarcharmax10.sql_type, 'A -1 limit should be converted to max (max) type.'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
should 'have correct #limit per schema definition' do
|
142
|
+
assert_equal 1, @nchar.limit
|
143
|
+
assert_equal 255, @nvarchar.limit
|
144
|
+
assert_equal nil, @ntext.limit, 'An ntext column limit is moot, it is a fixed variable length'
|
145
|
+
assert_equal 10, @nchar10.limit
|
146
|
+
assert_equal 100, @nvarchar100.limit
|
147
|
+
if sqlserver_2005?
|
148
|
+
assert_equal nil, @nvarcharmax.limit, 'Limits on max types are moot and we should let rails know that.'
|
149
|
+
assert_equal nil, @nvarcharmax10.limit, 'Limits on max types are moot and we should let rails know that.'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'For datetime columns' do
|
156
|
+
|
157
|
+
setup do
|
158
|
+
@date = SqlServerChronic.columns_hash['date']
|
159
|
+
@time = SqlServerChronic.columns_hash['time']
|
160
|
+
@datetime = SqlServerChronic.columns_hash['datetime']
|
161
|
+
end
|
162
|
+
|
163
|
+
should 'have correct simplified type for uncast datetime' do
|
164
|
+
assert_equal :datetime, @datetime.type
|
165
|
+
end
|
166
|
+
|
167
|
+
should 'all be a datetime #sql_type' do
|
168
|
+
assert_equal 'datetime', @date.sql_type
|
169
|
+
assert_equal 'datetime', @time.sql_type
|
170
|
+
assert_equal 'datetime', @datetime.sql_type
|
171
|
+
end
|
172
|
+
|
173
|
+
should 'all be have nil #limit' do
|
174
|
+
assert_equal nil, @date.limit
|
175
|
+
assert_equal nil, @time.limit
|
176
|
+
assert_equal nil, @datetime.limit
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'which have coerced types' do
|
180
|
+
|
181
|
+
setup do
|
182
|
+
christmas_08 = "2008-12-25".to_time
|
183
|
+
christmas_08_afternoon = "2008-12-25 12:00".to_time
|
184
|
+
@chronic_date = SqlServerChronic.create!(:date => christmas_08).reload
|
185
|
+
@chronic_time = SqlServerChronic.create!(:time => christmas_08_afternoon).reload
|
186
|
+
end
|
187
|
+
|
188
|
+
should 'have an inheritable attribute ' do
|
189
|
+
assert SqlServerChronic.coerced_sqlserver_date_columns.include?('date')
|
190
|
+
end
|
191
|
+
|
192
|
+
should 'have column and objects cast to date' do
|
193
|
+
assert_equal :date, @date.type, "This column: \n#{@date.inspect}"
|
194
|
+
assert_instance_of Date, @chronic_date.date
|
195
|
+
end
|
196
|
+
|
197
|
+
should 'have column objects cast to time' do
|
198
|
+
assert_equal :time, @time.type, "This column: \n#{@time.inspect}"
|
199
|
+
assert_instance_of Time, @chronic_time.time
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
context 'For decimal and numeric columns' do
|
207
|
+
|
208
|
+
setup do
|
209
|
+
@bank_balance = NumericData.columns_hash['bank_balance']
|
210
|
+
@big_bank_balance = NumericData.columns_hash['big_bank_balance']
|
211
|
+
@world_population = NumericData.columns_hash['world_population']
|
212
|
+
@my_house_population = NumericData.columns_hash['my_house_population']
|
213
|
+
end
|
214
|
+
|
215
|
+
should 'have correct simplified types' do
|
216
|
+
assert_equal :decimal, @bank_balance.type
|
217
|
+
assert_equal :decimal, @big_bank_balance.type
|
218
|
+
assert_equal :integer, @world_population.type, 'Since #extract_scale == 0'
|
219
|
+
assert_equal :integer, @my_house_population.type, 'Since #extract_scale == 0'
|
220
|
+
end
|
221
|
+
|
222
|
+
should 'have correct #sql_type' do
|
223
|
+
assert_equal 'decimal(10,2)', @bank_balance.sql_type
|
224
|
+
assert_equal 'decimal(15,2)', @big_bank_balance.sql_type
|
225
|
+
assert_equal 'decimal(10,0)', @world_population.sql_type
|
226
|
+
assert_equal 'decimal(2,0)', @my_house_population.sql_type
|
227
|
+
end
|
228
|
+
|
229
|
+
should 'have correct #limit' do
|
230
|
+
assert_equal nil, @bank_balance.limit
|
231
|
+
assert_equal nil, @big_bank_balance.limit
|
232
|
+
assert_equal nil, @world_population.limit
|
233
|
+
assert_equal nil, @my_house_population.limit
|
234
|
+
end
|
235
|
+
|
236
|
+
should 'return correct precisions and scales' do
|
237
|
+
assert_equal [10,2], [@bank_balance.precision, @bank_balance.scale]
|
238
|
+
assert_equal [15,2], [@big_bank_balance.precision, @big_bank_balance.scale]
|
239
|
+
assert_equal [10,0], [@world_population.precision, @world_population.scale]
|
240
|
+
assert_equal [2,0], [@my_house_population.precision, @my_house_population.scale]
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/reply'
|
3
|
+
|
4
|
+
class ConnectionTestSqlserver < ActiveRecord::TestCase
|
5
|
+
|
6
|
+
self.use_transactional_fixtures = false
|
7
|
+
|
8
|
+
fixtures :topics, :accounts
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@connection = ActiveRecord::Base.connection
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
should 'return finished DBI statment handle from #execute without block' do
|
16
|
+
handle = @connection.execute('SELECT * FROM [topics]')
|
17
|
+
assert_instance_of DBI::StatementHandle, handle
|
18
|
+
assert handle.finished?
|
19
|
+
end
|
20
|
+
|
21
|
+
should 'finish DBI statment handle from #execute with block' do
|
22
|
+
assert_all_statements_used_are_closed do
|
23
|
+
@connection.execute('SELECT * FROM [topics]') { }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
should 'return an unfinished DBI statement handler from #raw_execute' do
|
28
|
+
handle = @connection.send(:raw_execute,'SELECT * FROM [topics]')
|
29
|
+
assert_instance_of DBI::StatementHandle, handle
|
30
|
+
assert !handle.finished?
|
31
|
+
end
|
32
|
+
|
33
|
+
should 'finish connection from #raw_select' do
|
34
|
+
assert_all_statements_used_are_closed do
|
35
|
+
@connection.send(:raw_select,'SELECT * FROM [topics]')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
should 'affect rows' do
|
40
|
+
assert Topic.connection.instance_variable_get("@connection")["AutoCommit"]
|
41
|
+
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
|
42
|
+
updated = Topic.update(topic_data.keys, topic_data.values)
|
43
|
+
assert_equal 2, updated.size
|
44
|
+
assert_equal "1 updated", Topic.find(1).content
|
45
|
+
assert_equal "2 updated", Topic.find(2).content
|
46
|
+
assert_equal 2, Topic.delete([1, 2])
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'execute without block closes statement' do
|
50
|
+
assert_all_statements_used_are_closed do
|
51
|
+
@connection.execute("SELECT 1")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
should 'execute with block closes statement' do
|
56
|
+
assert_all_statements_used_are_closed do
|
57
|
+
@connection.execute("SELECT 1") do |sth|
|
58
|
+
assert !sth.finished?, "Statement should still be alive within block"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
should 'insert with identity closes statement' do
|
64
|
+
assert_all_statements_used_are_closed do
|
65
|
+
@connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
should 'insert without identity closes statement' do
|
70
|
+
assert_all_statements_used_are_closed do
|
71
|
+
@connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
should 'active closes statement' do
|
76
|
+
assert_all_statements_used_are_closed do
|
77
|
+
@connection.active?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def assert_all_statements_used_are_closed(&block)
|
85
|
+
existing_handles = []
|
86
|
+
ObjectSpace.each_object(DBI::StatementHandle) {|handle| existing_handles << handle}
|
87
|
+
GC.disable
|
88
|
+
yield
|
89
|
+
used_handles = []
|
90
|
+
ObjectSpace.each_object(DBI::StatementHandle) {|handle| used_handles << handle unless existing_handles.include? handle}
|
91
|
+
assert_block "No statements were used within given block" do
|
92
|
+
used_handles.size > 0
|
93
|
+
end
|
94
|
+
ObjectSpace.each_object(DBI::StatementHandle) do |handle|
|
95
|
+
assert_block "Statement should have been closed within given block" do
|
96
|
+
handle.finished?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
ensure
|
100
|
+
GC.enable
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/author'
|
3
|
+
require 'models/post'
|
4
|
+
require 'models/comment'
|
5
|
+
|
6
|
+
class EagerAssociationTestSqlserver < ActiveRecord::TestCase
|
7
|
+
end
|
8
|
+
|
9
|
+
class EagerAssociationTest < ActiveRecord::TestCase
|
10
|
+
|
11
|
+
COERCED_TESTS = [:test_count_with_include]
|
12
|
+
|
13
|
+
include SqlserverCoercedTest
|
14
|
+
|
15
|
+
fixtures :authors, :posts, :comments
|
16
|
+
|
17
|
+
def test_coerced_test_count_with_include
|
18
|
+
assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'cases/sqlserver_helper'
|
2
|
+
require 'models/company'
|
3
|
+
|
4
|
+
class InheritanceTestSqlserver < ActiveRecord::TestCase
|
5
|
+
end
|
6
|
+
|
7
|
+
class InheritanceTest < ActiveRecord::TestCase
|
8
|
+
|
9
|
+
COERCED_TESTS = [
|
10
|
+
:test_eager_load_belongs_to_primary_key_quoting,
|
11
|
+
:test_a_bad_type_column
|
12
|
+
]
|
13
|
+
|
14
|
+
include SqlserverCoercedTest
|
15
|
+
|
16
|
+
def test_coerced_test_eager_load_belongs_to_primary_key_quoting
|
17
|
+
assert_sql(/\(\[companies\].\[id\] = 1\)/) do
|
18
|
+
Account.find(1, :include => :firm)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_coerced_test_a_bad_type_column
|
23
|
+
Company.connection.insert "INSERT INTO [companies] ([id], #{QUOTED_TYPE}, [name]) VALUES(100, 'bad_class!', 'Not happening')"
|
24
|
+
assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|