Backfill default values to a column through migration - ruby-on-rails

I have a table(Users). In one of its columns(configs) i added a default value ("A"=>0) through a migration. Now all the new users i create have default value of A but the old users don't. I want to backfill the default value of A for the old users using migration. How do I do that?
given:
t.jsonb "configs", default: {"B"=>7, "C"=>10, "D"=>10}
This is my existing column. Here B, C and D have different values for different Users. I want to make it into
t.jsonb "configs", default: {"B"=>7, "C"=>10, "D"=>10, "A"=>0}
where the values of B, C and D stays the same for all Users but just the default value of "A" gets added to the existing json in the column.
rails - 4.2.11
db - postgres
I have gone through some documentations but couldn't find a comprehensive answer. Any help is appreciated.

From your comments is sounds like you want to update a JSONB column to have a new set of defaults, and any existing json hashes get the new key/value pair of "A": 0 added to the current value. A migration can change the DB but you will need to do it programmatically to update the rows that have values already, especially because their values are not all the same. With that said it could be something like:
User.all.each do |u|
u.configs["A"] = 0
u.save
end
This will iterate through all of the users and set the value of "a" to zero. If no "a" exists in the hash it will add it with the value of zero without touching anything else in the JSON. If "a" already exists for a user it will be set to zero. So if you have users whose value for "a" has already changed from the default of zero you can avoid them with:
User.all.each do |u|
unless conifigs["A"] # if "A" already exists skip this
u.configs["A"] = 0
u.save
end
end
Please read https://nandovieira.com/using-postgresql-and-jsonb-with-ruby-on-rails for information on how to leverage JSONB in Rails. It is a very powerful tool if you put in the code to really get the most use out of it. Be sure and read the part about store_accessor, it would help you to do a lot more with that JSONB column.

Related

Rails SQLite check returning double

I'm using Rails to search through a SQLite table (for other reasons I can't use the standard database-model system) using a SELECT query like so:
info = ActiveRecord::Base.connection.execute("SELECT * FROM #{form_name} WHERE EmailAddress = \"#{user_em}\";")
This returns the correct values, but for some reason the output is in duplicate, the difference being the 2nd set doesn't have column titles in the hash, instead going from 0-[num columns]. For example:
{"id"=>1, "Timestamp"=>"2/27/2017 14:26:03", "EmailAddress"=>"-snip-", 0=>1, 1=>"2/27/2017 14:26:03", 2=>"-snip-"}
(I'll note the obvious- there's only one row in the table with that information in it)
While it's not exactly a fatal problem, I'm interested as to why it's doing so and if it's possible to prevent it. Thanks!
This allows you to read the values both by column index or column name:
id = row[0]
timestamp = row["Timestamp"]

Find a record by bigint column with wildcard

I have an API that has a database with UPC-12 values in it. Sometimes API calls will come in with UPC-10 codes. My db upc column is bigint, so it removes the leading 0 on a UPC-12 value. That leaves the last digit as a wildcard when comparing to UPC-10.
I'd like to be able to check a UPC-10 value against records in the db to see if there's a match. Since I can't use LIKE, how do I do that?
The goal is to do something like:
def self.pull_product(upc)
upc_string = upc.to_s
if upc_string.length == 10
# product = Product.where... use a wildcard to try and match to existing record
else
product = Product.find_by_upc(upc)
end
end
This Rails 4 and Postgresql.
Just to clarify:
I might have a UPC-10 api call with a upc param like: 7618600002. My database has the UPC-12 equivalent: 76186000023. So if I just query for the param in the api call, I'll get nil.
I need a way to match the the UPC-10 param against my UPC-12 value in the database.
You need to use SQL like this:
upc between upc_string::int*10 and upc_string::int*10+9
I have no idea how to code it in Rails though.

Rails has_many with an integer primary key and a string foreign key

I have three rails objects: User, DemoUser and Stats. Both the User and the DemoUser have many stats associated with them. The User and Stats tables are stored on Postgresql (using ActiveRecord). The DemoUser is stored in redis. The id for the DemoUser is a (random) string. The id for the User is a (standard-rails) incrementing integer.
The stats table has a user_id column that can contain either the User id or the DemoUser id. For that reason, the user_id column is a string, rather than an integer.
There isn't an easy way to translate from the random string to an integer, but there's a very easy way to translate the integer id to a string (42 -> "42"). The ids are guaranteed not to overlap (there won't be a User instance with the same id as a DemoUser, ever).
I have some code that manages those stats. I'd like to be able to pass over a some_user instance (which can either be a DemoUser or a User) and then be able to use the id to fetch Stats, update them etc. Also would be nice to be able to define a has_many for the User model, so I can do things like user.stats
However, operations like user.stats would create a query like
SELECT "stats".* FROM "stats" WHERE "stats"."user_id" = 42
which then breaks with PG::UndefinedFunction: ERROR: operator does not exist: character varying = integer
Is there a way to either let the database (Postgresql), or Rails do auto-translation of the ids on JOIN? (the translation from integer to string should be simple, e.g. 42 -> "42")
EDIT: updated the question to try to make things as clear as possible. Happy to accept edits or answer questions to clarify anything.
You can't define a foreign key between two types that don't have built-in equality operators.
The correct solution is to change the string column to be an integer.
In your case you could create a user-defined = operator for varchar = string, but that would have messy side effects elsewhere in the database; for example, it would allow bogus code like:
SELECT 2014-01-02 = '2014-01-02'
to run without an error. So I'm not going to give you the code to do that. If you truly feel it's the only solution (which I don't think is likely to be correct) then see CREATE OPERATOR and CREATE FUNCTION.
One option would be to have separate user_id and demo_user_id columns in your stats table. The user_id would be an integer that you could use as a foreign key to the users table in PostgreSQL and the demo_user_id would be a string that would link to your Redis database. If you wanted to treat the database properly, you'd use a real FK to link stats.user_id to users.id to ensure referential integrity and you'd include a CHECK constraint to ensure that exactly one of stats.user_id and stats.demo_user_id was NULL:
check (user_id is null <> demo_user_id is null)
You'll have to fight ActiveRecord a bit to properly constrain your database of course, AR doesn't believe in fancy things like FKs and CHECKs even though they are necessary for data integrity. You'd have to keep demo_user_id under control by hand though, some sort of periodic scan to make sure they link up with values in Redis would be a good idea.
Now your User can look up stats using a standard association to the stats.user_id column and your DemoUser can use stats.demo_user_id.
For the time being, my 'solution' is not to use a has_many in Rails, but I can define some helper functions in the models if necessary. e.g.
class User < ActiveRecord::Base
# ...
def stats
Stats.where(user_id: self.id.to_s)
end
# ...
end
also, I would define some helper scopes to help enforce the to_s translation
class Stats < ActiveRecord::Base
scope :for_user_id, -> (id) { where(user_id: id.to_s) }
# ...
end
This should allow calls like
user.stats and Stats.for_user_id(user.id)
I think I misunderstood a detail of your issue before because it was buried in the comments.
(I strongly suggest editing your question to clarify points when comments show that there's something confusing/incomplete in the question).
You seem to want a foreign key from an integer column to a string column because the string column might be an integer, or might be some unrelated string. That's why you can't make it an integer column - it's not necessarily a valid number value, it might be a textual key from a different system.
The typical solution in this case would be to have a synthetic primary key and two UNIQUE constraints instead, one for keys from each system, plus a CHECK constraint preventing both from being set. E.g.
CREATE TABLE my_referenced_table (
id serial,
system1_key integer,
system2_key varchar,
CONSTRAINT exactly_one_key_must_be_set
CHECK (system1_key IS NULL != system2_key IS NULL),
UNIQUE(system1_key),
UNIQUE(system2_key),
PRIMARY KEY (id),
... other values ...
);
You can then have a foreign key referencing system1_key from your integer-keyed table.
It's not perfect, as it doesn't prevent the same value appearing in two different rows, one for system1_key and one for system2_key.
So an alternative might be:
CREATE TABLE my_referenced_table (
the_key varchar primary key,
the_key_ifinteger integer,
CONSTRAINT integerkey_must_equal_key_if_set
CHECK (the_key_ifinteger IS NULL OR (the_key_ifinteger::varchar = the_key)),
UNIQUE(the_key_ifinteger),
... other values ...
);
CREATE OR REPLACE FUNCTION my_referenced_table_copy_int_key()
RETURNS trigger LANGUAGE plpgsql STRICT
AS $$
BEGIN
IF NEW.the_key ~ '^[\d]+$' THEN
NEW.the_key_ifinteger := CAST(NEW.the_key AS integer);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER copy_int_key
BEFORE INSERT OR UPDATE ON my_referenced_table
FOR EACH ROW EXECUTE PROCEDURE my_referenced_table_copy_int_key();
which copies the integer value if it's an integer, so you can reference it.
All in all though I think the whole idea is a bit iffy.
I think I may have a solution for your problem, but maybe not a massively better one:
class User < ActiveRecord::Base
has_many :stats, primary_key: "id_s"
def id_s
read_attribute(:id).to_s
end
end
Still uses a second virtual column, but maybe more handy to use with Rails associations and is database agnostic.

How to update attribute that is being validate for uniqueness

Table 'leave_policies' has year, increment and total_entitled fields.
I have written 'validates_uniqueness_of :year' in LeavePolicy model.
update_attributes not working even I am not updating year field.
Please guide for mistake or suggest any better solution.
Class LeavePolicy
validates_uniqueness_of :year
end
#leave_policy is object of LeavePolicy
In table a row with :id = 1 ,year = 1 , increment= 2 , total_entitled = 3. If I update row with id : 1 like
#leave_policy.update_attributes(:total_entitled => 5)
I got the error "year is already taken".
You are sure there is no other row with year of 1, and that the #leave_policy is actually an instance of the row with id 1?
If it were actually a new record, it would fail this way, or if there were any other record with :year == 1.
Validations are run on all attributes, even if you use update_attributes only on some, so if the year is not unique, it will fail even if you are only updating the increment or total_entitled fields.
In general, validates_uniqueness is not a great answer on its own, as race conditions from checking in ruby can prevent it from enforcing uniqueness, so I would also use it with a unique key in the database.
If you want to be able to skip the unique year validation when changing the other fields, you can set the attributes on the model, then call
leave_policy.save(:validate => false) #for rails 3
leave_policy.save(false) #for rails 2
and that will skip all validations, but man, I would think twice.
That seems to defeat the purpose, so I would instead pursue why it is that rails thinks you do not have a unique year - perhaps a look in your db will show there really is a dupe row for the year == 1.

Ruby on Rails+PostgreSQL: usage of custom sequences

Say I have a model called Transaction which has a :transaction_code attribute.
I want that attribute to be automatically filled with a sequence number which may differ from id (e.g. Transaction with id=1 could have transaction_code=1000).
I have tried to create a sequence on postgres and then making the default value for the transaction_code column the nextval of that sequence.
The thing is, if I do not assign any value to #transaction.transaction_code on RoR, when I issue a #transaction.save on RoR, it tries to do the following SQL:
INSERT INTO transactions (transaction_code) VALUES (NULL);
What this does is create a new row on the Transactions table, with transaction_code as NULL, instead of calculating the nextval of the sequence and inserting it on the corresponding column. Thus, as I found out, if you specify NULL to postgres, it assumes you really want to insert NULL into that column, regardless of it having a default value (I'm coming from ORACLE which has a different behavior).
I'm open to any solution on this, either if it is done on the database or on RoR:
either there is a way to exclude attributes from ActiveRecord's
save
or there is a way to change a column's value before insert with a trigger
or there is a way to generate these sequence numbers within RoR
or any other way, as long as it works :-)
Thanks in advance.
For the moment, you might be stuck fetching and assigning the sequence in your ROR model like this:
before_create :set_transaction_code_sequence
def set_transaction_code_sequence
self.transaction_code = self.class.connection.select_value("SELECT nextval('transaction_code_seq')")
end
I'm not particularily fond of this solution, since I'd like to see this corrected in AR directly... but it does do the trick.
If you want to insert the default value in to a column in an INSERT statement, you can use the keyword DEFAULT - no quotes:
INSERT INTO mytable (col1, col2) VALUES (105, DEFAULT);
Or you could spell out the default, nextval(...) in your case. See the manual here.
A trigger for that case is simple. That's actually what I would recommend if you want to make sure that only numbers from your sequence are entered, no matter what.
CREATE OR REPLACE FUNCTION trg_myseq()
RETURNS trigger AS
$BODY$
BEGIN
NEW.mycol := nextval('my_seq');
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
CREATE TRIGGER myseq
BEFORE INSERT
ON mytable
FOR EACH ROW
EXECUTE PROCEDURE trg_myseq();
On a side note:
If you want to assign your own (non-sequential) numbers as 'sequence', I have written a solution for that in an answer a couple of days ago:
How to specify list of values for a postgresql sequence
I was still experiencing this issue with Rails7 - I could see that Rails was generating a NULL in the insert, but changing the column from integer to bigint solved it. - Rails then does not supply a value for my sequenced column and the DEFAULT nextval('number_seq') is used.

Resources