Move data from removed column to just created one in Rails migration - ruby-on-rails

I have a table 'Invoices' which contains two boolean columns:
Table name: invoices
id :integer not null, primary key
...
sent :boolean default(FALSE)
payment_received :boolean default(FALSE)
This two columns define status of invoice:
def status
if sent & payment_received
:paid
elsif sent | payment_received
:sent
else
:created
end
end
One day it was desided to remove these boolean columns and create new column that will contain invoice status by the help of Rails enum
status :integer
enum status: [ :created, :sent, :paid ]
So now I need to do 3 things:
Add new column 'status'
Calculate status for existing invoices, update status column
Remove columns 'sent' and 'payment_received'.
How can I do this? I can do this task easily on my local environment, but I can't understand how can I do this on production server. If, for example, I'll create a migration that update my table and a rake task that calculate status, migration pass first and my data from boolean columns will be removed before I can use them.
Note: if somehow it's important: I use Postgres.
Any help is appreciated!

Try the following migration.
class UpdateInvoicesTable < ActiveRecord::Migration
def self.up
add_column :invoices,:status,:string
Invoice.find_in_batches(batch_size: 2000) do |invoices|
invoices.each do |invoice|
if invoice.sent & invoice.payment_received
invoice.status = 'paid'
elsif invoice.sent | invoice.payment_received
invoice.status = 'sent'
else
invoice.status = 'created'
end
invoice.save
end
end
remove_column :invoices,:sent
remove_column :invoices,:payment_received
end
end

Related

When I run a schema migration before a data migration, with ActiveRecord, data does not properly update in DB

As of now, I have a users table with columns
id, name, email, status
status field is an integer type with values 1 and 2 representing an Active and Inactive user, respectively.
I would like to change the status field to a string type and migrate the data -- Convert 1 to "Active" and 2 to "Inactive"
I generated 2 migration files with rails g migration
user.rb
class User < ApplicationRecord
module Status
ACTIVE = 'Active'.freeze
INACTIVE = 'Inactive'.freeze
ALL = [ACTIVE, INACTIVE].freeze
end
validates :status, presence: true
validates :status, inclusion: Status::ALL
end
db/migrate/20190906115523_update_user_status_type.rb
def UpdateUserStatusType < ActiveRecord::Migration[5.2]
def up
change_column :users, :status, :string, default: User::Status::ACTIVE,
end
def down
User.where(status: User::Status::ACTIVE).update_all(status: 1)
User.where(status: User::Status::INACTIVE).update_all(status: 2)
change_column :users, :status, :integer, default: 1
end
end
db/migrate/20190906115828_update_user_statuses.rb
def UpdateUserStatuses < ActiveRecord::Migration[5.2]
def data
User.where(status: 1).update_all(status: User::Status::ACTIVE)
User.where(status: 2).update_all(status: User::Status::INACTIVE)
end
end
After running rails db:migrate
Expected: Each user's status should be converted to either "Active" or "Inactive" after migrations are finished.
Actual Results: Each user's status are converted to "0" of string type.
You're assuming after the first migration runs (change_column :users, :status, :string, default: User::Status::ACTIVE) you can still fetch the old values from the status column which is not the case. When you change the type of that column to string all the integer values are invalid so I suspect your database just changes all the invalid values to be "0" instead.
If I was told to make this change to an application that is heavily used in production, I would be roll out this change in a few separate pull requests/migrations. I'd create a whole new separate column, iterate through all the users, set the value of the new column depending on what the value in the old column is, and then delete the old column. This is a much safer way to make this change.

Setting up a polymorphic association where associated tables have different primary key types (UUID and Integer)

I have 5 tables which are all likeable by a user, and a likes table which records this.
The problem is one of the 5 tables uses uuid and not integer for their primary key. So I can't have a typical likeable_id & likeable_type
Is it possible to set up a polymorphic association that handles different primary key types?
Thanks in advance
I don't have your tables name, but here is an example for a join table
class CreateJoins < ActiveRecord::Migration
def change
create_table :joins do |t|
t.belongs_to :uuid_id_model, type: :uuid # you specify the type
t.belongs_to :integer_id_model # you don't need anything
t.index %i[uuid_id_model integer_id_model], unique: true
t.timestamps
end
end
end
I other words, specify on the columns that they are of type uuid
If you need to change a column type of your join tables, you can do like so :
class ChangeColumnType < ActiveRecord::Migration
def change
change_column :some_join_table, :some_join_column, :uuid
end
end
The solution I ended up with is adding a likeable_id_string which stores any uuid primary key. I then override the getters and setters to do the following:
# Overriding getters and setters to handle multiple primary key types on
# this polymorphic association ( String and Integer )
# Setters
def likeable=(value)
if !value.nil? && value.id.is_a?(String)
self[:likeable_id] = nil
self[:likeable_id_string] = value.id
self[:likeable_type] = value.class.name
else
self[:likeable_id_string] = nil
super(value)
end
end
# in case likeable_type and likeable_id are updated seperately. I believe
# this is done in FE (rather than passing an object to .likeable)
def likeable_id=(value)
if !value.nil? && value.is_a?(String)
self[:likeable_id] = nil
self[:likeable_id_string] = value
else
self[:likeable_id_string] = nil
super(value)
end
end
# Getters
def likeable
if self.likeable_id
return super
elsif !self.likeable_type.nil?
return self.likeable_type.constantize.find(likeable_id_string)
else
return nil
end
end
You would think that the likeable= setter isn't needed once I've overridden the likeable_id= .. but seems like it is. If anyone knows why I need both please let me know as it would seem logical that likeable delegates to likeable_id and likeable_type
I feel like there's a use case for this to be a Gem. Something I might do soon. Please get in touch if you would like to see this as a gem (or if you think that's a terrible idea)

Issue with check times overlaps

There are two models:
# Table name: activities_people
#
# activity_id :integer not null
# person_id :integer not null
# date :date not null
# id :integer not null, primary key
# == Schema Information
#
# Table name: activities
#
# id :integer not null, primary key
# name :string(20) not null
# description :text
# active :boolean not null
# day_of_week :string(20) not null
# start_on :time not null
# end_on :time not null
Relations:
activity.rb
has_many :activities_people
has_many :people, through: :activities_people
activity_people.rb
belongs_to :person
belongs_to :activity
I try to create validation that person can join to one activity taking place in specific date and time(start_on, end_on). If I will try sign up to another activity while before I joined to other exercises(same date, and times overlap) should throw error.
What I try:
def check_join_client
activities_date = person.activities_people.where('date = date', date: date)
if activities_date.exists && person.activities.where('id IN (?)', activities_date)
end
I don't know how to use create query(person.activities.where ...) to getting person activities related with activies_people. activities_date check if we joined to activities taking place in same date. Second I want get check start_on and end_on.
Thanks in advance.
If I'm understanding you correctly, you want to find the activites_people for a user that match a query by the date array and then raise an error unless an associated activity for those matched activities_people.
Your original code for check_join_client uses if incorrectly:
def check_join_client
activities_date = person.activities_people.where('date = date', date: date)
if activities_date.exists && person.activities.where('id IN (?)', activities_date)
end
To translate this to pseudocode, you're essentially saying:
result = query if result.condition_met?
However the if condition (the expression after the if) will be evaluated before you define results. It might be more clear if I show a correct approach:
result = query
return result if result.condition_met?
Now to go back to your question about loading associated records, try something like this:
activities_people_ids_matching_date = person.activities_people
.where(date: self.date)
.pluck(:id)
# use pluck to get an array of ids because that's all that's needed here
# not sure how you're getting the date variable, so I'm assuming self.date will work
# I can use `.any?` to see if the ids list is not empty.
condition_met = activities_people_ids_matching_date.any? &&\
person.activities
.where(activities_people_id: activities_people_ids_matching_date)
.any?
condition_met ? true : raise(StandardError, "my error")
There surely is a way to get this done with one query instead of two, but it seems like where you're at with Ruby concepts it's more important to focus on syntax and core functionality than SQL optimization.
The correct syntax (one of several options) is:
person.activities_people.where(date: date)

Seeding datetime into database fails

I am trying to seed a datetime into my PostgreSQL database:
6.times do |spots|
title = "FUN STUFF NUMBER #{spots+1}"
content = Faker::Lorem.sentence
user_id = spots + 1
spots = spots + 1
starts_at = DateTime.now + 1
Cposting.create!(
title: title,
content: content,
user_id: user_id,
spots: spots,
starts_at: starts_at
)
end
The migration looks like this, nothing special
class AddStartsAtToCpostings < ActiveRecord::Migration
def change
add_column :cpostings, :starts_at, :datetime
end
end
The problem is that starts_at after seeding is nil. Any idea why? Everything else gets inserted without problems. Also if I insert something manually in the database through the form it works, too.
Just use:
starts_at = Time.now
or,
starts_at = Time.now + 1
Update 1:
You can try this:
Change your migration to be like this:
class AddStartsAtToCpostings < ActiveRecord::Migration
def change_table
add_column :cpostings, :starts_at, :datetime
end
end
Use change_table method instead of change.
Update 2:
I just tested your migration locally. Your migration works fine and I can create objects from rails console using your migration and schema where start_at works fine if I use Time.now.

How to create a "collate nocase" column in a table in migration?

This answer https://stackoverflow.com/a/973785/1297371 to the question: How to set Sqlite3 to be case insensitive when string comparing?
tells how to create table with "COLLATE NOCASE" column.
My question is how to create such column in a rails migration?
i.e. how from
create table Test
(
Text_Value text collate nocase
);
create index Test_Text_Value_Index
on Test (Text_Value collate nocase);
do:
create_table :tests do
#WHAT HERE?
end
add_index #... WHAT HERE?
I added new migration where I deleted and recreated the index like:
class AddIndexToWordVariations < ActiveRecord::Migration
def change
remove_index :word_variations, :variation_word
add_index :word_variations, :variation_word, :COLLATE => :NOCASE
end
end
where 'word_variations' is my table
For people searching, if 'collate' is still not available in your version of rails (you'll get "Unknown key: :COLLATE"), then you have to create the index manually:
execute("create index Test_Text_Value_Index on Test (Text_Value collate nocase);")
In your 'def up' (the 'drop_table' will drop the index as well in your 'def down')

Resources