Sphinx indexer custom query for sql_attr_multi - ruby-on-rails

I managed to have a query for getting all the friends and friends of friends of a particular user. I have a users table and a friendships join table. For simplicity, lets just say that the users table only has a primary key. The friendships table has the user_id and friend_id columns. The query is as follows:
"SELECT DISTINCT friends.user_id, friends.friend_id, users.*
FROM FRIENDSHIPS friends, USERS
WHERE USERS.id = friends.friend_id AND friends.user_id = #{u.id}
UNION
SELECT DISTINCT fof.user_id, fof.friend_id, users.*
FROM FRIENDSHIPS friends, FRIENDSHIPS fof, USERS
WHERE USERS.id = fof.friend_id AND friends.friend_id = fof.user_id AND friends.user_id = #{u.id}"
u.id is the id of the user whom I wish to get all the friends and friends of friends.
This query works very well however I would like to convert this to a query usable by sphinx's sql_attr_multi. I tried directly placing it like so:
sql_attr_multi = uint fwfof from query; \
SELECT DISTINCT `friends`.`user_id`, `friends`.`friend_id`, `users`.`id` \
FROM `friendships` friends, `users` \
WHERE `users`.`id` = `friends`.`friend_id` AND `friends`.`user_id` = $id \
UNION \
SELECT DISTINCT `fof`.`user_id`, `fof`.`friend_id`, `users`.`id` \
FROM `friendships` `friends`, `friendships` `fof`, `users` \
WHERE `users`.`id` = `fof`.`friend_id` AND `friends`.`friend_id` = `fof`.`user_id` AND `friends`.`user_id` = $id
but that obviously failed.
How do I make this query usable by sphinx's indexer?
Thanks in advance for anything that will point me in the right direction.

I don't know enough about your data structure to say that this will 100% work, but I see one obvious problem:
WHEREusers.id=fof.friend_idANDfriends.friend_id=fof.user_idANDfriends.user_id= $id
The last part, WHERE user_id = $id is wrong. That syntax works when adding a sql_query_info attribute to a sphinx index, but not to sql_attr_multi.
Your query for the sql_attr_multi should select the id as the first column that matches the document id in your main sql_query.
The second column of the sql_attr_multi is the col you want to group together in the attr.

Related

How to write sub query in active record?

I have two tables users and posts and they have association of has_many. I want to fetch details of both users and posts in a single query. I'm able to manage the sql query but I don't want to use the raw query in the code (using execute method) as i think it is kind of simple thing and can be written using active record.
Here is the sql query
SELECT a.id, a.name, a.timestamp, b.id, b.user_id, b.title
FROM users a
INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = a.id
where id IN ( 1, 2, 3);
I think includes does not help here because i'm dealing with large data.
Can any one help me ?
If you just want those specific columns and nothing else then this will work
User.joins(:post)
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
posts.id as post_id, posts.user_id as post_user_id,
posts.title as post_title")
This will return an ActiveRecord::Relation of User objects with virtual attributes for post_id, post_user_id (Not sure why you need this one since you already selected users.id), and post_title.
The query produced will be
SELECT users.id,
users.name,
users.timestamp,
posts.id as post_id,
posts.user_id as post_user_id,
posts.title as post_title
FROM users
INNER JOIN posts on posts.user_id = users.id
where users.id IN ( 1, 2, 3);
Please note you may have multiple User objects, one for each Post, just as the SQL query does.
You can execute your exact query using the string version of joins e.g.
User.joins("INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = users.id")
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
b.id as post_id, b.user_id as post_user_id,
b.title as post_title")
Additionally to avoid some of the overhead you can use arel instead e.g.
users_table = User.arel_table
posts_table = Post.arel_table
query = users_table.project(Arel.star)
.join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql)
This will return an ActiveRecord::Result with 2 useful methods columns (the columns selected) and rows. You can convert this to a Hash(#to_hash) but note that any columns with duplicate names (id for instance) will overwrite one another.
You could fix this by specifying the colums you want selected in the project portion. e.g. your current query would be:
query = users_table.project(
users_table[:id],
users_table[:name],
users_table[:timestamp],
posts_table[:id].as('post_id'),
posts_table[:user_id].as('post_user_id'),
posts_table[:title].as('post_title')
).join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql).to_hash
Since none of the names collide now it can be structured into a nice Hash where the keys are the column names and the values or the row value for that record.
users = User.joins(:posts).includes(:posts).where(id: [1, 2, 3])
Will give you all the users with theirs posts.
then you can do whatever you want with them, but to access posts data for first retrieved user
first_user_posts = users.first.posts # this will not make additional DB queries as you used includes and data is already added
We use joins to have INNER JOIN statement in the SQL
We use includes to load all posts in the memory
I have two tables users and posts and they have association of
has_many. I want to fetch details of both users and posts in a single
query.
can be done with includes like
users = User.includes(:posts).where({posts: {user_id: [1,2,3]}})
other is eager_load and preload you can use as per your requirements, for more https://blog.arkency.com/2013/12/rails4-preloading/

PHP/MYSQL: Obtain two items from table in join using different criteria

I am doing a join between tables to get a user pic from a user table about someone who comments on an article. However, I need to get a separate picture from the user table for a different user, the person who wrote the article at the same time. (There is a strong reason to do this in one query, as it's actually only one part of a more complex request).
Can anyone suggest how I could get two user pics from the users table during the join? This code gives me the first user pic, the commenter's pic or upic. But how do I also get the second user pic?
commments
id|comment|articleid|userid
1|great article|1|2
article
id|article|userid
1|Restaurant Openings|1
users
id|pic
1|mary.png
2|joe.png
$sql = "SELECT c.comment,c.articleid,c.userid,u.id, u.pic as upic,a.id,
FROM `comments` c
LEFT JOIN `articles` a
ON c.articleid = a.id
LEFT JOIN `users` u
ON c.userid= u.id
WHERE c.id=1";
A separate query to get the author's pic for would be:
$sqlauthorpic = "SELECT u.pic,u.id from `users` u
WHERE u.id = 1";
Thanks for any suggestions.
Join users again this time with article table, since you are looking for a user that wrote the article:
SELECT c.comment,c.articleid,c.userid,u.id, u.pic as upic,a.id, w.pic as wpic
FROM `comments` c
LEFT JOIN `articles` a ON c.articleid = a.id
LEFT JOIN `users` u ON c.userid= u.id
JOIN `users` w ON w.id = a.userid
You can also get rid of LEFT joins because, i think there could be no comments with out an article (articleid) and no comment without userid.

Rails Postgres query to exclude any results that contain one of three records on join

This is a hard problem to describe but I have Rails query where I join another table and I want to exclude any results where the join table contain one of three conditions.
I have a Device model that relates to a CarUserRole model/record. In that CarUserRole record it will contain one of three :role - "owner", "monitor", "driver". I want to return any results where there is no related CarUserRole record where role: "owner". How would I do that?
This was my first attempt -
Device.joins(:car_user_roles).where('car_user_roles.role = ? OR car_user_roles.role = ? AND car_user_roles.role != ?', 'monitor', 'driver', 'owner')
Here is the sql -
"SELECT \"cars\".* FROM \"cars\" INNER JOIN \"car_user_roles\" ON \"car_user_roles\".\"car_id\" = \"cars\".\"id\" WHERE (car_user_roles.role = 'monitor' OR car_user_roles.role = 'driver' AND car_user_roles.role != 'owner')"
Update
I should mention that a device sometimes has multiple CarUserRole records. A device can have an "owner" and a "driver" CarUserRole. I should also note that they can only have one owner.
Anwser
I ended up going with #Reub's solution via our chat -
where(CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not)
Since the car_user_roles table can have multiple records with the same car_id, an inner join can result in the join table having multiple rows for each row in the cars table. So, for a car that has 3 records in the car_user_roles table (monitor, owner and driver), there will be 3 records in the join table (each record having a different role). Your query will filter out the row where the role is owner, but it will match the other two, resulting in that car being returned as a result of your query even though it has a record with role as 'owner'.
Lets first try to form an sql query for the result that you want. We can then convert this into a Rails query.
SELECT * FROM cars WHERE NOT EXISTS (SELECT id FROM car_user_roles WHERE role='owner' AND car_id = cars.id);
The above is sufficient if you want devices which do not have any car_user_role with role as 'owner'. But this can also give you devices which have no corresponding record in car_user_roles. If you want to ensure that the device has at least one record in car_user_roles, you can add the following to the above query.
AND EXISTS (SELECT id FROM car_user_roles WHERE role IN ('monitor', 'driver') AND car_id = cars.id);
Now, we need to convert this into a Rails query.
Device.where(
CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not
).where(
CarUserRole.where("car_user_roles.car_id = cars.id").where(role: ['monitor', 'driver']).exists
).all
You could also try the following if your Rails version supports exists?:
Device.joins(:car_user_roles).exists?(role: ['monitor', 'driver']).exists?(role: 'owner').not.select('cars.*').distinct
Select the distinct cars
SELECT DISTINCT (cars.*) FROM cars
Use a LEFT JOIN to pull in the car_user_roles
LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id
Select only the cars that DO NOT contain an 'owner' car_user_role
WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner')
Select only the cars that DO contain either a 'driver' or 'monitor' car_user_role
AND (car_user_roles.role IN ('driver','monitor'))
Put it all together:
SELECT DISTINCT (cars.*) FROM cars LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner') AND (car_user_roles.role IN ('driver','monitor'));
Edit:
Execute the query directly from Rails and return only the found object IDs
ActiveRecord::Base.connection.execute(sql).collect { |x| x['id'] }

what's the difference between by passing a symbol and string in where clause with joins

I have two models User and Book. User have username and email field, Book have author and title field. When I am using joins to get the users based on associated data with conditions passed in where clause, I am getting different results
When I run this
User.joins(:books).where('author = ? ','xxxx')
Mysql query generated is :
"SELECT `users`.* FROM `users` INNER JOIN `books` ON `books`.`user_id` = `users`.`id` WHERE (author = 'xxxxx' )"
gives me the user whose book include xxxx author whereas
When I run this
User.joins(:books).where(author: 'xxxx')
Mysql query generated is:
"SELECT `users`.* FROM `users` INNER JOIN `books` ON `books`.`user_id` = `users`.`id` WHERE `users`.`author` = 'xxxx'">
gives Mysql2::Error: Unknown column 'users.author' in 'where clause':
SELECT `users`.* FROM `users` INNER JOIN `books` ON `books`.`user_id` = `users`.`id` WHERE `users`.`author` = 'xxxx'
ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'users.author' in 'where clause': SELECT users.* FROM users INNER JOIN book ON book.user_id = users.id WHERE users.author = 'Dan Brown'
My question : is when I am passing a string encapsulated field and value in where clause it gives me result but when I pass a symbol in where clause it gives me mysql unknown column error. So how does ruby interpreter know from where to fetch data in symbol passing and string passing
When we join the tables especially in rails the rails has a power to take the aliasing of the joined tables.
Your query,
User.joins(:books).where(author: 'xxxx') searches for the author field in the user table
User.joins(:books).where(author: 'xxxx') always takes the field of first table and
User.joins(:books).where('author = ? ','xxxx') searches in both the tables
For example, User.joins(:books).where('id = ? ','xxxx'), try this query.
In the above You will get an error because id field is present in both the tables and it gets confused
but,
User.joins(:books).where(id: 'xxxx') works because it searches only in the users table.
so, you can use, refer author through books books: {author: 'xxxx'}
User.joins(:books).where(books: {author: 'xxxx'}) // your required query
or
User.joins(:books).where('author = ? ','xxxx')
User.joins(:books).where('author = ? ','xxxx') is free from sql injection but User.joins(:books).where(author: 'xxxx') is not .
It is good to use User.joins(:books).where('author = ? ','xxxx') as it is easier to write more complex where clause.
When you are writing User.joins(:books).where(author: 'xxxx') rails interpreter will try to interpret as users.author = 'xxxx'
You can check the link for more info.

Combine a join query with a normal condition

Users have a main category and multiple sub-categories
I would like to get all users who belong to a category, regardless if it is their main or sub.
#users = User.joins(:sub_categories).where('sub_category_id = ? OR type = ?', #sub_category.id, "User::#{category}User").page(params[:page])
A user's main category is also their STI type.
The query works, however I am getting duplicate results when trying to include the user's main type.
The query that is generated is:
User Load (0.0ms) SELECT "users".* FROM "users" INNER JOIN "user_sub_categories_users" ON "user_sub_categories_users"."user_id" = "users"."id" INNER JOIN "user_sub_categories" ON "user_sub_categories"."id" = "user_sub_categories_users"."sub_category_id" WHERE "users"."deleted_at" IS NULL AND (sub_category_id = 1 OR type = 'User::ModelUser') ORDER BY "users"."created_at" DESC LIMIT 20 OFFSET 0
EDIT: A user can not belong to a sub-category if its their main, so its safe to simply join the two conditions together
Because there is more than one group for each user, your join is creating multiple rows for each user, e.g.:
User Group
-------|------
UserA |GroupA
UserA |GroupB
UserB |GroupA
UserC |GroupA
UserC |GroupB
Three users, but five rows!
You can safely add a .uniq on the end of your query if you're just interested in the distinct users. In the context of an ActiveRecord query, .uniq will be converted to SQL's DISTINCT().

Resources