ActiveRecord Subquery Comparison as a Scope - sql

I'm rewriting a manual SQL query into an ActiveRecord query for use in a Rails app.
What it does it collect two groups of users from the same table: 1 who have a qualifying event (44 or 48) in the last week, and a group of users who have the same qualifying event a month ago.
This is the current query. I'm not sure how to turn this into an ActiveRecord scope:
select top 500 active_users_last_week.email
from (
select distinct(email) from user_event_tracking
where event_id in (44,48)
and sitedb_created_date between getdate()-8 and getdate()-1
and sitedb_name = 'XYZ'
and email not like '%#company.com'
and email not like '%#other_company.com'
) active_users_last_week,
(
select distinct(email) from user_event_tracking
where event_id in (44,48)
and sitedb_created_date between getdate()-60 and getdate()-30
and sitedb_name = 'XYZ'
and email not like '%#company.com'
and email not like '%#other_company.com
) active_users_last_month
where active_users_last_week.email = active_users_last_month.email;
Any suggestions on how to turn this into an ActiveRecord scope? I have these set as scopes already:
scope :active_events, lambda {
where("event_id in (44,48)")
}
scope :for_property, lambda { |property|
where('sitedb_name = ?', property)
}
scope :last_week, lambda {
where("sitedb_created_date between GETDATE()-8 and GETDATE()-1")
}
scope :last_month, lambda {
where("sitedb_created_date between GETDATE()-60 and GETDATE()-30")
}
scope :no_test_users, lambda {
where("email not like '%#company.com' and email not like '%#other_company.com'")
}
The scopes all work individually (and with each other). The question is how to get emails that are in both Event.active_events.last_week and Event.active_events.last_month in an efficient way.

Try this:
Event.select(:email).where(:event_id => [44,48], sitedb_created_date => (8.days.ago..1.day.ago), :sitedb_name => 'XYZ', :email => Event.select(:email).where(:event_id => [44,48], sitedb_created_date => (60.days.ago..30.days.ago), :sitedb_name => 'XYZ').where(Event.arel_table[:email].does_not_match_all(["%company.com","%other_company.com"])))
You might need to tinker with days to adjust them to your date range, I am not 100% sure if they are inclusive or not. You might need to change 8.days.ago to 7.days.ago etc.
You also should be able to do this with your scopes:
Event.active_events.last_week.where(:email => Event.active_events.last_month.select(:email))

Related

matching associations and no associated record cakephp 3

I have an association of Price belongsTo Season
I am trying to query all prices that match a specific date range when passed in the season as well as any that have none (Prices.season_id=0)
Here is what I have:
// build the query
$query = $this->Prices->find()
->where(['product_id'=>$q['product_id']])
->contain(['Seasons']);
if(!empty($to_date) && !empty($from_date)) {
$query->matching('Seasons', function ($q) {
return $q->where([
'from_date <= ' => $to_date,
'to_date >= ' => $from_date
]);
});
}
However, this will only return Prices explicitly associated with a Season. How do I make it return Prices.season_id=0 also?
The $query->matching() call internally creates a INNER JOIN and places the where-statements of the callback-function into the ON clause of the join. For retrieving items without the association you need a LEFT JOIN. So your codesnippet would look like this:
if(!empty($to_date) && !empty($from_date)) {
$query->leftJoinWith('Seasons', function ($q){return $q;});
$query->where([[
'or' => [
'Season.id IS NULL',
[
'from_date <= ' => $to_date,
'to_date >= ' => $from_date,
],
],
]]);
}
So we create a normal INNER JOIN and place the conditions in the normal (outmost) where clause of the query.
The double array is for disambiguation of probably other where conditions with an or connection.
I myself stumbled over the column IS NULL instead of 'column' => null syntax.
PS: This works for all associations. For hasMany and belongsToMany you have to group the results with $query->group('Prices.id')

How to concatonate conditions with Active Record scope, Rails 4

I am trying to create a scope for an Active Record model using rails 4. The scope deals with 2 conditions which I am able to execute individually:
1) includes(:categories).for_category("proposal")
2) where(:is_published => true)
I need a scope that returns all records where either condition is met (not necessarily both). So something like:
scope :proposal, -> { includes(:categories).for_category("proposal") || where(:is_published => true)}
Thanks for any help.
**EDIT**
Per comment here is the definition of the for_category scope taken from https://github.com/comfy/comfortable-mexican-sofa/blob/b16bb14bec7bffee61949d72a5f2c9eca3c95bf8/lib/comfortable_mexican_sofa/extensions/is_categorized.rb
scope :for_category, lambda { |*categories|
if (categories = [categories].flatten.compact).present?
self.distinct.
joins(:categorizations => :category).
where('comfy_cms_categories.label' => categories)
end
}
You'll need to define a new scope which includes an sql "OR". I don't know what your "for_category" scope is doing but let's say it's something like
scope :for_category, ->(category_name) { where("categories.name" => category_name)}
Then you could make a new scope like this: note that the where arguments are a list of values rather than a hash (the hash form can only produce sql for = tests joined with AND)
scope :published_or_for_category ->(category_name){ includes(:categories).where("categories.name = ? OR proposals.is_published = ?", category_name, true) }
Based on Max's suggestions I got the following to work:
scope :published_or_for_category, ->(category_name){
includes(:categories).where(
"comfy_cms_categories.label = ? OR is_published = ?",
category_name, true
)
}
I might mention that the following seems to work as well:
scope :proposal, -> { includes(:categories).for_category("proposal") | where(:is_published => true) }
Is one better that the other?

How can I get an ActiveRecord query to ignore nil conditions?

In order to avoid having to construct complicated dynamic SQL queries, I'd like to be able to just pass in nil values in my conditions, and have those ignored. Is that supported by ActiveRecord?
Here is an example.
event = Event.find(:all, :conditions => {
:title => params[:title],
:start_time => params[:start_time],
:end_time => params[:end_time]
}).first
In that particular case, if params[:start_time] is set to nil, ActiveRecord will search for those Events that have their start_time set to null. Instead, I'd like it to just ignore start_time. How do I do that?
You don't have to "create complicated dynamic SQL queries" to do what you need. Simply construct your conditions hash separately, and either exclude the null values at the time of creation or after you've created the hash.
conditions = {}
conditions[:title] = params[:title] unless params[:title].blank?
conditions[:start_time] = params[:start_time] unless params[:start_time].blank?
conditions[:end_time] = params[:end_time] unless params[:end_time].blank?
or
conditions = {:title => params[:title], :start_time => params[:start_time], :end_time => params[:end_time]}
conditions.delete_if {|k,v| v.blank? }
or
conditions = params.reject {|k,v| !([:title, :start_time, :end_time]).include?(k) }
but that last form will only work if the keys are actually symbols. In Rails the params hash is a HashWithIndifferentAccess which allows you to access the text keys as symbols. Of course you could just use the text values in your array of keys to include if necessary.
and then query with your pre-built conditions hash:
event = Event.find(:all, :conditions => conditions).first

ActiveRecord Arel OR condition

How can you combine 2 different conditions using logical OR instead of AND?
NOTE: 2 conditions are generated as rails scopes and can't be easily changed into something like where("x or y") directly.
Simple example:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
It's easy to apply AND condition (which for this particular case is meaningless):
(admins.merge authors).to_sql
#=> select ... from ... where kind = 'admin' AND kind = 'author'
But how can you produce the following query having 2 different Arel relations already available?
#=> select ... from ... where kind = 'admin' OR kind = 'author'
It seems (according to Arel readme):
The OR operator is not yet supported
But I hope it doesn't apply here and expect to write something like:
(admins.or authors).to_sql
ActiveRecord queries are ActiveRecord::Relation objects (which maddeningly do not support or), not Arel objects (which do).
[ UPDATE: as of Rails 5, "or" is supported in ActiveRecord::Relation; see https://stackoverflow.com/a/33248299/190135 ]
But luckily, their where method accepts ARel query objects. So if User < ActiveRecord::Base...
users = User.arel_table
query = User.where(users[:kind].eq('admin').or(users[:kind].eq('author')))
query.to_sql now shows the reassuring:
SELECT "users".* FROM "users" WHERE (("users"."kind" = 'admin' OR "users"."kind" = 'author'))
For clarity, you could extract some temporary partial-query variables:
users = User.arel_table
admin = users[:kind].eq('admin')
author = users[:kind].eq('author')
query = User.where(admin.or(author))
And naturally, once you have the query you can use query.all to execute the actual database call.
I'm a little late to the party, but here's the best suggestion I could come up with:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
admins = admins.where_values.reduce(:and)
authors = authors.where_values.reduce(:and)
User.where(admins.or(authors)).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE ((\"users\".\"kind\" = 'admin' OR \"users\".\"kind\" = 'author'))"
As of Rails 5 we have ActiveRecord::Relation#or, allowing you to do this:
User.where(kind: :author).or(User.where(kind: :admin))
...which gets translated into the sql you'd expect:
>> puts User.where(kind: :author).or(User.where(kind: :admin)).to_sql
SELECT "users".* FROM "users" WHERE ("users"."kind" = 'author' OR "users"."kind" = 'admin')
From the actual arel page:
The OR operator works like this:
users.where(users[:name].eq('bob').or(users[:age].lt(25)))
I've hit the same problem looking for an activerecord alternative to mongoid's #any_of.
#jswanner answer is good, but will only work if the where parameters are a Hash :
> User.where( email: 'foo', first_name: 'bar' ).where_values.reduce( :and ).method( :or )
=> #<Method: Arel::Nodes::And(Arel::Nodes::Node)#or>
> User.where( "email = 'foo' and first_name = 'bar'" ).where_values.reduce( :and ).method( :or )
NameError: undefined method `or' for class `String'
To be able to use both strings and hashes, you can use this :
q1 = User.where( "email = 'foo'" )
q2 = User.where( email: 'bar' )
User.where( q1.arel.constraints.reduce( :and ).or( q2.arel.constraints.reduce( :and ) ) )
Indeed, that's ugly, and you don't want to use that on a daily basis. Here is some #any_of implementation I've made : https://gist.github.com/oelmekki/5396826
It let do that :
> q1 = User.where( email: 'foo1' ); true
=> true
> q2 = User.where( "email = 'bar1'" ); true
=> true
> User.any_of( q1, q2, { email: 'foo2' }, "email = 'bar2'" )
User Load (1.2ms) SELECT "users".* FROM "users" WHERE (((("users"."email" = 'foo1' OR (email = 'bar1')) OR "users"."email" = 'foo2') OR (email = 'bar2')))
Edit : since then, I've published a gem to help building OR queries.
Just make a scope for your OR condition:
scope :author_or_admin, where(['kind = ? OR kind = ?', 'Author', 'Admin'])
Using SmartTuple it's going to look something like this:
tup = SmartTuple.new(" OR ")
tup << {:kind => "admin"}
tup << {:kind => "author"}
User.where(tup.compile)
OR
User.where((SmartTuple.new(" OR ") + {:kind => "admin"} + {:kind => "author"}).compile)
You may think I'm biased, but I still consider traditional data structure operations being far more clear and convenient than method chaining in this particular case.
To extend jswanner answer (which is actually awesome solution and helped me) for googling people:
you can apply scope like this
scope :with_owner_ids_or_global, lambda{ |owner_class, *ids|
with_ids = where(owner_id: ids.flatten).where_values.reduce(:and)
with_glob = where(owner_id: nil).where_values.reduce(:and)
where(owner_type: owner_class.model_name).where(with_ids.or( with_glob ))
}
User.with_owner_ids_or_global(Developer, 1, 2)
# => ...WHERE `users`.`owner_type` = 'Developer' AND ((`users`.`owner_id` IN (1, 2) OR `users`.`owner_id` IS NULL))
What about this approach: http://guides.rubyonrails.org/active_record_querying.html#hash-conditions (and check 2.3.3)
admins_or_authors = User.where(:kind => [:admin, :author])
Unfortunately it is not supported natively, so we need to hack here.
And the hack looks like this, which is pretty inefficient SQL (hope DBAs are not looking at it :-) ):
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
both = User.where("users.id in (#{admins.select(:id)}) OR users.id in (#{authors.select(:id)})")
both.to_sql # => where users.id in (select id from...) OR users.id in (select id from)
This generates subselets.
And a little better hack (from SQL perspective) looks like this:
admins_sql = admins.arel.where_sql.sub(/^WHERE/i,'')
authors_sql = authors.arel.where_sql.sub(/^WHERE/i,'')
both = User.where("(#{admins_sql}) OR (#{authors_sql})")
both.to_sql # => where <admins where conditions> OR <authors where conditions>
This generates proper OR condition, but obviously it only takes into account the WHERE part of the scopes.
I chose the 1st one until I'll see how it performs.
In any case, you must be pretty careful with it and watch the SQL generated.

Optimise ignoring of undefined variables when building find conditions in Rails

I have a an method that retrieves Groups that are present in certain areas. Groups are given a country_id, region_id and city_id
The UI gives three select boxes to choose a country, a region from that country and then a city from that region. To find all groups in a particular city, I have this code:
#groups = Group.find(:all, :conditions => {:city_id => params[:city_id]})
This all works fine, but I also want it to find all groups in an area when the lower criteria isn't specified. For example, If a country and region are given, but not city, I'd like to find it by the region.
What I'm doing is this:
if !params[:city_id].nil?
#groups = Group.find(:all, :conditions => {:city_id => params[:city_id]})
else
if !params[:region_id].nil?
#groups = Group.find(:all, :conditions => {:region_id => params[:region_id]})
else
#groups = Group.find(:all, :conditions => {:country_id => params[:country_id]})
end
end
This works perfectly well, but it seems like it's a little inefficient. Am I doing it the best way or can I streamline a little?
One idea I had was to have a single find checking against all parameters, but I could not work out how to effectively 'ignore' parameters that were nil - my main thought was to check which ones were not set and set them to something like '*' or 'true', but that's not how SQL plays the game.
Sounds like a job for named scopes:
class Group < ActiveRecord::Base
named_scope :in_city, lambda { |city_id| {
:conditions => { :city_id => city_id }
}}
named_scope :in_region, lambda { |region_id | {
:conditions => { :region_id => region_id }
}}
named_scope :in_country, lambda { |country_id | {
:conditions => { :country_id => country_id }
}}
end
This establishes some simple scopes for restricting the Group records. Presumably you have indexed your database properly so these are quick to resolve.
The controller is much easier to implement then:
def index
#group_scope = Group
if (!params[:city_id].blank?)
#group_scope = #group_scope.in_city(params[:city_id])
elsif (!params[:region_id].blank?)
#group_scope = #group_scope.in_region(params[:region_id])
elsif (!params[:country_id].blank?)
#group_scope = #group_scope.in_country(params[:country_id])
end
#groups = #group_scope.all
end
Generally you should be testing for .blank? instead of .nil? as some form elements can send in empty results, such as a select with something akin to "All" as the default.
You could use some Ruby idioms to get something a little more succinct.
Try something like this: (untested code!)
def index
#groups = Group.find :all, :conditions => [:city_id, :region_id, :country_id].inject {} do |conditions, name|
conditions[name] = params[name] unless params[name].blank?
conditions
end
end
If every value in params is a candidate for :conditions you can just do this:
#groups = Group.all(:conditions => params.reject { |idx, val| val.nil? })
This just throws out nil values from params and uses the remaining values for conditions.
If you don't want to use all of the values in params, you have two options. You can just get rid of a bunch of redundancy in your original code:
conditions = if !params[:city_id].nil?
{ :city_id => params[:city_id] }
elsif !params[:region_id].nil?
{ :region_id => params[:region_id] }
else
{ :country_id => params[:country_id] }
end
#groups = Group.all(:conditions => conditions)
You can knock of a few more lines like this, but it sacrifices a bit of readability IMO:
conditions = if !params[:city_id].nil? then { :city_id => params[:city_id] }
elsif !params[:region_id].nil? then { :region_id => params[:region_id] }
else { :country_id => params[:country_id] }
end
Or you can do something like this:
conditions = [:city_id, :region_id, :country_id].inject({}) do |hsh, sym|
hsh[sym] = params[sym] unless params[sym].nil?
hsh
end
#groups = Group.all(:conditions => conditions)
This has the advantage that you don't need to add another condition for each symbol.