From da41061b4ea1162fefe3a12b6215e4280ca42bf1 Mon Sep 17 00:00:00 2001 From: Sam Bostock Date: Thu, 19 Aug 2021 18:19:56 -0400 Subject: [PATCH 1/3] Add high_precision_current_timestamp to adapters CURRENT_TIMESTAMP provides differing precision on different databases. This method can be used in queries instead to provide a high precision current time regardless of the database being connected to. CURRENT_TIMESTAMP continues to be used as the default, unless overriden. --- .../abstract/database_statements.rb | 13 +++++++++++++ .../mysql/database_statements.rb | 9 +++++++++ .../postgresql/database_statements.rb | 8 ++++++++ .../sqlite3/database_statements.rb | 9 +++++++++ 4 files changed, 39 insertions(+) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 82e0d8e613..4cdd4b7721 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -441,6 +441,19 @@ module ActiveRecord end end + # This is a safe default, even if not high precision on all databases + HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc: + private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP + + # Returns an Arel SQL literal for the CURRENT_TIMESTAMP for usage with + # arbitrary precision date/time columns. + # + # Adapters supporting datetime with precision should override this to + # provide as much precision as is available. + def high_precision_current_timestamp + HIGH_PRECISION_CURRENT_TIMESTAMP + end + private def execute_batch(statements, name = nil) statements.each do |statement| diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 610c4efedb..8ea3f06e33 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -79,6 +79,15 @@ module ActiveRecord end alias :exec_update :exec_delete + # https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_current-timestamp + # https://dev.mysql.com/doc/refman/5.7/en/date-and-time-type-syntax.html + HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP(6)").freeze # :nodoc: + private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP + + def high_precision_current_timestamp + HIGH_PRECISION_CURRENT_TIMESTAMP + end + private def execute_batch(statements, name = nil) combine_multi_statements(statements).each do |statement| diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 33010b0b92..796e89378d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -122,6 +122,14 @@ module ActiveRecord execute("ROLLBACK", "TRANSACTION") end + # From https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT + HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP").freeze # :nodoc: + private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP + + def high_precision_current_timestamp + HIGH_PRECISION_CURRENT_TIMESTAMP + end + private def execute_batch(statements, name = nil) execute(combine_multi_statements(statements)) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb index b61ee405bd..070150ccc7 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb @@ -95,6 +95,15 @@ module ActiveRecord reset_read_uncommitted end + # https://stackoverflow.com/questions/17574784 + # https://www.sqlite.org/lang_datefunc.html + HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')").freeze # :nodoc: + private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP + + def high_precision_current_timestamp + HIGH_PRECISION_CURRENT_TIMESTAMP + end + private def reset_read_uncommitted read_uncommitted = Thread.current.thread_variable_get("read_uncommitted") From 58cfa9259d1e236231c2d8c6b338306d9dc3c06c Mon Sep 17 00:00:00 2001 From: Sam Bostock Date: Tue, 10 Aug 2021 17:05:40 -0400 Subject: [PATCH 2/3] Add precision: 6 to books.{created,updated}_at test table Some tests will require precision. --- activerecord/test/schema/schema.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 87bdf91ba4..3fbd8570d4 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -119,8 +119,13 @@ ActiveRecord::Schema.define do t.index :isbn, where: "published_on IS NOT NULL", unique: true t.index "(lower(external_id))", unique: true if supports_expression_index? - t.datetime :created_at - t.datetime :updated_at + if supports_datetime_with_precision? + t.datetime :created_at, precision: 6 + t.datetime :updated_at, precision: 6 + else + t.datetime :created_at + t.datetime :updated_at + end t.date :updated_on end From 213c4c5b50be5e3205eb78a4caed25bb154c2e51 Mon Sep 17 00:00:00 2001 From: Sam Bostock Date: Tue, 10 Aug 2021 17:31:36 -0400 Subject: [PATCH 3/3] Use full precision for `updated_at` in `insert_all`/`upsert_all` `CURRENT_TIMESTAMP` provides differing precision depending on the database, and not all databases support explicitly specifying additional precision. Instead, we delegate to the new `connection.high_precision_current_timestamp` for the SQL to produce a high precision timestamp on the current database. --- activerecord/CHANGELOG.md | 12 ++++++++++++ activerecord/lib/active_record/insert_all.rb | 2 +- activerecord/test/cases/insert_all_test.rb | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index fb942a152b..417b252125 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* Use full precision for `updated_at` in `insert_all`/`upsert_all` + + `CURRENT_TIMESTAMP` provides differing precision depending on the database, + and not all databases support explicitly specifying additional precision. + + Instead, we delegate to the new `connection.high_precision_current_timestamp` + for the SQL to produce a high precision timestamp on the current database. + + Fixes #42992 + + *Sam Bostock* + * Add config option for ignoring tables when dumping the schema cache. Applications can now be configured to ignore certain tables when dumping the schema cache. diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb index fefd8bd0a5..0e3de9c453 100644 --- a/activerecord/lib/active_record/insert_all.rb +++ b/activerecord/lib/active_record/insert_all.rb @@ -198,7 +198,7 @@ module ActiveRecord def touch_model_timestamps_unless(&block) model.timestamp_attributes_for_update_in_model.filter_map do |column_name| if touch_timestamp_attribute?(column_name) - "#{column_name}=(CASE WHEN (#{updatable_columns.map(&block).join(" AND ")}) THEN #{model.quoted_table_name}.#{column_name} ELSE CURRENT_TIMESTAMP END)," + "#{column_name}=(CASE WHEN (#{updatable_columns.map(&block).join(" AND ")}) THEN #{model.quoted_table_name}.#{column_name} ELSE #{connection.high_precision_current_timestamp} END)," end end.join end diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb index 9b4660efd6..56cca2e1a5 100644 --- a/activerecord/test/cases/insert_all_test.rb +++ b/activerecord/test/cases/insert_all_test.rb @@ -364,6 +364,15 @@ class InsertAllTest < ActiveRecord::TestCase assert_equal Time.now.year, Book.find(101).updated_on.year end + def test_upsert_all_respects_updated_at_precision_when_touched_implicitly + skip unless supports_insert_on_duplicate_update? && supports_datetime_with_precision? + + Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: 5.years.ago, updated_on: 5.years.ago }] + Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8) }] + + assert_not_predicate Book.find(101).updated_at.usec, :zero?, "updated_at should have sub-second precision" + end + def test_upsert_all_uses_given_updated_at_over_implicit_updated_at skip unless supports_insert_on_duplicate_update?