How to search array through ransack gem? - ruby-on-rails

I'm using ransack gem for searching in rails application. I need to search an array of email_ids in User table.
Referring to this issue at ransacking, I followed the steps and added this to the initializers folder ransack.rb
Ransack.configure do |config|
{
contained_within_array: :contained_within,
contained_within_or_equals_array: :contained_within_or_equals,
contains_array: :contains,
contains_or_equals_array: :contains_or_equals,
overlap_array: :overlap
}.each do |rp, ap|
config.add_predicate rp, arel_predicate: ap, wants_array: true
end
end
In the rails console, if i do like this:
a = User.search(email_contains_array: ['priti#gmail.com'])
it produces the sql like this:
"SELECT \"users\".* FROM \"users\" WHERE \"users\".\"deleted_at\" IS NULL AND (\"users\".\"email\" >> '---\n- priti#gmail.com\n')"
and gives error like this:
User Load (1.8ms) SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL AND ("users"."email" >> '---
- priti#gmail.com
')
ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: operator does not exist: character varying >> unknown
LINE 1: ...RE "users"."deleted_at" IS NULL AND ("users"."email" >> '---
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL AND ("users"."email" >> '---
- priti#gmail.com
')
Expected is this query:
SELECT "users".* FROM "users" WHERE ("users"."roles" #> '{"3","4"}')
What is wrong am I doing?

I met the same problem as you do. I'm using Rails 5, and I need to search an array of roles in User table
It seems that you have already add postgres_ext gem in your gemfile, but it has some problems if you are using it in Rails 5 application.
So it is a choice for you to add a contain query in Arel Node by yourself instead of using postgres_ext gem
And if you are using other version of Rails, I think it works well too.
I have an User model, and an array attribute roles. What I want to do is to use ransack to search roles. It is the same condition like yours.
ransack can't search array.
But PostgresSQL can search array like this:
User.where("roles #> ?", '{admin}').to_sql)
it produce the sql query like this:
SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL AND (roles #> '{admin}')
So what I want to do is to add a similar contains query in Arel Nodes
You can do it this way:
# app/config/initializers/arel.rb
require 'arel/nodes/binary'
require 'arel/predications'
require 'arel/visitors/postgresql'
module Arel
class Nodes::ContainsArray < Arel::Nodes::Binary
def operator
:"#>"
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_ContainsArray(o, collector)
infix_value o, collector, ' #> '
end
end
module Predications
def contains(other)
Nodes::ContainsArray.new self, Nodes.build_quoted(other, self)
end
end
end
Because you can custom ransack predicate,
so add contains Ransack predicate like this:
# app/config/initializers/ransack.rb
Ransack.configure do |config|
config.add_predicate 'contains',
arel_predicate: 'contains',
formatter: proc { |v| "{#{v}}" },
validator: proc { |v| v.present? },
type: :string
end
Done!
Now, you can search array:
User.ransack(roles_contains: 'admin')
The SQL query will be like this:
SELECT \"users\".* FROM \"users\" WHERE \"users\".\"deleted_at\" IS NULL AND (\"users\".\"roles\" #> '{[\"admin\"]}')
Yeah!

I achieved this using ransackable scopes.
Where Author has a column "affiliation" consisting of an array of strings.
scope :affiliation_includes, ->(str) {where("array_to_string(affiliation,',') ILIKE ?", "%#{str}%")}
def self.ransackable_scopes(auth_object=nil)
[:affiliation_includes]
end
then the following works
Author.ransack(affiliation_includes:'dresden')
The advantage of this over use of postgres '#>' is that it can match substrings within the array, because of the %'s in the where command (get rid of %'s for whole string match). It is also case-insensitive (use LIKE instead of ILIKE for case-sensitive.

Related

Rails: query postgres jsonb using where error

I have a jsonb column in my postgres performances table called authorization where I store the uuid of a user as a key and their authorization level as the value e.g.
{ 'sf4wfw4fw4fwf4f': 'owner', 'ujdtud5vd9': 'editor' }
I use the below Rails query in my Performance model to search for all records where the user is an owner:
class Performance < ApplicationRecord
def self.performing_or_owned_by(account)
left_outer_joins(:artists)
.where(artists: { id: account } )
.or(Performance.left_outer_joins(:artists)
# this is where the error happens
.where("authorization #> ?", { account => "owner" }.to_json)
).order('lower(duration) DESC')
.uniq
end
end
Where account is the account uuid of the user. However, when I run the query I get the following error:
ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: syntax error at or near "#>")
LINE 1: ..._id" WHERE ("artists"."id" = $1 OR (authorization #> '{"28b5...
The generated SQL is:
SELECT "performances".* FROM "performances"
LEFT OUTER JOIN "artist_performances" ON "artist_performances"."performance_id" = "performances"."id"
LEFT OUTER JOIN "artists" ON "artists"."id" = "artist_performances"."artist_id" WHERE ("artists"."id" = $1 OR (authorization #> '{"28b5fc7f-3a31-473e-93d4-b36f3b913269":"owner"}'))
ORDER BY lower(duration) DESC
I tried several things but keep getting the same error. Where am I going wrong?
The solution as per comment in the original question is to wrap the authorization in double-quotes. Eg:
.where('"authorization" #> ?', { account => "owner" }.to_json)
The ->> operator gets a JSON object field as text.
So it looks you need this query:
left_outer_joins(:artists).
where("artists.id = ? OR authorization ->> ? = 'owner'", account, account).
order('lower(duration) DESC').
uniq

ActiveRecord pluck to SQL

I know these two statements perform the same SQL:
Using select
User.select(:email)
# SELECT `users`.`email` FROM `users`
And using pluck
User.all.pluck(:email)
# SELECT `users`.`email` FROM `users`
Now I need to get the SQL statement derived from each method. Given that the select method returns an ActiveRecord::Relation, I can call the to_sql method. However, I cannot figure out how to get the SQL statement derived from a pluck operation on an ActiveRecord::Relation object, given that the result is an array.
Please, take into account that this is a simplification of the problem. The number of attributes plucked can be arbitrarily high.
Any help would be appreciated.
You cannot chain to_sql with pluck as it doesn't return ActiveRecord::relation. If you try to do, it throws an exception like so
NoMethodError: undefined method `to_sql' for [[""]]:Array
I cannot figure out how to get the SQL statement derived from a pluck
operation on an ActiveRecord::Relation object, given that the result
is an array.
Well, as #cschroed pointed out in the comments, they both(select and pluck) perform same SQL queries. The only difference is that pluck return an array instead of ActiveRecord::Relation. It doesn't matter how many attributes you are trying to pluck, the SQL statement will be same as select
Example:
User.select(:first_name,:email)
#=> SELECT "users"."first_name", "users"."email" FROM "users"
Same for pluck too
User.all.pluck(:first_name,:email)
#=> SELECT "users"."first_name", "users"."email" FROM "users"
So, you just need to take the SQL statement returned by the select and believe that it is the same for the pluck. That's it!
You could monkey-patch the ActiveRecord::LogSubscriber class and provide a singleton that would register any active record queries, even the ones that doesn't return ActiveRecord::Relation objects:
class QueriesRegister
include Singleton
def queries
#queries ||= []
end
def flush
#queries = []
end
end
module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
def sql(event)
QueriesRegister.instance.queries << event.payload[:sql]
"#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
end
end
end
Run you query:
User.all.pluck(:email)
Then, to retrieve the queries:
QueriesRegister.instance.queries

How to query comma separated values in Postgres string field? ROR / ActiveRecord

I'm using Ruby on Rails 4.2.3 and I have two string fields in users table two_factor_secret_key and two_factor_recovery_codes. The first one has a string token while the second one has ten tokens separated by ;. They look something like:
[62] pry(main)> ap User.last
User Load (6.0ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT 1
#<User:0x007f8b3b009ad8> {
:id => 1,
:email => "test#me.com",
......
......
:two_factor_secret_key => "jvyzizqib47gp4au",
:two_factor_recovery_codes => "jgbcfjzh3lhpm6dv;er34rihizdlzu4fc;u32wwdmxl2dnxslv;liqcswrhjyfrpbkk;ttuezeszxnwzjent;4noxzmo4rg5jed5w;eynndg2gy5yzrdi3;h275valgjiiee7r3;f6m5xwujmtnucshe;tvhzxibvj4ikulyg"
}
How can I query users table in a way I search two_factor_secret_key and two_factor_recovery_codes.
In this example if the query was based on jvyzizqib47gp4au I would like to return the user and the same if I use er34rihizdlzu4fc I should get true as it's included in two_factor_recovery_codes?
It's easy to find the record using two_factor_secret_key with something like: User.where(two_factor_secret_key: "jvyzizqib47gp4au"), however I'm not sure how should I query two_factor_recovery_codes with ; separated values.
Please note that I don't wanna use serialization with activerecords as ROR app is not the one who is responsible of generating the data in my case.
Any help?
You can use:
user = User.where("two_factor_secret_key LIKE :search OR two_factor_recovery_codes LIKE :search", search: "%jvyzizqib47gp4au%")

Random ActiveRecord with where and excluding certain records

I would like to write a class function for my model that returns one random record that meets my condition and excludes some records. The idea is that I will make a "random articles section."
I would like my function to look like this
Article.randomArticle([1, 5, 10]) # array of article ids to exclude
Some pseudo code:
ids_to_exclude = [1,2,3]
loop do
returned_article = Article.where(published: true).sample
break unless ids_to_exclude.include?(returned_article.id)
do
Lets look at DB specific option.
class Article
# ...
def self.random(limit: 10)
scope = Article.where(published: true)
# postgres, sqlite
scope.limit(limit).order('RANDOM()')
# mysql
scope.limit(limit).order('RAND()')
end
end
Article.random asks the database to get 10 random records for us.
So lets look at how we would add an option to exclude some records:
class Article
# ...
def self.random(limit: 10, except: nil)
scope = Article.where(published: true)
if except
scope = scope.where.not(id: except)
end
scope.limit(limit).order('RANDOM()')
end
end
Now Article.random(except: [1,2,3]) would get 10 records where the id is not [1,2,3].
This is because .where in rails returns a scope which is chain-able. For example:
> User.where(email: 'test#example.com').where.not(id: 1)
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 AND ("users"."id" != $2) [["email", "test#example.com"], ["id", 1]]
=> #<ActiveRecord::Relation []>
We could even pass a scope here:
# cause everyone hates Bob
Article.random( except: Article.where(author: 'Bob') )
See Rails Quick Tips - Random Records for why a DB specific solution is a good choice here.
You can use some like this:
ids_to_exclude = [1,2,3,4]
Article.where("published = ? AND id NOT IN (?)", true , ids_to_exclude ).order( "RANDOM()" ).first

Why does rails add "order_by id" to all queries? Which ends up breaking Postgres

The following rails code:
class User < MyModel
def top?
data = self.answers.select("sum(up_votes) total_up_votes").first
return (data.total_up_votes.present? && data.total_up_votes >= 10)
end
end
Generates the following query (note the order_by added by Rails):
SELECT
sum(up_votes) total_up_votes
FROM
"answers"
WHERE
"answers"."user_id" = 100
ORDER BY
"answers"."id" ASC
This throws an error in Postgres:
PG::GroupingError: ERROR: column "answers.id" must appear in the GROUP BY clause or be used in an aggregate function
Is rails' database abstraction only made with MySQL in mind?
No, the 'order by id' is added to ensure .first always returns the same result. Without an ORDER BY clause, the result is not guaranteed to be the same under the SQL spec.
For your case, you should use .sum() instead of .select() to do this more simply:
def top?
self.answers.sum(:up_votes) >= 10
end
You used #first method at the end. That's the reason.
self.answers.select("sum(up_votes) total_up_votes").first # <~~~
Model.first finds the first record ordered by the primary key
Look at the clause
ORDER BY
"answers"."id" ASC # this is the primary key of your table ansers.
Check the documentation of 1.1.3 first or #first .

Resources