Skip to content

Commit f1a0fa9

Browse files
committed
Refactor microsecond precision to be database agnostic
The various databases don't actually need significantly different handling for this behavior, and they can achieve it without knowing about the type of the object. The old implementation was returning a string, which will cause problems such as breaking TZ aware attributes, and making it impossible for the adapters to supply their logic for time objects.
1 parent f926c1c commit f1a0fa9

File tree

11 files changed

+55
-84
lines changed

11 files changed

+55
-84
lines changed

activerecord/lib/active_record/connection_adapters/abstract/quoting.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,12 @@ def quoted_date(value)
127127
end
128128
end
129129

130-
value.to_s(:db)
130+
result = value.to_s(:db)
131+
if value.respond_to?(:usec) && value.usec > 0
132+
"#{result}.#{sprintf("%06d", value.usec)}"
133+
else
134+
result
135+
end
131136
end
132137

133138
def prepare_binds_for_database(binds) # :nodoc:

activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def prepare_column_options(column)
2929

3030
limit = column.limit || native_database_types[column.type][:limit]
3131
spec[:limit] = limit.inspect if limit
32-
spec[:precision] = column.precision.inspect if column.precision
32+
spec[:precision] = column.precision.inspect if column.precision && column.precision != 0
3333
spec[:scale] = column.scale.inspect if column.scale
3434

3535
default = schema_default(column) if column.has_default?

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -263,18 +263,6 @@ def index_algorithms
263263
{}
264264
end
265265

266-
# QUOTING ==================================================
267-
268-
# Quote date/time values for use in SQL input. Includes microseconds
269-
# if the value is a Time responding to usec.
270-
def quoted_date(value) #:nodoc:
271-
if value.acts_like?(:time) && value.respond_to?(:usec)
272-
"#{super}.#{sprintf("%06d", value.usec)}"
273-
else
274-
super
275-
end
276-
end
277-
278266
# Returns a bind substitution value given a bind +column+
279267
# NOTE: The column param is currently being used by the sqlserver-adapter
280268
def substitute_at(column, _unused = 0)
@@ -409,15 +397,15 @@ def column_name_for_operation(operation, node) # :nodoc:
409397
protected
410398

411399
def initialize_type_map(m) # :nodoc:
412-
register_class_with_limit m, %r(boolean)i, Type::Boolean
413-
register_class_with_limit m, %r(char)i, Type::String
414-
register_class_with_limit m, %r(binary)i, Type::Binary
415-
register_class_with_limit m, %r(text)i, Type::Text
416-
register_class_with_limit m, %r(date)i, Type::Date
417-
register_class_with_limit m, %r(time)i, Type::Time
418-
register_class_with_limit m, %r(datetime)i, Type::DateTime
419-
register_class_with_limit m, %r(float)i, Type::Float
420-
register_class_with_limit m, %r(int)i, Type::Integer
400+
register_class_with_limit m, %r(boolean)i, Type::Boolean
401+
register_class_with_limit m, %r(char)i, Type::String
402+
register_class_with_limit m, %r(binary)i, Type::Binary
403+
register_class_with_limit m, %r(text)i, Type::Text
404+
register_class_with_precision m, %r(date)i, Type::Date
405+
register_class_with_precision m, %r(time)i, Type::Time
406+
register_class_with_precision m, %r(datetime)i, Type::DateTime
407+
register_class_with_limit m, %r(float)i, Type::Float
408+
register_class_with_limit m, %r(int)i, Type::Integer
421409

422410
m.alias_type %r(blob)i, 'binary'
423411
m.alias_type %r(clob)i, 'text'
@@ -451,6 +439,13 @@ def register_class_with_limit(mapping, key, klass) # :nodoc:
451439
end
452440
end
453441

442+
def register_class_with_precision(mapping, key, klass) # :nodoc:
443+
mapping.register_type(key) do |*args|
444+
precision = extract_precision(args.last)
445+
klass.new(precision: precision)
446+
end
447+
end
448+
454449
def extract_scale(sql_type) # :nodoc:
455450
case sql_type
456451
when /\((\d+)\)/ then 0

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -712,11 +712,6 @@ def initialize_type_map(m) # :nodoc:
712712
m.alias_type %r(year)i, 'integer'
713713
m.alias_type %r(bit)i, 'binary'
714714

715-
m.register_type(%r(datetime)i) do |sql_type|
716-
precision = extract_precision(sql_type)
717-
MysqlDateTime.new(precision: precision)
718-
end
719-
720715
m.register_type(%r(enum)i) do |sql_type|
721716
limit = sql_type[/^enum\((.+)\)/i, 1]
722717
.split(',').map{|enum| enum.strip.length - 2}.max
@@ -734,6 +729,14 @@ def register_integer_type(mapping, key, options) # :nodoc:
734729
end
735730
end
736731

732+
def extract_precision(sql_type)
733+
if /datetime/ === sql_type
734+
super || 0
735+
else
736+
super
737+
end
738+
end
739+
737740
def fetch_type_metadata(sql_type, collation = "", extra = "")
738741
MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?)
739742
end
@@ -916,14 +919,6 @@ def create_table_definition(name, temporary = false, options = nil, as = nil) #
916919
TableDefinition.new(native_database_types, name, temporary, options, as)
917920
end
918921

919-
class MysqlDateTime < Type::DateTime # :nodoc:
920-
private
921-
922-
def has_precision?
923-
precision || 0
924-
end
925-
end
926-
927922
class MysqlString < Type::String # :nodoc:
928923
def type_cast_for_database(value)
929924
case value
@@ -945,7 +940,7 @@ def cast_value(value)
945940
end
946941

947942
def type_classes_with_standard_constructor
948-
super.merge(string: MysqlString, date_time: MysqlDateTime)
943+
super.merge(string: MysqlString)
949944
end
950945
end
951946
end

activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@ module OID # :nodoc:
55
class DateTime < Type::DateTime # :nodoc:
66
include Infinity
77

8-
def type_cast_for_database(value)
9-
if has_precision? && value.acts_like?(:time) && value.year <= 0
10-
bce_year = format("%04d", -value.year + 1)
11-
super.sub(/^-?\d+/, bce_year) + " BC"
12-
else
13-
super
14-
end
15-
end
16-
178
def cast_value(value)
189
if value.is_a?(::String)
1910
case value

activerecord/lib/active_record/type/date_time.rb

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,25 @@ def type
1111
end
1212

1313
def type_cast_for_database(value)
14-
return super unless value.acts_like?(:time)
15-
16-
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
17-
18-
if value.respond_to?(zone_conversion_method)
19-
value = value.send(zone_conversion_method)
14+
if precision && value.respond_to?(:usec)
15+
number_of_insignificant_digits = 6 - precision
16+
round_power = 10 ** number_of_insignificant_digits
17+
value = value.change(usec: value.usec / round_power * round_power)
2018
end
2119

22-
return value unless has_precision?
20+
if value.acts_like?(:time)
21+
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
2322

24-
result = value.to_s(:db)
25-
if value.respond_to?(:usec) && (1..6).cover?(precision)
26-
"#{result}.#{sprintf("%0#{precision}d", value.usec / 10 ** (6 - precision))}"
27-
else
28-
result
23+
if value.respond_to?(zone_conversion_method)
24+
value = value.send(zone_conversion_method)
25+
end
2926
end
27+
28+
value
3029
end
3130

3231
private
3332

34-
alias has_precision? precision
35-
3633
def cast_value(string)
3734
return string unless string.is_a?(::String)
3835
return if string.empty?

activerecord/test/cases/adapters/mysql2/connection_test.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,4 @@ def test_logs_name_rename_column_sql
122122
ensure
123123
@connection.execute "DROP TABLE `bar_baz`"
124124
end
125-
126-
if mysql_56?
127-
def test_quote_time_usec
128-
assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0))
129-
assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime)
130-
end
131-
end
132125
end

activerecord/test/cases/adapters/mysql2/datetime_test.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44
class DateTimeTest < ActiveRecord::TestCase
55
self.use_transactional_fixtures = false
66

7-
class Foo < ActiveRecord::Base; end
8-
9-
def test_default_datetime_precision
10-
ActiveRecord::Base.connection.create_table(:foos, force: true)
11-
ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime
12-
ActiveRecord::Base.connection.add_column :foos, :updated_at, :datetime
13-
assert_nil activerecord_column_option('foos', 'created_at', 'precision')
7+
teardown do
8+
ActiveRecord::Base.connection.drop_table(:foos, if_exists: true)
149
end
1510

11+
class Foo < ActiveRecord::Base; end
12+
1613
def test_datetime_data_type_with_precision
1714
ActiveRecord::Base.connection.create_table(:foos, force: true)
1815
ActiveRecord::Base.connection.add_column :foos, :created_at, :datetime, precision: 1

activerecord/test/cases/adapters/postgresql/quoting_test.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@ def test_quote_float_infinity
2727
assert_equal "'Infinity'", @conn.quote(infinity)
2828
end
2929

30-
def test_quote_time_usec
31-
assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
32-
assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
33-
end
34-
3530
def test_quote_range
3631
range = "1,2]'; SELECT * FROM users; --".."a"
3732
type = OID::Range.new(Type::Integer.new, :int8range)

activerecord/test/cases/adapters/postgresql/timestamp_test.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,13 @@ def test_formatting_timestamp_according_to_precision
145145
date = ::Time.utc(2014, 8, 17, 12, 30, 0, 999999)
146146
Foo.create!(created_at: date, updated_at: date)
147147
assert foo = Foo.find_by(created_at: date)
148+
assert_equal 1, Foo.where(updated_at: date).count
148149
assert_equal date.to_s, foo.created_at.to_s
149150
assert_equal date.to_s, foo.updated_at.to_s
150151
assert_equal 000000, foo.created_at.usec
151152
assert_equal 999900, foo.updated_at.usec
153+
ensure
154+
ActiveRecord::Base.connection.drop_table(:foos, if_exists: true)
152155
end
153156

154157
private

0 commit comments

Comments
 (0)