Rails: query postgres jsonb using where error - ruby-on-rails

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

Related

StatementInvalid Rails Query

I've got the following query that works:
jobs = current_location.jobs.includes(:customer).all.where(complete: complete)
However, when I add a where clause to query the first name of the customer table, I get an error.
jobs = current_location.jobs.includes(:customer).all.where(complete: complete).where("customers.fist_name = ?", "Bob")
Here is the error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "customers"
LINE 1: ...bs"."complete" = $2 AND "jobs"."status" = $3 AND (customers....
^
: SELECT "jobs".* FROM "jobs" INNER JOIN "jobs_users" ON "jobs"."id" = "jobs_users"."job_id" WHERE "jobs_users"."user_id" = $1 AND "jobs"."complete" = $2 AND "jobs"."status" = $3 AND (customers.last_name = 'Bob') ORDER BY "jobs"."start" DESC LIMIT $4 OFFSET $5
The current_location method:
def current_location
return current_user.locations.find_by(id: cookies[:current_location])
end
Location Model
has_many :jobs
has_and_belongs_to_many :customers
Job Model
belongs_to :location
belongs_to :customer
Customer Model
has_many :jobs
has_and_belongs_to_many :locations
How can I fix this issue?
includes will only join the table if you set a reference to the association.
When using includes you ensure a reference to the association in 2 fashions:
You can use the references method this will join the table whether or not there are any query conditions (If you MUST use raw SQL as shown in your question then this is the method you would need to use) e.g.
current_location.jobs
.includes(:customer)
.references(:customer)
Or you can use the hash finder version of where: (Please note that when using an associative reference in the where clause you must reference the table name, in this case customers and not the association name customer)
current_location.jobs
.includes(:customer)
.where(customers: {first_name: "Bob" })
Both of these will eager load the customer for the jobs referenced.
The first option (references) will OUTER JOIN the customers table so that all the jobs are loaded even if they have no customers as long as no query conditions reference the customers table.
The second option (using where) will OUTER JOIN the customers table but given the query parameter against the customers table it will act very much like an INNER JOIN.
If you only need to search the jobs based on customer information then joins is a better choice as this will create an INNER JOIN with the customers table but will not try to load any of the customer data in the query e.g.
current_location.jobs.joins(:customer).where(customers: {first_name: "Bob" })
joins will always include the associated table regardless of a reference in the query.
Sidenote: the all in both your queries is completely unnecessary
includes(:customer) does not necessarily join the customers table into the SQL query. You need to use joins(:customer) to force Rails to join the customers table into the SQL query and make it available to query conditions.
jobs = current_location.jobs
.joins(:customer)
.includes(:customer)
.where(complete: complete)
.where(customers: { first_name: 'Bob' })

ActiveRecord select with OR and exclusive limit

I have the need to query the database and retrieve the last 10 objects that are either active or declined. We use the following:
User.where(status: [:active, :declined]).limit(10)
Now we need to get the last 10 of each status (total of 20 users)
I've tried the following:
User.where(status: :active).limit(10).or(User.where(status: : declined).limit(10))
# SELECT "users".* FROM "users" WHERE ("users"."status" = $1 OR "users"."status" = $2) LIMIT $3
This does the same as the previous query and returns only 10 users, of mixed statuses.
How can I get the last 10 active users and the last 10 declined users with a single query?
I'm not sure that SQL allows doing what you want. First thing I would try would be to use a subquery, something like this:
class User < ApplicationRecord
scope :active, -> { where status: :active }
scope :declined, -> { where status: :declined }
scope :last_active_or_declined, -> {
where(id: active.limit(10).pluck(:id))
.or(where(id: declined.limit(10).pluck(:id))
}
end
Then somewhere else you could just do
User.last_active_or_declined()
What this does is to perform 2 different subqueries asking separately for each of the group of users and then getting the ones in the propper group ids. I would say you could even forget about the pluck(:id) parts since ActiveRecord is smart enough to add the proper select clause to your SQL, but I'm not 100% sure and I don't have any Rails project at hand where I can try this.
limit is not a permitted value for #or relationship. If you check the Rails code, the Error raised come from here:
def or!(other) # :nodoc:
incompatible_values = structurally_incompatible_values_for_or(other)
unless incompatible_values.empty?
raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
end
# more code
end
You can check which methods are restricted further down in the code here:
STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
def structurally_incompatible_values_for_or(other)
STRUCTURAL_OR_METHODS.reject do |method|
get_value(method) == other.get_value(method)
end
end
You can see in the Relation class here that limit is restricted:
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache,
:skip_preloading]
So you will have to resort to raw SQL I'm afraid
I don't think you can do it with a single query, but you can do it with two queries, get the record ids, and then build a query using those record ids.
It's not ideal but as you're just plucking ids the impact isn't too bad.
user_ids = User.where(status: :active).limit(10).pluck(:id) + User.where(status: :declined).limit(10).pluck(id)
users = User.where(id: user_ids)
I think you can use UNION. Install active_record_union and replace or with union:
User.where(status: :active).limit(10).union(User.where(status: :declined).limit(10))

Mysql2::Error: Unknown column in where clause Rails

I receive an error of
Mysql2::Error: Unknown column 'requests.access_level_id' in 'where clause':
SELECT `requests`.*
FROM `requests` LEFT OUTER JOIN `users` ON `users`.`id` = `requests`.`from_id`
WHERE `requests`.`access_level_id` = 1
ORDER BY id DESC
Model
class Request < ApplicationRecord
belongs_to :user, foreign_key: :from_id
end
Controller
#req = Request.left_outer_joins(:user).where(access_level_id: 1).order('id DESC')
How can I remove requests from the WHERE clause requests.access_level_id = 1? I just want access_level_id = 1 to be in the where statement.
As you requested, you can add where clause without requests as,
#req = Request.left_outer_joins(:user).where('access_level_id = ?', 1).order('id DESC')
But its good to keep relative aliasing for access_level_id. If its users then please use it like,
#req = Request.left_outer_joins(:user).where(users: { access_level_id: 1 }).order('id DESC')
Assuming that access_level_id is field for user, you can replace your query with following:
#req = Request.left_outer_joins(:user).where('users.access_level_id = ?', 1).order('id DESC')
By default the fields in where conditions are considered to be belonging to Request method in your query.
Hope this helps you.
Please let me know if you face any issue.

Rails - Active Record: Find all records which have a count on has_many association with certain attributes

A user has many identities.
class User < ActiveRecord::Base
has_many :identities
end
class Identity < ActiveRecord::Base
belongs_to :user
end
An identity has an a confirmed:boolean column. I'd like to query all users that have an only ONE identity. This identity must also be confirmed false.
I've tried this
User.joins(:identities).group("users.id").having( 'count(user_id) = 1').where(identities: { confirmed: false })
But this returns users with one identity confirmed:false but they could also have additional identities if they are confirmed true. I only want users with only one identity confirmed:false and no additional identities that are have confirmed attribute as true.
I've also tried this but obviously it's slow and I'm looking for the right SQL to just do this in one query.
def self.new_users
users = User.joins(:identities).where(identities: { confirmed: false })
users.select { |user| user.identities.count == 1 }
end
Apologies upfront if this was already answered but I could not find a similar post.
One solution is to use rails nested queries
User.joins(:identities).where(id: Identity.select(:user_id).unconfirmed).group("users.id").having( 'count(user_id) = 1')
And here's the SQL generated by the query
SELECT "users".* FROM "users"
INNER JOIN "identities" ON "identities"."user_id" = "users"."id"
WHERE "users"."id" IN (SELECT "identities"."user_id" FROM "identities" WHERE "identities"."confirmed" = 'f')
GROUP BY users.id HAVING count(user_id) = 1
I still don't think this is the most efficient way. While I'm able to generate only one SQL query (meaning only one network call to the db), I'm still have to do two scans: one scan on the USERS table and one scan on the IDENTITIES table. This can be optimized by indexing the identities.confirmed column but this still doesn't solve the two full scans problem.
For those who understand the query plan here it is:
QUERY PLAN
-------------------------------------------------------------------------------------------
HashAggregate (cost=32.96..33.09 rows=10 width=3149)
Filter: (count(identities.user_id) = 1)
-> Hash Semi Join (cost=21.59..32.91 rows=10 width=3149)
Hash Cond: (identities.user_id = identities_1.user_id)
-> Hash Join (cost=10.45..21.61 rows=20 width=3149)
Hash Cond: (identities.user_id = users.id)
-> Seq Scan on identities (cost=0.00..10.70 rows=70 width=4)
-> Hash (cost=10.20..10.20 rows=20 width=3145)
-> Seq Scan on users (cost=0.00..10.20 rows=20 width=3145)
-> Hash (cost=10.70..10.70 rows=35 width=4)
-> Seq Scan on identities identities_1 (cost=0.00..10.70 rows=35 width=4)
Filter: (NOT confirmed)
(12 rows)
def self.new_users
joins(:identities).group("identities.user_id").having("count(identities.user_id) = 1").where(identities: {confirmed: false}).uniq
end
I think group_concat may be the answer here, if you have the function in your DBMS. (if not there may be an equivalent). This will collect all the values for the field from the group into a comma-separated string. We want ones where this string is equal to "false": ie, there's just one, and it's false (which i think is your requirement, it's a little unclear). . I think this should work if we let Rails handle the translation of false into however the DB stores it.
User.joins(:identities).group("identities.user_id").having("group_concat(identities.confirmed) = ?", false)
EDIT - if your database stores false as 0 then the above will generate sql like having group_concat(identities.confirmed) = 0. Because the result of the group_concat is a string, then it may (in some DBMS's) do a string-to-integer cast on the results before comparing it to 0, which will return lots of false positives if all the other strings cast to 0. In that case you can try this:
User.joins(:identities).group("identities.user_id").having("group_concat(identities.confirmed) = '?'", false)
(note quotes around ?)
EDIT2 - postgres version.
I've not tried this but it looks like recent versions of postgres have a function array_agg() which does the same as mysql's group_concat(). Because postgres stores true/false as 't'/'f' we shouldn't need to wrap the ? in quotes. Try this:
User.joins(:identities).group("identities.user_id").having("array_agg(identities.confirmed) = ?", false)

What is wrong with my scope below

I am writing an active record scope in rails. I have the following sql:
SELECT user.roll_number, first_name, last_name FROM user
INNER JOIN classes ON user.roll_number = classes.roll_number
AND classes.id IN #{id} ORDER BY(roll_number)
I wrote the scope in the user class, basically the tables are in the db2 database, so i wrote the scope as shown below
scope :by_id, lambda { |id|
{ select("user.roll_number, first_name, last_name")
.joins("INNER JOIN classes ON (user.roll_number = classes.roll_number \
and classes.id = #{id})")
}
}
Is there something wrong with the scope? I am getting "unexpected keyword end error"
Like said in the comment, you need to remove those {} braces. I'm guessing the error you are seeing is because this:
{ select("...") }
Is interpreted as an instantiation of a Hash, with a key of select("...") and no value. Ruby isn't happy with you.
Your follow-up:
What if i pass multiple ids? i am getting empty list, even though there are records for id 1 and id 2. i am passing id in the array form as id = [1,2]
You made a SQL lambda, so it's all on you to make that SQL work. A list of [1,2] is going to look like:
INNER JOIN classes ON (user.roll_number = classes.roll_number
and classes.id = [1,2])
Will that work on your SQL DB? Wouldn't you want to use and classes.id IN (1,2)?

Resources