-
Notifications
You must be signed in to change notification settings - Fork 63
Add incorrect Postgres timestamp type check #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,3 +7,4 @@ test/dummy/.sass-cache | |
| *.gem | ||
| Gemfile.lock | ||
| gemfiles/**.lock | ||
| /.idea | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "active_record_doctor/detectors/base" | ||
|
|
||
| module ActiveRecordDoctor | ||
| module Detectors | ||
| class IncorrectTimestampType < Base # :nodoc: | ||
| @description = "Detects timestamp columns without timezone information on PostgreSQL." | ||
| @config = { | ||
| ignore_tables: { | ||
| description: "Tables whose timestamp columns should not be checked.", | ||
| global: true | ||
| }, | ||
| ignore_columns: { | ||
| description: "Columns that should not be checked.", | ||
| global: true | ||
| } | ||
| } | ||
|
|
||
| private | ||
|
|
||
| def message(error: nil, table: nil, column: nil) | ||
| return error if error | ||
|
|
||
| <<~MESSAGE | ||
| Incorrect timestamp type: The column `#{table}.#{column}` is `timestamp without time zone`. | ||
| It's recommended to use `timestamp with time zone` for PostgreSQL. | ||
| MESSAGE | ||
| end | ||
|
|
||
| def detect | ||
| return unless Utils.postgresql?(connection) | ||
|
|
||
| each_table(except: config(:ignore_tables)) do |table| | ||
| each_column(table, except: config(:ignore_columns)) do |column| | ||
| next unless timestamp_column?(column) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this method call necessary, given |
||
|
|
||
| # For PostgreSQL, column.sql_type will be 'timestamp without time zone' | ||
| # or 'timestamp with time zone'. | ||
| # Other databases might have different sql_type strings. | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to mention other database, as this detector won't run for them, so this sounds like an irrelevant detail. |
||
| if column.sql_type == "timestamp without time zone" | ||
| problem!(table: table, column: column.name) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| check_rails_default_timezone | ||
| check_sql_adapter | ||
|
Comment on lines
+47
to
+48
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't have config checkers now, so I suggest removing them for now. I'd like to think a bit more about how to make them work. |
||
| end | ||
|
|
||
| def check_rails_default_timezone | ||
| return unless defined?(Rails) | ||
| return unless Rails.respond_to?(:initialized?) && Rails.initialized? | ||
| return if Rails.application.config.active_record.default_timezone == :utc | ||
|
|
||
| problem!(error: <<~MESSAGE) | ||
| Rails time zone is not set to UTC. | ||
| This can lead to incorrect handling of timestamps without timezone information. | ||
| You can set it in your application configuration like this: | ||
| Rails.application.config.time_zone = "UTC" | ||
| MESSAGE | ||
| end | ||
|
|
||
| def check_sql_adapter | ||
| return unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) | ||
| return if ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type == :timestamptz | ||
|
|
||
| problem!(error: <<~MESSAGE) | ||
| PostgreSQL adapter's datetime type is not set to `timestamptz`. | ||
| This can lead to incorrect handling of timestamps without timezone information. | ||
| You can set it in your initializer like this: | ||
| ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz | ||
| MESSAGE | ||
| end | ||
|
|
||
| def timestamp_column?(column) | ||
| [:datetime, :timestamp].include?(column.type) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class ActiveRecordDoctor::Detectors::IncorrectTimestampTypeTest < Minitest::Test | ||
| def setup | ||
| skip unless postgresql? | ||
| ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we remove that and use |
||
| end | ||
|
|
||
| def test_timestamp_without_time_zone_is_error | ||
| Context.create_table(:events) do |t| | ||
| t.column :occurred_at, :timestamp, null: false | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is |
||
| end.define_model | ||
|
|
||
| # Simulate column.sql_type as 'timestamp without time zone' | ||
| ActiveRecord::Base.connection.change_column :events, :occurred_at, "timestamp without time zone" | ||
|
Comment on lines
+14
to
+15
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's pass the right type when the column is being created. |
||
|
|
||
| assert_problems(<<~OUTPUT) | ||
| Incorrect timestamp type: The column `events.occurred_at` is `timestamp without time zone`. | ||
| It's recommended to use `timestamp with time zone` for PostgreSQL. | ||
| OUTPUT | ||
| end | ||
|
|
||
| def test_timestamp_with_time_zone_is_ok | ||
| Context.create_table(:events) do |t| | ||
| t.timestamptz :occurred_at, null: false | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above re |
||
| end.define_model | ||
|
|
||
| refute_problems | ||
| end | ||
|
|
||
| def test_ignore_tables | ||
| Context.create_table(:events) do |t| | ||
| t.column :occurred_at, :timestamp, null: false | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above re |
||
| end.define_model | ||
|
|
||
| config_file(<<-CONFIG) | ||
| ActiveRecordDoctor.configure do |config| | ||
| config.detector :incorrect_timestamp_type, | ||
| ignore_tables: ["events"] | ||
| end | ||
| CONFIG | ||
|
|
||
| refute_problems | ||
| end | ||
|
|
||
| def test_ignore_columns | ||
| Context.create_table(:events) do |t| | ||
| t.column :occurred_at, :timestamp, null: false | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above re |
||
| end.define_model | ||
|
|
||
| config_file(<<-CONFIG) | ||
| ActiveRecordDoctor.configure do |config| | ||
| config.detector :incorrect_timestamp_type, | ||
| ignore_columns: ["events.occurred_at"] | ||
| end | ||
| CONFIG | ||
|
|
||
| refute_problems | ||
| end | ||
|
|
||
| def test_no_timestamp_columns_is_ok | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this test needed? |
||
| Context.create_table(:events) do |t| | ||
| t.string :name | ||
| end.define_model | ||
|
|
||
| refute_problems | ||
| end | ||
|
|
||
| def test_postgresql_adapter_datetime_type_timestamptz | ||
| return unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) | ||
|
|
||
| assert_equal ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type, :timestamptz | ||
| end | ||
|
|
||
| def test_rails_config_time_zone_and_default_timezone | ||
| return unless defined?(Rails) | ||
| return unless Rails.respond_to?(:application) | ||
|
|
||
| # assert_equal ActiveRecord::Base.default_timezone, :utc | ||
| # Ensure that the default timezone is set to UTC in the application config | ||
| assert_equal Rails.application.config.active_record.default_timezone, :utc | ||
| end | ||
|
Comment on lines
+69
to
+82
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove configuration-related tests along with the corresponding production code. |
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,11 @@ class ApplicationRecord < ActiveRecord::Base | |
|
|
||
| ActiveRecord::Base.establish_connection :primary | ||
|
|
||
| # Enable pgcrypto extension for PostgreSQL if needed (for gen_random_uuid) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this needed by the changes submitted in this PR? If not, let's remove this. |
||
| if adapter == "postgresql" | ||
| ActiveRecord::Base.connection.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;") | ||
| end | ||
|
|
||
| # Transient Record contexts used by the test class below. | ||
| Context = TransientRecord.context_for ApplicationRecord | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be tool-specific, not project specific, and thus belongs to the system-wide or user-level
gitignore, not a project.gitignore. Let's remove this entry.