Arel wth lateral table in the from clause, - ruby-on-rails-5

I have 3 table users, roles and roles_users where in the last table is the intermediate table. So, user can have multiple roles and any role can belong to multiple users.
The problem statement is that we need to render the list of users and their roles matching the search criteria.
select users.id, dynamic_roles.name
from users, lateral (
select GROUP_CONCAT( DISTINCT( roles.name ) ) as name
from roles, roles_users
where (
( roles_users.user_id = users.id AND roles_users.role_id = roles.id )
)
) dynamic_roles
where dynamic_roles.name LIKE '%admin%' AND dynamic_roles.name LIKE '%manager%';
What I tried is as below:
rs = Role.joins(:users).select("GROUP_CONCAT( DISTINCT( #{Role.table_name}.name ) ) as name")
users = User.arel_table #predefined reference received as argument to a method that is supposed to compose the arel query.
users = users.project(users['id']).distinct
users.to_sql
=> "SELECT DISTINCT users.id FROM users"
users.from('dynamic_roles').to_sql
=> "SELECT DISTINCT users.id FROM dynamic_roles"
users.lateral('dynamic_roles').to_sql
=> TypeError: Cannot visit Arel::Nodes::Lateral
from /Users/prasadsurase/.rvm/gems/ruby-2.7.1/gems/activerecord-6.0.3.6/lib/arel/visitors/visitor.rb:39:in `rescue in visit'
Caused by NoMethodError: undefined method `visit_Arel_Nodes_Lateral' for #<Arel::Visitors::MySQL:0x00007f97424477b8>
Referring https://apidock.com/rails/v6.0.0/Arel/SelectManager/from and https://apidock.com/rails/v6.0.0/Arel/SelectManager/lateral

Your issue is that the mysql visitors do not include a visitor for Lateral (the way postgres does).
We could go about adding our own visitor but it is easier to get around this using a NamedFunction instead since the LATERAL syntax is the same as a function.
As with most things complicated, this is not the prettiest solution but it will provide the desired result.
roles = Arel::Table.new('roles')
role_users = Arel::Table.new('role_users')
users = Arel::Table.new('users')
group = Arel::Nodes::NamedFunction.new('GROUP_CONCAT',
[Arel::Nodes::Grouping.new(roles[:name])],
'name')
group.distinct = true
sub = roles.project(group)
.from([roles, role_users])
.where( role_users[:user_id].eq(users[:id]).and(
role_users[:role_id].eq( roles[:id]))
)
dynamic_roles = Arel::Table.new('dynamic_roles')
lateral = Arel::Nodes::NamedFunction.new('LATERAL',[sub], dynamic_roles.name)
users.project(users[:id], dynamic_roles[:name])
.from([ users, lateral])
.where( dynamic_roles[:name].matches(Arel::Nodes::Quoted.new("%admin%")).and(
dynamic_roles[:name].matches(Arel::Nodes::Quoted.new("%manager%"))
))
This will Produce the following SQL:
SELECT users.id, dynamic_roles.name
FROM users, LATERAL(
(
SELECT GROUP_CONCAT( DISTINCT (roles.name)) AS name
FROM roles, role_users
WHERE role_users.user_id = users.id AND role_users.role_id = roles.id
)
) AS dynamic_roles
WHERE
dynamic_roles.name LIKE '%admin%' AND dynamic_roles.name LIKE '%manager%'

Related

Laravel orderByRaw count column not exits

I have a Laravel API in which I want to provide a list of hashtags with the count of posts and comments in which a hashtag is used and order the list by the total count of posts and comments.
Here are my tables: posts, comments, hashtags, hashtag_post, comment_hashtag. Also, proper relationship methods are defined in my models.
After reading the Laravel's documentation and searching the web, I wrote this code:
Hashtag::withCount(['posts', 'comments'])
->orderByRaw('posts_count + comments_count desc')
->paginate(15);
If I use orderBy with only posts_count or comments_count alone, the query works and I get a list, but with the above code, I get this error:
Illuminate/Database/QueryException with message 'SQLSTATE[42703]: Undefined column: 7 ERROR: column "posts_count" does not exist ...
I've used tinker to see if those columns are defined properly on the SQL query, and here's what I've got:
select "hashtags".*, (select count(*) from "posts" inner join "hashtag_post" on "posts"."id" = "hashtag_post"."post_id" where "hashtags"."id" = "hashtag_post"."hashtag_id" and "posts"."deleted_at" is null) as "posts_count", (select count(*) from "comments" inner join "comment_hashtag" on "comments"."id" = "comment_hashtag"."comment_id" where "hashtags"."id" = "comment_hashtag"."hashtag_id" and "comments"."deleted_at" is null) as "comments_count" from "hashtags" where "hashtags"."deleted_at" is null order by posts_count + comments_count desc
I've searched the web about the error, but I found nothing useful.
Edit 1: Based on #Ersoy comment I've changed my query as follow:
Hashtag::selectRaw('Count(posts.id) + Count(comments.id) as total_count')
->withCount(['posts', 'comments'])
->groupBy('hashtags.id')
->orderByRaw('(Count(posts.id) + Count(comments.id)) desc')
->paginate(15);
And as I expected, it didn't work. It says:
Illuminate/Database/QueryException with message 'SQLSTATE[42P01]: Undefined table: 7 ERROR: missing FROM-clause entry for table "posts"
The thing is, likes and comments in the other question, as I understand from the query, are 1-N relationships while my relationships are N-M. Also, I think using the Count function like that won't work without a join. Correct me if I'm wrong.
Edit 2: One of my friends gave me this SQL query which gives the result I want:
select
h.*,
count(distinct hp.post_id) as posts_count,
count(distinct ch.comment_id) as comments_count
from
hashtags as h
left join hashtag_post as hp on
h.id = hp.hashtag_id
left join posts p on
hp.post_id = p.id
left join comment_hashtag ch on
h.id = ch.hashtag_id
left join comments c on
ch.comment_id = c.id
where
h.deleted_at is null
and p.deleted_at is null
and c.deleted_at is null
group by
h.id
order by
( count(distinct hp.post_id) + count(distinct ch.comment_id) ) desc;
But I don't know how to convert it properly to an Eloquent query.
DB::table('hashtags as h')
->leftJoin('hashtag_post as hp', 'h.id', '=' ,'hp.hashtag_id')
->leftJoin('posts as p', 'hp.post_id', '=' ,'p.id')
->leftJoin('comment_hashtag as ch', 'h.id', '=', 'ch.hashtag_id')
->leftJoin('comments as c', 'ch.comment_id', '=' ,'c.id')
->whereNull('h.deleted_at')
->whereNull('p.deleted_at')
->whereNull('c.deleted_at')
->selectRaw("*, count(distinct hp.post_id) + count(distinct ch.comment_id) as total_count")
->groupBy('h.id')
// try
->orderByRaw('count(distinct hp.post_id) + count(distinct ch.comment_id) desc')
// or
->orderByRaw('total_count desc')
->get();
Both the orderByRaw statement are similar, you can try any of the above. Hopefully, it should work.
The following code works perfectly if the Laravel version is 6+:
Hashtag::withCount(['posts', 'comments'])
->orderByRaw('posts_count + comments_count desc')
->paginate(15);
However, I'm currently using Laravel 5.7 and I can't upgrade it now. Thus I should use an alternative to get the desired result:
Hashtag::leftJoin('hashtag_post as hp', 'hashtags.id', '=', 'hp.hashtag_id')
->leftJoin('posts as p', 'hp.post_id', '=', 'p.id')
->leftJoin('comment_hashtag as ch', 'hashtags.id', '=', 'ch.hashtag_id')
->leftJoin('comments as c', 'ch.comment_id', '=', 'c.id')
->whereNull('p.deleted_at')
->whereNull('c.deleted_at')
->selectRaw('hashtags.*, count(distinct hp.post_id) as posts_count, count(distinct ch.comment_id) as comments_count')
->groupBy('hashtags.id')
->orderByRaw('count(distinct hp.post_id) + count(distinct ch.comment_id) desc')
->paginate(15)
Thanks to #tarun-jain

SQL query to get the author who having maximum number of post (custom post type) in Wordpress

I would like to get the author information based on a particular criteria. The criteria is that, would like to get the author who having maximum number of post(custom post type).
This is the code I am trying to get the result.
$author_query = new WP_User_Query(array (
'orderby' => 'post_count',
'order' => 'DESC',
));
$authors = $author_query->get_results();
foreach ( $authors as $author ) {
echo $author->ID;
echo $author->display_name;
}
I solved this using custom sql query. Posting the answer for others who need it in the future.
SELECT SQL_CALC_FOUND_ROWS wp_users.ID,post_count FROM wp_users RIGHT JOIN (SELECT post_author, COUNT(*) as post_count FROM wp_posts WHERE ( ( post_type = 'custom-post-type' AND ( post_status = 'publish' ) ) ) GROUP BY post_author) p ON (wp_users.ID = p.post_author) WHERE 1=1 ORDER BY post_count DESC

Need help on query to select data from union of two tables in Orcacle

Need help in below query:
My requirement is, if role_id is existing for a user in si_org_roles ( first union), then no need to pick up data from audit tables
( its used for list of user deleted from a role) and note that a user can have multiple org - role, ie. org-role to role is many - one relationship.
SELECT m.user_id, m.org_role_id, m.last_update_date,ro.role_id
FROM si_user_org_roles m,si_org_roles ro where m.org_role_id = ro.org_role_id
and ro.role_id = 100074
union
SELECT au.user_id_o,
au.org_role_id_o,
au.last_update_date_o,
ru.role_id
FROM si_user_org_roles_au au,si_org_roles ru
WHERE au.transaction_type = 'Delete'
and au.org_role_id_o = ru.org_role_id
How about this:
SELECT m.user_id,
m.org_role_id,
m.last_update_date,
ro.role_id
FROM si_user_org_roles m,
si_org_roles ro
where m.org_role_id = ro.org_role_id
and ro.role_id = 100074
union
SELECT au.user_id_o,
au.org_role_id_o,
au.last_update_date_o,
ru.role_id
FROM si_user_org_roles_au au,si_org_roles ru
WHERE au.transaction_type = 'Delete'
and au.org_role_id_o = ru.org_role_id
and not exists(select 'X'
from si_user_org_roles m,
si_org_roles ro
where m.org_role_id = ro.org_role_id
and ro.org_role_id = ru.org_role_id
and ro.role_id = 100074)
Totally untested, since you didn't provide table definitions or sample data.
The idea here is that the first half (before union) does the select based on role_id, if there is no data available, the first query will return no rows. Then the second half (after union) goes after the audit data, but, I added the exists clause. The exists clause essentially checks if the first half returned data, and if it did, prevents the second half from returning data.
Hope that makes sense.
You can use NOT EXISTS clause to skip the selection from the audit table.
NOT EXISTS (SELECT role_id
FROM si_org_roles
WHERE role_id = 100074); -- to check the role_id is avlailable in si_org_roles table.
Try like this,
SELECT m.user_id, m.org_role_id, m.last_update_date,ro.role_id
FROM si_user_org_roles m,
si_org_roles ro
WHERE m.org_role_id = ro.org_role_id
AND ro.role_id = 100074
UNION
SELECT au.user_id_o, au.org_role_id_o, au.last_update_date_o, ru.role_id
FROM si_user_org_roles_au au,
si_org_roles ru
WHERE au.transaction_type = 'Delete'
AND au.org_role_id_o = ru.org_role_id
AND NOT EXISTS (SELECT role_id
FROM si_org_roles
WHERE role_id = 100074);

sql confusion with ANDs

I am having a bit of confusion with SQL querying using ANDs.
the query is as follows:
SELECT count(cdr.id) as callTotal,
sum(cdr.callDurationSeconds) as timeTotal,
users.firstname,
users.surname,
MAX(cdr.callDuration) as maxCallDuration,
AVG(cdr.answeredTime) as avgAnswerTime,
AVG(cdr.callDurationSeconds) as avgTimeTotal,
cdr.userId,
COUNT(IF(cdr.callDurationSeconds > '0', 1, NULL)) AS answeredCalls
FROM cdr
LEFT JOIN users ON cdr.userId = users.id
WHERE cdr.company = 'C47B0BCCDCE92F'
AND cdr.dateCreated LIKE '2012-11-02%'
AND cdr.userId = 'U4BC3128209B01'
OR cdr.userId = 'U4A9FCCD73C6BB'
GROUP BY userId ORDER BY users.surname ASC ;
I want to query by the date on the second to last line, but then I wish to retrieve items with any of the posted cdr.userIds.
However it seems to disregard the date in this instance. I need the date to be non-optional and retrieve any data with the select Ids on the chosen date.
If you don't wanna play with OR / AND operators precedence in SQL (which is a rather bad idea), just add brackets.
AND (cdr.userId = 'U4BC3128209B01' OR cdr.userId = 'U4A9FCCD73C6BB')
or as pointed by Martin Smith, in your case, use an IN
AND cdr.userId IN ('U4BC3128209B01','U4A9FCCD73C6BB')
If I understand you correctly you want to select the items with that date AND have a specific id?
SELECT count(cdr.id) as callTotal,
sum(cdr.callDurationSeconds) as timeTotal,
users.firstname,
users.surname,
MAX(cdr.callDuration) as maxCallDuration,
AVG(cdr.answeredTime) as avgAnswerTime,
AVG(cdr.callDurationSeconds) as avgTimeTotal,
cdr.userId,
COUNT(IF(cdr.callDurationSeconds > '0', 1, NULL)) AS answeredCalls
FROM cdr
LEFT JOIN users ON cdr.userId = users.id
WHERE cdr.company = 'C47B0BCCDCE92F'
AND cdr.dateCreated LIKE '2012-11-02%'
AND cdr.userId IN('U4BC3128209B01','U4A9FCCD73C6BB')
GROUP BY userId ORDER BY users.surname ASC ;
You could wrap the OR with parenthesis like the others mentioned or use IN like this
SELECT count(cdr.id) as callTotal,
sum(cdr.callDurationSeconds) as timeTotal,
users.firstname,
users.surname,
MAX(cdr.callDuration) as maxCallDuration,
AVG(cdr.answeredTime) as avgAnswerTime,
AVG(cdr.callDurationSeconds) as avgTimeTotal,
cdr.userId,
COUNT(IF(cdr.callDurationSeconds > '0', 1, NULL)) AS answeredCalls
FROM cdr
LEFT JOIN users ON cdr.userId = users.id
WHERE cdr.company = 'C47B0BCCDCE92F'
AND cdr.dateCreated LIKE '2012-11-02%'
AND cdr.userId IN ('U4BC3128209B01','U4A9FCCD73C6BB') GROUP BY userId ORDER BY users.surname ASC ;
That way there's no OR, so no confusion
In SQL AND has a higher order then OR so by default your where resolves to
WHERE (cdr.company = 'C47B0BCCDCE92F'
AND cdr.dateCreated LIKE '2012-11-02%'
AND cdr.userId = 'U4BC3128209B01') OR cdr.userId = 'U4A9FCCD73C6BB'
Where you want
WHERE cdr.company = 'C47B0BCCDCE92F'
AND cdr.dateCreated LIKE '2012-11-02%'
AND (cdr.userId = 'U4BC3128209B01' OR cdr.userId = 'U4A9FCCD73C6BB')
Use brackets and it will clear things up.

Rewrite sql query to Zend_Db_Select

I'm trying to build the following sql query as a Zend_Db_Select object
$sql = "
SELECT
u.id,
u.email,
s.nic as nic,
(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='login') as logins,
(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='export') as exports,
(SELECT MAX(time) FROM event WHERE user_id = u.id AND event='login') as lastlogin,
(DATEDIFF(u.expire_date, NOW())) as daysleft
FROM
user u,
seller s
WHERE
u.seller_id = s.id";
with no luck. I can't get the subquerys working in the Zend_Db_Select object.
Is it possible to achieve the same same result by joins instead of subquerys?
Any hints on how to get this working would be highly appreciated.
Try something like this:
$select->from(array('u'=>'user'),array('id','email'));
$select->join(array('s'=>'seller'),'s.id = u.seller_id', array('nic'));
$select->columns(array(
'logins' =>"(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='login')",
'exports' =>"(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='export')",
'lastLogin' =>"(SELECT MAX(time) FROM event WHERE user_id = u.id AND event='login')",
'daysLeft' =>"(DATEDIFF(u.expire_date, NOW()))",));
$stmt = $select->query();
var_dump($stmt->fetchAll());