Skip to content

Commit 096e1b7

Browse files
committed
Refactor _type_cast & _quote.
I made a mistake and made `_type_cast` do quoting too. This is now fixed and we have greatly simplified it and `_quote` and use these two to cast and quote as needed in our sp_executesql helpers since we need to format both cast and quoted SQL strings. Changed `sp_executesql_sql_type` to have a default `String` case to `nvarchar(max)` not ideal and there are places where we prefer the bind contain the proper SQLServer type. For example, uniquness validations yielding a `ActiveRecord::Relation::QueryAttribute` with a [basic cast type](https://github.com/rails/rails/blob/5-0-stable/activerecord/lib/active_record/validations/uniqueness.rb#L80) vs the in scope `cast_type` further up. Added `sp_executesql_sql_param` which looks out for all `SQLServer::Data` and quotes it accordingly. For example, date & time objects need single quote '' vs. national N'' quoting. * New base `SQLServer::Data` shared for all types. Delegates quoting decisoin back to type. * Time types use variouse flavors of (Date|Time)#strptime in concert with SQL Server date/time formats in fast string processing.
1 parent 4692c57 commit 096e1b7

File tree

9 files changed

+78
-55
lines changed

9 files changed

+78
-55
lines changed

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def sp_executesql_types_and_parameters(binds)
240240
types, params = [], []
241241
binds.each_with_index do |attr, index|
242242
types << "@#{index} #{sp_executesql_sql_type(attr)}"
243-
params << type_cast(attr.value_for_database)
243+
params << sp_executesql_sql_param(attr)
244244
end
245245
[types, params]
246246
end
@@ -250,9 +250,20 @@ def sp_executesql_sql_type(attr)
250250
case value = attr.value_for_database
251251
when Numeric
252252
'int'.freeze
253+
when String
254+
'nvarchar(max)'.freeze
253255
else
254256
raise TypeError, "sp_executesql_sql_type can not find sql type for attr #{attr.inspect}"
255-
quote(value)
257+
end
258+
end
259+
260+
def sp_executesql_sql_param(attr)
261+
case attr.value_for_database
262+
when Type::Binary::Data,
263+
ActiveRecord::Type::SQLServer::Data
264+
quote(attr.value_for_database)
265+
else
266+
quote(type_cast(attr.value_for_database))
256267
end
257268
end
258269

lib/active_record/connection_adapters/sqlserver/quoting.rb

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,10 @@ def _quote(value)
8686

8787
def _type_cast(value)
8888
case value
89-
when nil
90-
"NULL"
91-
when Symbol
92-
_quote(value.to_s)
93-
when String, ActiveSupport::Multibyte::Chars
94-
_quote(value)
95-
when Type::Binary::Data
96-
_quote(value)
9789
when ActiveRecord::Type::SQLServer::Data
98-
_quote(value)
99-
else super
90+
value.to_s
91+
else
92+
super
10093
end
10194
end
10295

lib/active_record/connection_adapters/sqlserver/type/data.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def to_s
1919
end
2020
alias_method :to_str, :to_s
2121

22+
def inspect
23+
@value.inspect
24+
end
25+
2226
end
2327
end
2428
end

lib/active_record/connection_adapters/sqlserver/type/date.rb

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,32 @@ def sqlserver_type
1010

1111
def serialize(value)
1212
return unless value.present?
13-
return value if value.is_a?(Data)
14-
Data.new super, self
13+
date = value.to_s(:_sqlserver_dateformat)
14+
Data.new date, self
1515
end
1616

1717
def deserialize(value)
18-
return value.value if value.is_a?(Data)
19-
super
18+
value.is_a?(Data) ? super(value.value) : super
2019
end
2120

2221
def type_cast_for_schema(value)
2322
serialize(value).quoted
2423
end
2524

2625
def quoted(value)
27-
date = value.to_s(:_sqlserver_dateformat)
28-
Utils.quote_string_single(date)
26+
Utils.quote_string_single(value)
27+
end
28+
29+
private
30+
31+
def fast_string_to_date(string)
32+
::Date.strptime(string, fast_string_to_date_format)
33+
rescue ArgumentError
34+
super
35+
end
36+
37+
def fast_string_to_date_format
38+
::Date::DATE_FORMATS[:_sqlserver_dateformat]
2939
end
3040

3141
end

lib/active_record/connection_adapters/sqlserver/type/datetime.rb

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,23 @@ def sqlserver_type
1212

1313
def serialize(value)
1414
return super unless value.acts_like?(:time)
15-
Data.new super, self
15+
datetime = super.to_s(:_sqlserver_datetime).tap do |v|
16+
fraction = quote_fractional(value)
17+
v << ".#{fraction}" unless fraction.to_i.zero?
18+
end
19+
Data.new datetime, self
1620
end
1721

1822
def deserialize(value)
19-
datetime = value.is_a?(Data) ? value.value : super
20-
return unless datetime
21-
zone_conversion(datetime)
23+
value.is_a?(Data) ? super(value.value) : super
2224
end
2325

2426
def type_cast_for_schema(value)
2527
serialize(value).quoted
2628
end
2729

2830
def quoted(value)
29-
datetime = value.to_s(:_sqlserver_datetime)
30-
datetime = "#{datetime}".tap do |v|
31-
fraction = quote_fractional(value)
32-
v << ".#{fraction}" unless fraction.to_i.zero?
33-
end
34-
Utils.quote_string_single(datetime)
31+
Utils.quote_string_single(value)
3532
end
3633

3734
private
@@ -42,9 +39,18 @@ def cast_value(value)
4239
apply_seconds_precision(value)
4340
end
4441

45-
def zone_conversion(value)
46-
method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
47-
value.respond_to?(method) ? value.send(method) : value
42+
def fast_string_to_time(string)
43+
fast_string_to_time_zone.strptime(string, fast_string_to_time_format).time
44+
rescue ArgumentError
45+
super
46+
end
47+
48+
def fast_string_to_time_format
49+
"#{::Time::DATE_FORMATS[:_sqlserver_datetime]}.%N".freeze
50+
end
51+
52+
def fast_string_to_time_zone
53+
::Time.zone || ActiveSupport::TimeZone.new('UTC')
4854
end
4955

5056
end

lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,12 @@ def type
88
:datetimeoffset
99
end
1010

11-
def type_cast_for_schema(value)
12-
serialize(value).inspect
13-
end
14-
1511
def sqlserver_type
1612
"datetimeoffset(#{precision.to_i})"
1713
end
1814

1915
def quoted(value)
20-
datetime = value.to_s(:_sqlserver_datetimeoffset)
21-
Utils.quote_string_single(datetime)
22-
end
23-
24-
private
25-
26-
def fast_string_to_time(string)
27-
dateformat = ::Time::DATE_FORMATS[:_sqlserver_dateformat]
28-
::Time.strptime string, "#{dateformat} %H:%M:%S.%N %:z"
29-
end
30-
31-
def zone_conversion(value)
32-
value
16+
Utils.quote_string_single(value)
3317
end
3418

3519
end

lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def sqlserver_type
1414

1515
private
1616

17+
def fast_string_to_time_format
18+
::Time::DATE_FORMATS[:_sqlserver_datetime]
19+
end
20+
1721
def apply_seconds_precision(value)
1822
value.change usec: 0
1923
end

lib/active_record/connection_adapters/sqlserver/type/time.rb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,15 @@ class Time < ActiveRecord::Type::Time
88

99
def serialize(value)
1010
return super unless value.acts_like?(:time)
11-
Data.new super, self
11+
time = value.to_s(:_sqlserver_time).tap do |v|
12+
fraction = quote_fractional(value)
13+
v << ".#{fraction}" unless fraction.to_i.zero?
14+
end
15+
Data.new time, self
16+
end
17+
18+
def deserialize(value)
19+
value.is_a?(Data) ? super(value.value) : super
1220
end
1321

1422
def type_cast_for_schema(value)
@@ -20,11 +28,7 @@ def sqlserver_type
2028
end
2129

2230
def quoted(value)
23-
time = value.to_s(:_sqlserver_time).tap do |v|
24-
fraction = quote_fractional(value)
25-
v << ".#{fraction}" unless fraction.to_i.zero?
26-
end
27-
Utils.quote_string_single(time)
31+
Utils.quote_string_single(value)
2832
end
2933

3034
private

test/cases/column_test_sqlserver.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,14 @@ def assert_obj_set_and_save(attribute, value)
281281
type.limit.must_be_nil
282282
type.precision.must_be_nil
283283
type.scale.must_be_nil
284-
# Can cast strings.
284+
# Can cast strings. SQL Server format.
285+
obj.date = '04-01-0001'
286+
obj.date.must_equal Date.civil(0001, 4, 1)
287+
obj.save!
288+
obj.date.must_equal Date.civil(0001, 4, 1)
289+
obj.reload
290+
obj.date.must_equal Date.civil(0001, 4, 1)
291+
# Can cast strings. ISO format.
285292
obj.date = '0001-04-01'
286293
obj.date.must_equal Date.civil(0001, 4, 1)
287294
obj.save!

0 commit comments

Comments
 (0)