how to improve query using rails and postgres - ruby-on-rails

I have three models postshas one for post_rating, post_waiting_time
post.rb
name_column type example
id integer 20
name string 'welcome'
user_id integer 1
post_rating.rb
name_column type example
id integer 10
rating_label enum poor,average,great
post_id integer 20
post_waiting_time.rb
name_column type example
id integer 10
waiting_label enum 0-3,4-6,7-10
post_id integer 20
i try use rating_label, waiting_label in query use eager_load, joins but can't write multi label in query
posts_lists = user.posts.eager_load(:post_rating, :post_waiting_time).where("post_waiting_times.waiting_label = ? ", '0-3')
this query work good with one value but i need multi query use waiting_label for example 0-3, 4-7 i try use IN() but i have error
Post.eager_load(:post_rating, :post_waiting_time).where('post_waiting_times.waiting_label IN( ? )', 'more_30,0-
3')
error message for last query
ActiveRecord::StatementInvalid (PG::InvalidTextRepresentation: ERROR: invalid input value for enum tag_label: "more_30,0-3")
LINE 1: ...ERE (post_waiting_times.waiting_label IN( 'more_30,0...
i fix this with
write query for filter by rating_label, waiting_label
posts_lists = user.posts
posts_lists.each do |record|
flag_filter = false
unless waiting_filter.empty?
flag_filter = if waiting_filter.include?(record.post_waiting_time.waiting_label)
true
else
false
end
end
unless rating_filter.empty?
flag_filter = if rating_filter.include?(record.post_rating.rating_label)
true
else
false
end
end
if flag
puts record
end
but this not efficiency way
i use Rails 6, PostgreSQL

You are doing it wrong, you can directly pass an array for comparison in rails. Here is the updated query
Post.eager_load(:post_rating, :post_waiting_time).where(post_waiting_times: {waiting_label: ['more_30','0-3']})
Hope this solves the problem.

Related

How to prevent added quotes in Order by in Rails

I am attempting to order by a column of type "character varying []" and cast it to an integer[] while sorting (instead of using the default ASCII comparison sort). I am using a Postgresql database.
I've found that the following query works as expected:
select <col> from <table> order by <col>::integer[] desc
Unfortunately, when I attempt to programmatically do this in rails it is adding quotes around the column and casting suffix. This results in it thinking "::integer[]" is part of the column name - and of course there is no such column. Thus the query fails.
Here is the rails code:
scope.order([ '<col>::integer[]', 'desc', 'NULLS LAST' ].join(' '))
And this is the query it produces:
select <col> from <table> order by "<table>"."<col>::integer[]" desc
How can I implement this properly with rails?
Thanks to #engineersmnky comment, I found the solution that I need in my code.
In the code I'm actually processing an array of columns (and directions) to sort by. It turns out the solution was indeed to use the Arel.sql() function to process the order by parameters prior to calling scope.order(), with the end result looking something like this:
def sort(scope, sorts)
str = \
sorts.map |sort| do
col = get_sort_column_alias(sort[0])
dir = sort[1]
nullpos = (dir == 'asc') ? 'FIRST' : 'LAST'
"#{col} #{dir} NULL #{nullpos}"
end
scope.order(Arel.sql(str))
end
def get_sort_column_alias(col)
case col
when 'target' then 'target::integer[]'
...
else col
end
end

Rails Enum return integer value, not string representation

I have a table which contains an integer based column (status) which I'm using for an enum attribute within the Rails model.
At the moment of doing:
Post.select(:id, ..., :status)
Defined as:
enum status: { inactive: 0, active: 1, ... }
It returns everything as expected, but the status column is returned in its string value as inactive, active, etc. But I need it as an integer.
How can I get that?
I'm currently just using ActiveRecord::Base.connection.execute and passing a raw query:
ActiveRecord::Base.connection.execute('select id, ..., status from posts')
It won't be the most elegant way but this is the only way I see possible without looping over your objects.
You can achieve this by creating 2 classes like this:
class Post < ApplicationRecord
end
And the other one:
class PostSave < Post
enum status: { inactive: 0, active: 1, ... }
end
With this when you use Post class you won't get enum value for the "status" column but when you use PostSave class you can use your enum as you were already using.
Now when you do
Post.select(:id, :status)
It will give you integer values for the "status" column as you desire.
Yep. And that's simply how enum field works in Rails. Data is stored as integer, but displayed as string - based on what's defined in enum:
https://api.rubyonrails.org/v5.2.3/classes/ActiveRecord/Enum.html
If you really want to get integer try one of those:
Remove enum declaration. Data is stored as Integer. Without that line query will return Integer
Thats not the most beautiful code but: Post.statuses[post.status] will work
Have you tried this?
# Rails < 5
post = Post.find(123)
post.read_attribute(:status)
# Rails >= 5
post = Post.find(123)
post.read_attribute_before_type_cast(:status)
You can give this a try:
Post.select(:id, ..., 'status as int_status')
I'm not sure this directly applies as I do not know Rails or your exact requirement. But you indicated the desire to "get a collection of records from the DB"; so perhaps it will.
Postgres contains 2 tables in pg_catalog you need to join to get the collection values for a enum: pg_type and pg_enum as follows (where ENUM_NAME is replaced by the appropriate enum type name:
select t.typname,e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 'ENUM_NAME'
order by e.enumsortorder;
One difference I've notice is that in Postgres the associated numeric value is defined as Real, not as Integer. This is due to how the values are actually maintained. Example:
create type t_ord_stat as enum ('Ordered', 'Delivered', 'Billed', 'Complete', 'Canceled');
select e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 't_ord_stat'
order by e.enumsortorder;
alter type t_ord_stat add value 'Suppended: Out-of-Stock' after 'Ordered';
alter type t_ord_stat add value 'Suppended: Credit Check' before 'Suppended: Out-of-Stock' ;
alter type t_ord_stat add value 'Suppended: Customer Refused Delivery' before 'Billed';
select e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 't_ord_stat'
order by e.enumsortorder;
The above gives the actual numeric values underlying the enum string value (as column name indicates it's for sorting purpose). You could get a relative integer value with
select row_number() over() relative_seq, en.enumlabel
from (select e.enumsortorder, e.enumlabel
from pg_type t
join pg_enum e
on (t.oid = e.enumtypid)
where t.typname = 't_ord_stat'
order by e.enumsortorder) en;
I'll leave the actual conversion to Rails to you. Hope it helps.
You could map the column in the SQL statement to another field and read it from there instead, that way Rails passes the value directly as it doesn't know it's an enum (it doesn't know the column at all):
Post.select('id, title, status, status AS status_int').each do |post|
puts post.status # 'inactive'
puts post.status_int # 0
end
If you HAVE to have the same column name, then you're out of luck unless you do a little bit more work:
class Post
STATUSES = %w(inactive active)
end
Post.all.each do |post|
index = Post::STATUSES.index(post.status)
# use 'index' here instead of `post.status`
end
If neither of those are sufficient then this most certainly sounds like the http://xyproblem.info/ ... as these answers should work for 99% of cases. So you should probably explain WHY you cant just use status_int while working with the objects, or why you can't store the int in a variable, or... why you need access to the integer at all, which defeats the purpose of an enum.
The above solutions are working. I have tried with raw sql on mysql version 5.7.28.
the below query is working with mysql only.
I am still looking query for postgres, I will let you know soon.
create table Posts(id integer, title varchar(100), status ENUM ('active', 'inactive') NOT NULL);
insert into Posts(id, title, status) values(1, "Hello", 'inactive')
select title, status+0 from Posts;

why does Rails think 6,500 is greater than 10,000?

I seeded a database with four different values in the "result" column. They're supposed to represent dollar values but that's irrelevant.
10,000; 6,500; 1,000; and 0
In the model, I created this class method
def self.result
order("result DESC")
end
In the controller, I called it
#decisions = Decision.result
In the index, it's listing them in the following order
6,500;
10,000;
1,000;
0
When I switch DESC to ASC....
def self.result
order("result ASC")
end
it reverses the order
0
1,000;
10,000;
6,500;
You're storing the numbers as strings. Change the column type to DECIMAL.
Your column type is string, not number, they are ordered by string order.
What is the data type of that column? If it's a string then a string starting with 6 is "bigger" than one starting with 1

Rails returns a nil, when searching with find_by

I'm beginning to learn RoR, but i've a problem which i don't understand. With Product.find :all returns all the records from DB. But if i want to find_by_gender(1) (or even 2) it returns a nil, i'm certain that the db contains products with a gender
My code controller:
gender = params[:gender].to_i
#search_results = Product.find_by_gender(gender)
this returns a nill,
What am i doing wrong?
Greetings!
find_by_... returns either first record or nil if none found, find_all_by_... returns all records that match (or empty array if none). In your case nil means no records found with gender = 1.
Verify your data first!
Look at some sample records:
Do something like:
Product.all(:limit => 5).each {|product| product.id.to_s + product.gender}
or go into sql
sql> select id, gender from products where id < 6;
If you are to verify what the gender values are you can then create named scopes in your model for those conditions, e.g. (rails3)
(Product Model - app/models/product.rb)
scope :male where(:gender) = male_value # i.e. 1 or 'M' or 'Male' or whatever
scope :female where(:gender) = female_value # i.e. '2' or 'F' or whatever
Which will then you let write Products.male or Products.female !
Final note - should gender be in your users table? , or is this for male / female specific products?
in rails console execute
Product.pluck(:gender)
And u will know that values does it have in AR(i think true and false), so u have to use query Product.find_by_gender(true)

Rails, how to sanitize SQL in find_by_sql

Is there a way to sanitize sql in rails method find_by_sql?
I've tried this solution:
Ruby on Rails: How to sanitize a string for SQL when not using find?
But it fails at
Model.execute_sql("Update users set active = 0 where id = 2")
It throws an error, but sql code is executed and the user with ID 2 now has a disabled account.
Simple find_by_sql also does not work:
Model.find_by_sql("UPDATE user set active = 0 where id = 1")
# => code executed, user with id 1 have now ban
Edit:
Well my client requested to make that function (select by sql) in admin panel to make some complex query(joins, special conditions etc). So I really want to find_by_sql that.
Second Edit:
I want to achieve that 'evil' SQL code won't be executed.
In admin panel you can type query -> Update users set admin = true where id = 232 and I want to block any UPDATE / DROP / ALTER SQL command.
Just want to know, that here you can ONLY execute SELECT.
After some attempts I conclude sanitize_sql_array unfortunatelly don't do that.
Is there a way to do that in Rails??
Sorry for the confusion..
Try this:
connect = ActiveRecord::Base.connection();
connect.execute(ActiveRecord::Base.send(:sanitize_sql_array, "your string"))
You can save it in variable and use for your purposes.
I made a little snippet for this that you can put in initializers.
class ActiveRecord::Base
def self.escape_sql(array)
self.send(:sanitize_sql_array, array)
end
end
Right now you can escape your query with this:
query = User.escape_sql(["Update users set active = ? where id = ?", true, params[:id]])
And you can call the query any way you like:
users = User.find_by_sql(query)
Slightly more general-purpose:
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This one lets you call it just like you'd type in a where clause, without extra brackets, and using either array-style ? or hash-style interpolations.
User.find_by_sql(["SELECT * FROM users WHERE (name = ?)", params])
Source: http://blog.endpoint.com/2012/10/dont-sleep-on-rails-3-sql-injection.html
Though this example is for INSERT query, one can use similar approach for UPDATE queries. Raw SQL bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places << "(?,?,?,?)" # Append to array
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I prefer to do it with key parameters. In your case it may looks like this:
Model.find_by_sql(["UPDATE user set active = :active where id = :id", active: 0, id: 1])
Pay attention, that you pass ONLY ONE parameter to :find_by_sql method - its an array, which contains two elements: string query and hash with params (since its our favourite Ruby, you can omit the curly brackets).

Resources