activerecord-sqlserver-adapter 2.3.10 → 2.3.11
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -0
- data/Rakefile +2 -2
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +88 -15
- data/test/cases/connection_test_sqlserver.rb +64 -62
- data/test/cases/execute_procedure_test_sqlserver.rb +17 -9
- data/test/cases/sqlserver_helper.rb +3 -0
- data/test/connections/native_sqlserver/connection.rb +2 -1
- data/test/connections/native_sqlserver_odbc/connection.rb +2 -1
- metadata +4 -4
data/CHANGELOG
CHANGED
data/Rakefile
CHANGED
@@ -22,7 +22,7 @@ task :test => ['test:odbc']
|
|
22
22
|
|
23
23
|
namespace :test do
|
24
24
|
|
25
|
-
['odbc','adonet'].each do |mode|
|
25
|
+
['dblib','odbc','adonet'].each do |mode|
|
26
26
|
|
27
27
|
Rake::TestTask.new(mode) do |t|
|
28
28
|
t.libs = test_libs(mode)
|
@@ -44,7 +44,7 @@ end
|
|
44
44
|
|
45
45
|
namespace :profile do
|
46
46
|
|
47
|
-
['odbc','adonet'].each do |mode|
|
47
|
+
['dblib','odbc','adonet'].each do |mode|
|
48
48
|
namespace mode.to_sym do
|
49
49
|
|
50
50
|
Dir.glob("test/profile/*_profile_case.rb").sort.each do |test_file|
|
@@ -12,6 +12,9 @@ module ActiveRecord
|
|
12
12
|
config.reverse_merge! :mode => :odbc, :host => 'localhost', :username => 'sa', :password => ''
|
13
13
|
mode = config[:mode].to_s.downcase.underscore.to_sym
|
14
14
|
case mode
|
15
|
+
when :dblib
|
16
|
+
raise ArgumentError, 'Missing :dataserver configuration.' unless config.has_key?(:dataserver)
|
17
|
+
require_library_or_gem 'tiny_tds'
|
15
18
|
when :odbc
|
16
19
|
require_library_or_gem 'odbc' unless defined?(ODBC)
|
17
20
|
require 'active_record/connection_adapters/sqlserver_adapter/core_ext/odbc'
|
@@ -173,16 +176,18 @@ module ActiveRecord
|
|
173
176
|
class SQLServerAdapter < AbstractAdapter
|
174
177
|
|
175
178
|
ADAPTER_NAME = 'SQLServer'.freeze
|
176
|
-
VERSION = '2.3.
|
179
|
+
VERSION = '2.3.11'.freeze
|
177
180
|
DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
|
178
181
|
SUPPORTED_VERSIONS = [2000,2005,2008].freeze
|
179
182
|
LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].to_set.freeze
|
180
183
|
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
|
181
184
|
LOST_CONNECTION_EXCEPTIONS = {
|
185
|
+
:dblib => ['TinyTds::Error'],
|
182
186
|
:odbc => ['ODBC::Error'],
|
183
187
|
:adonet => ['TypeError','System::Data::SqlClient::SqlException']
|
184
188
|
}
|
185
189
|
LOST_CONNECTION_MESSAGES = {
|
190
|
+
:dblib => [/closed connection/],
|
186
191
|
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i],
|
187
192
|
:adonet => [/current state is closed/, /network-related/]
|
188
193
|
}
|
@@ -348,6 +353,15 @@ module ActiveRecord
|
|
348
353
|
# CONNECTION MANAGEMENT ====================================#
|
349
354
|
|
350
355
|
def active?
|
356
|
+
connected = case @connection_options[:mode]
|
357
|
+
when :dblib
|
358
|
+
!@connection.closed?
|
359
|
+
when :odbc
|
360
|
+
true
|
361
|
+
else :adonet
|
362
|
+
true
|
363
|
+
end
|
364
|
+
return false if !connected
|
351
365
|
raw_connection_do("SELECT 1")
|
352
366
|
true
|
353
367
|
rescue *lost_connection_exceptions
|
@@ -362,6 +376,8 @@ module ActiveRecord
|
|
362
376
|
|
363
377
|
def disconnect!
|
364
378
|
case @connection_options[:mode]
|
379
|
+
when :dblib
|
380
|
+
@connection.close rescue nil
|
365
381
|
when :odbc
|
366
382
|
@connection.disconnect rescue nil
|
367
383
|
else :adonet
|
@@ -414,8 +430,8 @@ module ActiveRecord
|
|
414
430
|
end
|
415
431
|
|
416
432
|
def execute(sql, name = nil, skip_logging = false)
|
417
|
-
if
|
418
|
-
with_identity_insert_enabled(
|
433
|
+
if id_insert_table_name = query_requires_identity_insert?(sql)
|
434
|
+
with_identity_insert_enabled(id_insert_table_name) { do_execute(sql,name) }
|
419
435
|
else
|
420
436
|
do_execute(sql,name)
|
421
437
|
end
|
@@ -424,19 +440,27 @@ module ActiveRecord
|
|
424
440
|
def execute_procedure(proc_name, *variables)
|
425
441
|
vars = variables.map{ |v| quote(v) }.join(', ')
|
426
442
|
sql = "EXEC #{proc_name} #{vars}".strip
|
443
|
+
name = 'Execute Procedure'
|
427
444
|
results = []
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
445
|
+
case @connection_options[:mode]
|
446
|
+
when :dblib
|
447
|
+
results << select(sql, name).map { |r| r.with_indifferent_access }
|
448
|
+
when :odbc
|
449
|
+
log(sql, name) do
|
450
|
+
raw_connection_run(sql) do |handle|
|
451
|
+
get_rows = lambda {
|
452
|
+
rows = handle_to_names_and_values handle, :fetch => :all
|
453
|
+
rows.each_with_index { |r,i| rows[i] = r.with_indifferent_access }
|
454
|
+
results << rows
|
455
|
+
}
|
437
456
|
get_rows.call
|
457
|
+
while handle_more_results?(handle)
|
458
|
+
get_rows.call
|
459
|
+
end
|
438
460
|
end
|
439
461
|
end
|
462
|
+
when :adonet
|
463
|
+
results << select(sql, name).map { |r| r.with_indifferent_access }
|
440
464
|
end
|
441
465
|
results.many? ? results : results.first
|
442
466
|
end
|
@@ -813,6 +837,23 @@ module ActiveRecord
|
|
813
837
|
def connect
|
814
838
|
config = @connection_options
|
815
839
|
@connection = case @connection_options[:mode]
|
840
|
+
when :dblib
|
841
|
+
appname = config[:appname] || Rails.application.class.name.split('::').first rescue nil
|
842
|
+
encoding = config[:encoding].present? ? config[:encoding] : nil
|
843
|
+
TinyTds::Client.new({
|
844
|
+
:dataserver => config[:dataserver],
|
845
|
+
:username => config[:username],
|
846
|
+
:password => config[:password],
|
847
|
+
:database => config[:database],
|
848
|
+
:appname => appname,
|
849
|
+
:login_timeout => config[:dblib_login_timeout],
|
850
|
+
:timeout => config[:dblib_timeout],
|
851
|
+
:encoding => encoding
|
852
|
+
}).tap do |client|
|
853
|
+
client.execute("SET ANSI_DEFAULTS ON").do
|
854
|
+
client.execute("SET IMPLICIT_TRANSACTIONS OFF").do
|
855
|
+
client.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do
|
856
|
+
end
|
816
857
|
when :odbc
|
817
858
|
if config[:dsn].include?(';')
|
818
859
|
driver = ODBC::Driver.new.tap do |d|
|
@@ -889,6 +930,8 @@ module ActiveRecord
|
|
889
930
|
def raw_connection_run(sql)
|
890
931
|
with_auto_reconnect do
|
891
932
|
case @connection_options[:mode]
|
933
|
+
when :dblib
|
934
|
+
@connection.execute(sql)
|
892
935
|
when :odbc
|
893
936
|
block_given? ? @connection.run_block(sql) { |handle| yield(handle) } : @connection.run(sql)
|
894
937
|
else :adonet
|
@@ -899,15 +942,21 @@ module ActiveRecord
|
|
899
942
|
|
900
943
|
def raw_connection_do(sql)
|
901
944
|
case @connection_options[:mode]
|
945
|
+
when :dblib
|
946
|
+
@insert_sql ? @connection.execute(sql).insert : @connection.execute(sql).do
|
902
947
|
when :odbc
|
903
948
|
@connection.do(sql)
|
904
949
|
else :adonet
|
905
950
|
@connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_non_query
|
906
951
|
end
|
952
|
+
ensure
|
953
|
+
@insert_sql = false
|
954
|
+
@update_sql = false
|
907
955
|
end
|
908
956
|
|
909
957
|
def finish_statement_handle(handle)
|
910
958
|
case @connection_options[:mode]
|
959
|
+
when :dblib
|
911
960
|
when :odbc
|
912
961
|
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
|
913
962
|
when :adonet
|
@@ -924,12 +973,24 @@ module ActiveRecord
|
|
924
973
|
end
|
925
974
|
|
926
975
|
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
927
|
-
|
976
|
+
@insert_sql = true
|
977
|
+
case @connection_options[:mode]
|
978
|
+
when :dblib
|
979
|
+
execute(sql, name) || id_value
|
980
|
+
else
|
981
|
+
super || select_value("SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
|
982
|
+
end
|
928
983
|
end
|
929
984
|
|
930
985
|
def update_sql(sql, name = nil)
|
931
|
-
|
932
|
-
|
986
|
+
@update_sql = true
|
987
|
+
case @connection_options[:mode]
|
988
|
+
when :dblib
|
989
|
+
execute(sql, name)
|
990
|
+
else
|
991
|
+
execute(sql, name)
|
992
|
+
select_value('SELECT @@ROWCOUNT AS AffectedRows')
|
993
|
+
end
|
933
994
|
end
|
934
995
|
|
935
996
|
def info_schema_query
|
@@ -955,6 +1016,7 @@ module ActiveRecord
|
|
955
1016
|
|
956
1017
|
def handle_more_results?(handle)
|
957
1018
|
case @connection_options[:mode]
|
1019
|
+
when :dblib
|
958
1020
|
when :odbc
|
959
1021
|
handle.more_results
|
960
1022
|
when :adonet
|
@@ -964,12 +1026,23 @@ module ActiveRecord
|
|
964
1026
|
|
965
1027
|
def handle_to_names_and_values(handle, options={})
|
966
1028
|
case @connection_options[:mode]
|
1029
|
+
when :dblib
|
1030
|
+
handle_to_names_and_values_dblib(handle, options)
|
967
1031
|
when :odbc
|
968
1032
|
handle_to_names_and_values_odbc(handle, options)
|
969
1033
|
when :adonet
|
970
1034
|
handle_to_names_and_values_adonet(handle, options)
|
971
1035
|
end
|
972
1036
|
end
|
1037
|
+
|
1038
|
+
def handle_to_names_and_values_dblib(handle, options={})
|
1039
|
+
query_options = {}.tap do |qo|
|
1040
|
+
qo[:timezone] = ActiveRecord::Base.default_timezone || :utc
|
1041
|
+
qo[:first] = true if options[:fetch] == :one
|
1042
|
+
qo[:as] = options[:fetch] == :rows ? :array : :hash
|
1043
|
+
end
|
1044
|
+
handle.each(query_options)
|
1045
|
+
end
|
973
1046
|
|
974
1047
|
def handle_to_names_and_values_odbc(handle, options={})
|
975
1048
|
@connection.use_utc = ActiveRecord::Base.default_timezone == :utc if @connection_supports_native_types
|
@@ -12,24 +12,6 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase
|
|
12
12
|
end
|
13
13
|
|
14
14
|
|
15
|
-
should 'return finished ODBC statment handle from #execute without block' do
|
16
|
-
assert_all_statements_used_are_closed do
|
17
|
-
@connection.execute('SELECT * FROM [topics]')
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
should 'finish ODBC 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 'finish connection from #raw_select' do
|
28
|
-
assert_all_statements_used_are_closed do
|
29
|
-
@connection.send(:raw_select,'SELECT * FROM [topics]')
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
15
|
should 'affect rows' do
|
34
16
|
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
|
35
17
|
updated = Topic.update(topic_data.keys, topic_data.values)
|
@@ -39,38 +21,6 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase
|
|
39
21
|
assert_equal 2, Topic.delete([1, 2])
|
40
22
|
end
|
41
23
|
|
42
|
-
should 'execute without block closes statement' do
|
43
|
-
assert_all_statements_used_are_closed do
|
44
|
-
@connection.execute("SELECT 1")
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
should 'execute with block closes statement' do
|
49
|
-
assert_all_statements_used_are_closed do
|
50
|
-
@connection.execute("SELECT 1") do |sth|
|
51
|
-
assert !sth.finished?, "Statement should still be alive within block"
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
should 'insert with identity closes statement' do
|
57
|
-
assert_all_statements_used_are_closed do
|
58
|
-
@connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
should 'insert without identity closes statement' do
|
63
|
-
assert_all_statements_used_are_closed do
|
64
|
-
@connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
should 'active closes statement' do
|
69
|
-
assert_all_statements_used_are_closed do
|
70
|
-
@connection.active?
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
24
|
should 'allow usage of :database connection option to remove setting from dsn' do
|
75
25
|
assert_equal 'activerecord_unittest', @connection.current_database
|
76
26
|
begin
|
@@ -82,6 +32,61 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase
|
|
82
32
|
end
|
83
33
|
end
|
84
34
|
|
35
|
+
context 'ODBC connection management' do
|
36
|
+
|
37
|
+
should 'return finished ODBC statement handle from #execute without block' do
|
38
|
+
assert_all_odbc_statements_used_are_closed do
|
39
|
+
@connection.execute('SELECT * FROM [topics]')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'finish ODBC statement handle from #execute with block' do
|
44
|
+
assert_all_odbc_statements_used_are_closed do
|
45
|
+
@connection.execute('SELECT * FROM [topics]') { }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'finish connection from #raw_select' do
|
50
|
+
assert_all_odbc_statements_used_are_closed do
|
51
|
+
@connection.send(:raw_select,'SELECT * FROM [topics]')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
should 'execute without block closes statement' do
|
56
|
+
assert_all_odbc_statements_used_are_closed do
|
57
|
+
@connection.execute("SELECT 1")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
should 'execute with block closes statement' do
|
62
|
+
assert_all_odbc_statements_used_are_closed do
|
63
|
+
@connection.execute("SELECT 1") do |sth|
|
64
|
+
assert !sth.finished?, "Statement should still be alive within block"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
should 'insert with identity closes statement' do
|
70
|
+
assert_all_odbc_statements_used_are_closed do
|
71
|
+
@connection.insert("INSERT INTO accounts ([id], [firm_id],[credit_limit]) values (999, 1, 50)")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
should 'insert without identity closes statement' do
|
76
|
+
assert_all_odbc_statements_used_are_closed do
|
77
|
+
@connection.insert("INSERT INTO accounts ([firm_id],[credit_limit]) values (1, 50)")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
should 'active closes statement' do
|
82
|
+
assert_all_odbc_statements_used_are_closed do
|
83
|
+
@connection.active?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end if connection_mode_odbc?
|
88
|
+
|
89
|
+
|
85
90
|
context 'Connection management' do
|
86
91
|
|
87
92
|
setup do
|
@@ -116,21 +121,18 @@ class ConnectionTestSqlserver < ActiveRecord::TestCase
|
|
116
121
|
|
117
122
|
private
|
118
123
|
|
119
|
-
def
|
124
|
+
def assert_all_odbc_statements_used_are_closed(&block)
|
125
|
+
odbc = @connection.raw_connection.class.parent
|
120
126
|
existing_handles = []
|
121
|
-
ObjectSpace.each_object(
|
122
|
-
|
127
|
+
ObjectSpace.each_object(odbc::Statement) { |h| existing_handles << h }
|
128
|
+
existing_handle_ids = existing_handles.map(&:object_id)
|
129
|
+
assert existing_handles.all?(&:finished?), "Somewhere before the block some statements were not closed"
|
130
|
+
GC.disable
|
123
131
|
yield
|
124
132
|
used_handles = []
|
125
|
-
ObjectSpace.each_object(
|
126
|
-
|
127
|
-
|
128
|
-
end
|
129
|
-
ObjectSpace.each_object(ODBC::Statement) do |handle|
|
130
|
-
assert_block "Statement should have been closed within given block" do
|
131
|
-
handle.finished?
|
132
|
-
end
|
133
|
-
end
|
133
|
+
ObjectSpace.each_object(odbc::Statement) { |h| used_handles << h unless existing_handle_ids.include?(h.object_id) }
|
134
|
+
assert used_handles.size > 0, "No statements were used within given block"
|
135
|
+
assert used_handles.all?(&:finished?), "Statement should have been closed within given block"
|
134
136
|
ensure
|
135
137
|
GC.enable
|
136
138
|
end
|
@@ -20,15 +20,23 @@ class ExecuteProcedureTestSqlserver < ActiveRecord::TestCase
|
|
20
20
|
assert_equal 'TABLE', table_info[:TABLE_TYPE], "Table Info: #{table_info.inspect}"
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
if connection_mode_odbc?
|
24
|
+
|
25
|
+
should 'allow multiple result sets to be returned' do
|
26
|
+
results1, results2 = @klass.execute_procedure('sp_helpconstraint','accounts')
|
27
|
+
assert_instance_of Array, results1
|
28
|
+
assert_instance_of HashWithIndifferentAccess, results1.first
|
29
|
+
assert results1.first['Object Name']
|
30
|
+
assert_instance_of Array, results2
|
31
|
+
assert_instance_of HashWithIndifferentAccess, results2.first
|
32
|
+
assert results2.first['constraint_name']
|
33
|
+
assert results2.first['constraint_type']
|
34
|
+
end
|
35
|
+
|
36
|
+
else
|
37
|
+
|
38
|
+
should 'allow multiple result sets to be returned'
|
39
|
+
|
32
40
|
end
|
33
41
|
|
34
42
|
|
@@ -104,6 +104,7 @@ end
|
|
104
104
|
module ActiveRecord
|
105
105
|
class TestCase < ActiveSupport::TestCase
|
106
106
|
class << self
|
107
|
+
def connection_mode_dblib? ; ActiveRecord::Base.connection.instance_variable_get(:@connection_options)[:mode] == :dblib ; end
|
107
108
|
def connection_mode_odbc? ; ActiveRecord::Base.connection.instance_variable_get(:@connection_options)[:mode] == :odbc ; end
|
108
109
|
def sqlserver_2000? ; ActiveRecord::Base.connection.sqlserver_2000? ; end
|
109
110
|
def sqlserver_2005? ; ActiveRecord::Base.connection.sqlserver_2005? ; end
|
@@ -122,6 +123,8 @@ module ActiveRecord
|
|
122
123
|
end
|
123
124
|
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found in:\n#{$queries_executed.inspect}"
|
124
125
|
end
|
126
|
+
def connection_mode_dblib? ; self.class.connection_mode_dblib? ; end
|
127
|
+
def connection_mode_odbc? ; self.class.connection_mode_odbc? ; end
|
125
128
|
def sqlserver_2000? ; self.class.sqlserver_2000? ; end
|
126
129
|
def sqlserver_2005? ; self.class.sqlserver_2005? ; end
|
127
130
|
def sqlserver_2008? ; self.class.sqlserver_2008? ; end
|
@@ -2,7 +2,8 @@ print "Using SQLServer via ADONET\n"
|
|
2
2
|
require_dependency 'models/course'
|
3
3
|
require 'logger'
|
4
4
|
|
5
|
-
ActiveRecord::Base.logger = Logger.new(
|
5
|
+
ActiveRecord::Base.logger = Logger.new(File.expand_path(File.join(SQLSERVER_TEST_ROOT,'debug.log')))
|
6
|
+
ActiveRecord::Base.logger.level = 0
|
6
7
|
|
7
8
|
ActiveRecord::Base.configurations = {
|
8
9
|
'arunit' => {
|
@@ -2,7 +2,8 @@ print "Using SQLServer via ODBC\n"
|
|
2
2
|
require_dependency 'models/course'
|
3
3
|
require 'logger'
|
4
4
|
|
5
|
-
ActiveRecord::Base.logger = Logger.new(
|
5
|
+
ActiveRecord::Base.logger = Logger.new(File.expand_path(File.join(SQLSERVER_TEST_ROOT,'debug.log')))
|
6
|
+
ActiveRecord::Base.logger.level = 0
|
6
7
|
|
7
8
|
ActiveRecord::Base.configurations = {
|
8
9
|
'arunit' => {
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-sqlserver-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 21
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 2
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
version: 2.3.
|
9
|
+
- 11
|
10
|
+
version: 2.3.11
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ken Collins
|
@@ -19,7 +19,7 @@ autorequire:
|
|
19
19
|
bindir: bin
|
20
20
|
cert_chain: []
|
21
21
|
|
22
|
-
date: 2010-
|
22
|
+
date: 2010-10-17 00:00:00 -04:00
|
23
23
|
default_executable:
|
24
24
|
dependencies:
|
25
25
|
- !ruby/object:Gem::Dependency
|