diff --git a/db/migrate/20241212180047_set_language_id_not_null_on_users.rb b/db/migrate/20241212180047_set_language_id_not_null_on_users.rb new file mode 100644 index 000000000..75fcb5421 --- /dev/null +++ b/db/migrate/20241212180047_set_language_id_not_null_on_users.rb @@ -0,0 +1,29 @@ +class SetLanguageIdNotNullOnUsers < ActiveRecord::Migration[6.1] + + def up + # Ensure default language exists before applying the changes + default_language_id = handle_default_language_id + # Disallow null values for user.language_id + # Also, set all currently null user.language_id values to the app's default language_id + change_column_null :users, :language_id, false, default_language_id + end + + def down + # Allow null values for user.language_id + change_column_null :users, :language_id, true + end + + private + + # Return Language.default.id or raise an exception if not found, causing the migration to fail + def handle_default_language_id + default_language = Language.default + + if default_language.nil? + Rails.logger.error 'Error: Language.default not found. Please ensure the default language is set.' + message = 'Migration aborted: No database changes were performed due to missing Language.default.' + raise StandardError, message + end + default_language.id + end +end diff --git a/db/schema.rb b/db/schema.rb index 466b1fbda..c835db3c1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_08_20_190548) do +ActiveRecord::Schema.define(version: 2024_12_12_180047) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -819,7 +819,7 @@ t.string "api_token" t.integer "invited_by_id" t.string "invited_by_type" - t.integer "language_id" + t.integer "language_id", null: false t.string "recovery_email" t.boolean "active", default: true t.integer "department_id" diff --git a/lib/tasks/update_nil_user_language_ids_from_orgs.rake b/lib/tasks/update_nil_user_language_ids_from_orgs.rake new file mode 100644 index 000000000..50fcf15c0 --- /dev/null +++ b/lib/tasks/update_nil_user_language_ids_from_orgs.rake @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +namespace :data_migration do + desc 'Updates nil `user.language_id` values using `user.org.language_id`' + task update_nil_user_language_ids_from_orgs: :environment do + users_to_be_updated = query_nil_language_users_for_update + # Exit rake task if users_to_be_updated is empty + ensure_update_needed(users_to_be_updated) + # user_ids will be used to later output the updated results + user_ids = users_to_be_updated.pluck(:id) + # Perform the update + update_users(users_to_be_updated) + # Output a breakdown of the updated results + output_update_results(user_ids) + # Output if any users still have nil language_id after update + output_remaining_nil_language_users + puts 'Rake task completed.' + end + + private + + # Queries users that will be updated within this rake task + # The following nil_language users WILL NOT be updated within this rake task: + # - users where `user.org == nil` or `user.org.language_id == nil` + # (However, no such users should exist) + def query_nil_language_users_for_update + query_nil_language_users.joins(:org) + .where.not(orgs: { language_id: nil }) + end + + def query_nil_language_users + User.where(language_id: nil) + end + + # Exits rake task if there is no update to perform + # Otherwise, outputs number of users to be updated + def ensure_update_needed(users_to_be_updated) + if users_to_be_updated.empty? + print_and_warn('No users to be updated. Exiting rake task.') + exit(0) + else + puts "Found #{users_to_be_updated.count} users to update." + end + end + + # Performs a single bulk update on users_to_be_updated + def update_users(users_to_be_updated) + puts 'Proceeding to use `user.org.language_id` to update each nil `user.language_id`' + puts '------------------------------------------------------------------------' + + # Subquery for update_all() action + # Fetches `user.org.language_id` for all users in `users_to_be_updated` + subquery = Org.where('orgs.id = users.org_id').select(:language_id).limit(1) + + # Batch update `users_to_be_updated` with `user.org.language_id` values from subquery + users_to_be_updated.update_all("language_id = (#{subquery.to_sql})") + + puts 'Update complete.' + end + + # Outputs a breakdown of the updated language_id counts + def output_update_results(user_ids) + language_id_counts = User.where(id: user_ids) + .group(:language_id) + .count + + puts 'Summary of language_id counts for updated users:' + + language_id_counts.each do |language_id, count| + puts "language_id: #{language_id}, count: #{count}" + end + + puts "Total count: #{language_id_counts.values.sum}" + end + + def output_remaining_nil_language_users + # Query remaining users with nil language_id + # Now we are using query_nil_language_users (not query_nil_language_users_for_update) + remaining_users = query_nil_language_users + if remaining_users.exists? + # NOTE: Any remaining nil language_id users will be addressed in the SetLanguageIdNotNullOnUsers migration + print_and_warn("#{remaining_users.count} users still have a nil language_id.") + else + puts 'Successfully updated language_id for all users (i.e. All users now have a non-nil language_id value).' + end + end + + # Message is printed to console and logs a warning + def print_and_warn(message) + puts message + Rails.logger.warn(message) + end +end