Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ end
# Required for samples
gem "docker-api"
gem "sinatra-activerecord"

# Force google-protobuf to compile from source to avoid ABI issues in CI
if ENV["CI"]
gem "google-protobuf", force_ruby_platform: true
end
2 changes: 1 addition & 1 deletion acceptance/cases/migration/change_schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def test_add_column_with_timestamp_type

column = connection.columns(:testings).find { |c| c.name == "foo" }

assert_equal :time, column.type
assert_equal :datetime, column.type
assert_equal "TIMESTAMP", column.sql_type
end

Expand Down
2 changes: 1 addition & 1 deletion acceptance/cases/migration/schema_dumper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_dump_schema_contains_commit_timestamp
connection = pool_or_connection
schema = StringIO.new
ActiveRecord::SchemaDumper.dump connection, schema
assert schema.string.include?("t.time \"last_updated\", allow_commit_timestamp: true"), schema.string
assert_match(/t\.(datetime|timestamp) "last_updated",.*allow_commit_timestamp: true/, schema.string, schema.string)
Comment thread
sakthivelmanii marked this conversation as resolved.
end

def test_dump_schema_contains_virtual_column
Expand Down
126 changes: 123 additions & 3 deletions acceptance/cases/type/time_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,29 @@ def test_set_and_save_time
end

def test_date_time_string_value_with_subsecond_precision
expected_time = ::Time.local 2017, 07, 4, 14, 19, 10, 897761
expected_time = ::Time.utc 2017, 7, 4, 14, 19, 10, 897761

string_value = "2017-07-04 14:19:10.897761"

record = TestTypeModel.new start_time: string_value
assert_equal expected_time, record.start_time.utc

record.save!
record.reload
assert_equal expected_time, record.start_time.utc

assert_equal record, TestTypeModel.find_by(start_time: string_value)
end

def test_date_time_with_string_value_with_non_iso_format
string_value = "04/07/2017 2:19pm"
expected_time = ::Time.local 2017, 07, 4, 14, 19
expected_time = ::Time.utc 2017, 7, 4, 14, 19

record = TestTypeModel.new start_time: string_value
assert_equal expected_time, record.start_time

record.save!
record.reload
assert_equal expected_time, record.start_time.utc

assert_equal record, TestTypeModel.find_by(start_time: string_value)
Expand All @@ -82,6 +84,124 @@ def test_default_year_is_correct

assert_equal expected_time, record.start_time
end

def test_multiparameter_datetime
expected_time = ::Time.utc 2020, 12, 25, 10, 30, 0
record = TestTypeModel.new start_time: { 1 => 2020, 2 => 12, 3 => 25, 4 => 10, 5 => 30 }

assert_equal expected_time, record.start_time

record.save!
record.reload

assert_equal expected_time, record.start_time
end

def test_date_time_string_value_with_timezone_aware_attributes
old_zone = ::Time.zone
::Time.zone = "UTC"
TestTypeModel.time_zone_aware_attributes = true
TestTypeModel.reset_column_information

string_value = "2017-07-04 14:19:10.897761"
expected_time = ::Time.zone.local 2017, 7, 4, 14, 19, 10, 897761

record = TestTypeModel.new start_time: string_value
assert_equal expected_time, record.start_time

record.save!
record.reload
assert_equal expected_time, record.start_time

assert_equal record, TestTypeModel.find_by(start_time: string_value)
ensure
TestTypeModel.time_zone_aware_attributes = false
TestTypeModel.reset_column_information
::Time.zone = old_zone
end

def test_string_value_with_paris_timezone
old_zone = ::Time.zone
::Time.zone = "Paris" # Paris is UTC+2 in July (DST)
TestTypeModel.time_zone_aware_attributes = true
TestTypeModel.reset_column_information

begin
string_value = "2017-07-04 14:19:10.897761123"
# 14:19:10 in Paris (DST) is 12:19:10 UTC
# Subseconds: 897761123 nanoseconds = 897761.123 microseconds
expected_time = ::Time.zone.local(2017, 7, 4, 14, 19, 10, Rational(897761123, 1000))

record = TestTypeModel.new start_time: string_value
assert_equal expected_time, record.start_time
assert_equal 897761123, record.start_time.nsec
assert_instance_of ActiveSupport::TimeWithZone, record.start_time
assert_equal "Paris", record.start_time.time_zone.name

record.save!

# Verify directly in the database using the raw Spanner client (bypasses ActiveRecord)
raw_client = Google::Cloud::Spanner.new(
project: connector_config["project"],
emulator_host: connector_config["emulator_host"]
)
db_client = raw_client.client(
connector_config["instance"],
connector_config["database"]
)

results = db_client.execute "SELECT start_time FROM test_types WHERE id = #{record.id}"
raw_value = results.rows.first[:start_time]

# Spanner client returns TIMESTAMP as a UTC Time object
expected_raw_time = ::Time.utc(2017, 7, 4, 12, 19, 10, Rational(897761123, 1000))
assert_equal expected_raw_time, raw_value
assert_equal 897761123, raw_value.nsec
assert_equal "UTC", raw_value.zone

record.reload

assert_equal expected_time, record.start_time
assert_equal 897761123, record.start_time.nsec
assert_instance_of ActiveSupport::TimeWithZone, record.start_time
assert_equal "Paris", record.start_time.time_zone.name
ensure
TestTypeModel.time_zone_aware_attributes = false
TestTypeModel.reset_column_information
::Time.zone = old_zone
end
end

def test_string_value_with_utc_and_local_timezones
# 1. With 'Z' (UTC)
string_value_utc = "2017-07-04 14:19:10.897761123Z"
expected_time_utc = ::Time.utc 2017, 7, 4, 14, 19, 10, Rational(897761123, 1000)
record = TestTypeModel.new start_time: string_value_utc
assert_equal expected_time_utc, record.start_time
assert_equal 897761123, record.start_time.nsec

# 2. With Offset (+02:00)
string_value_offset = "2017-07-04 14:19:10.897761123+02:00"
# 14:19:10+02:00 is 12:19:10 UTC
expected_time_offset = ::Time.utc 2017, 7, 4, 12, 19, 10, Rational(897761123, 1000)
record = TestTypeModel.new start_time: string_value_offset
assert_equal expected_time_offset, record.start_time
assert_equal 897761123, record.start_time.nsec

# 3. Without offset, but with ActiveRecord.default_timezone = :local
old_default_timezone = ActiveRecord.default_timezone
ActiveRecord.default_timezone = :local
begin
string_value_no_offset = "2017-07-04 14:19:10.897761123"
# Should be interpreted as local time
expected_time_local = ::Time.local 2017, 7, 4, 14, 19, 10, Rational(897761123, 1000)
record = TestTypeModel.new start_time: string_value_no_offset
assert_equal expected_time_local, record.start_time
assert_equal 897761123, record.start_time.nsec
ensure
ActiveRecord.default_timezone = old_default_timezone
end
end
end
end
end
end
21 changes: 21 additions & 0 deletions examples/snippets/Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

require_relative "config/environment"
require "docker"
require "google/cloud/spanner"

def fetch_samples
Dir.entries(".").select do |entry|
Expand Down Expand Up @@ -50,6 +51,25 @@ task :run, [:sample] do |_t, args|
run_sample sample
end

def wait_for_emulator
puts "Waiting for Spanner emulator to be ready..."
spanner = Google::Cloud::Spanner.new project: "test-project", emulator_host: "127.0.0.1:9010"
retries = 20
begin
# Make a cheap API call to verify the emulator is actually responding
spanner.instances
puts "Spanner emulator is ready."
rescue StandardError => e
if retries > 0
retries -= 1
sleep 0.5
retry
else
puts "Timed out waiting for Spanner emulator. Last error: #{e.message}"
end
end
end

def run_sample sample
puts "Running #{sample}"
puts "Downloading Spanner emulator image..."
Expand All @@ -68,6 +88,7 @@ def run_sample sample
begin
puts "Starting Spanner emulator..."
container.start!
wait_for_emulator
Dir.chdir sample do
sh "ruby ../bin/create_emulator_instance.rb"
sh "rake db:migrate"
Expand Down
31 changes: 22 additions & 9 deletions lib/active_record/type/spanner/time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,24 @@
module ActiveRecord
module Type
module Spanner
class Time < ActiveRecord::Type::Time
class Time < ActiveRecord::Type::DateTime
def cast_value value

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To support nanoseconds precision in GoogleSQL dialect. By default cast_value is stripping the precision to 6.

if value.is_a? ::String
return if value.empty?
begin
if ActiveRecord.default_timezone == :utc
::DateTime.parse(value).to_time.getutc
else
::Time.parse(value).getlocal
end
rescue StandardError
super
end
else
super
end
end

def serialize_with_isolation_level value, isolation_level
if value == :commit_timestamp
return "PENDING_COMMIT_TIMESTAMP()" if isolation_level == :dml
Expand All @@ -24,18 +41,14 @@ def serialize value
val.acts_like?(:time) ? val.utc.rfc3339(9) : val
end

def user_input_in_time_zone value
return value.in_time_zone if value.is_a? ::Time
super value
def value_from_multiparameter_assignment values
defaults = { 1 => 2000, 2 => 1, 3 => 1 }
super defaults.merge(values)
end

private

def cast_value value
if value.is_a? ::String
value = value.empty? ? nil : ::Time.parse(value)
end

def apply_seconds_precision value

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To support nanoseconds precision in GoogleSQL dialect. By default cast_value is stripping the precision to 6.

value
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ def test_create_singer_with_last_performance_as_non_iso_string
# could represent both 4th of July and 7th of April. This test verifies that it is encoded to the
# same value as ::Time.parse(..) would encode it to.
timestamp_string = "04/07/2017 2:19pm"
timestamp = ::Time.parse(timestamp_string)
timestamp = ::Time.utc(2017, 7, 4, 14, 19, 0)

Singer.transaction do
Singer.create(first_name: "Dave", last_name: "Allison", last_performance: timestamp_string)
Expand All @@ -689,7 +689,7 @@ def test_find_singer_by_last_performance_as_non_iso_string
# could represent both 4th of July and 7th of April. This test verifies that it is encoded to the
# same value as ::Time.parse(..) would encode it to.
timestamp_string = "04/07/2017 2:19pm"
timestamp = ::Time.parse(timestamp_string)
timestamp = ::Time.utc(2017, 7, 4, 14, 19, 0)
Singer.find_by(last_performance: timestamp_string)

request = @mock.requests.select {|req| req.is_a?(Google::Cloud::Spanner::V1::ExecuteSqlRequest) && req.sql == select_sql }.first
Expand Down
Loading