How to refer a table column as clause condition with Arel? - ruby-on-rails

I have this scope:
scope :in_range, lambda { |begin_time, end_time, excluded_sales = nil|
where(
'start_at BETWEEN ? AND ? OR finish_at BETWEEN ? AND ? OR ? BETWEEN start_at AND finish_at OR ? BETWEEN start_at AND finish_at',
begin_time, end_time, begin_time, end_time, begin_time, end_time)
.where.not(id: excluded_sales)
}
This produces
> Sale.in_range(Time.now, Time.now + 1)
Sale Load (0.6ms) SELECT "sales".* FROM "sales" WHERE (start_at BETWEEN '2014-12-12 10:45:47.065712' AND '2014-12-12 10:45:48.065714' OR finish_at BETWEEN '2014-12-12 10:45:47.065712' AND '2014-12-12 10:45:48.065714' OR '2014-12-12 10:45:47.065712' BETWEEN start_at AND finish_at OR '2014-12-12 10:45:48.065714' BETWEEN start_at AND finish_at) AND ("sales"."id" IS NOT NULL)
I need to rewrite this scope using Arel because I believe I should not use raw SQL syntax here.
But I do not know how to use in method with table column reference. I've tried this but got exception:
> s = Sale.arel_table
> begin_time = Time.now
> Sale.where(begin_time.in(s[:start_at]..s[:finish_at]))
--> ArgumentError: bad value for range
Also tried this, but had no luck:
> s = Sale.arel_table
> begin_time = Time.now
> Sale.where(begin_time.in('sales.start_time'..'sales.end_time')
--> NoMethodError: undefined method `round' for "sales.end_time".."sales.start_time":Range
Please advise how to refer a table column as clause condition with Arel?
Thank you!
UPDATED:
There are four conditions connected with OR statement:
start_at in range of begin_time and end_time
finish_at in range of begin_time and end_time
begin_time in range of start_time and finish_time
end_time in range of start_time and finish_time
Basically it is just a date ranges overlap.
I rewrote it to
scope :in_range, lambda { |date_range, excluded_sales = nil|
s = Sale.arel_table
where(
s.grouping(s[:starts_at].gteq(date_range.first).and(s[:starts_at].lteq(date_range.last)))
.or(s.grouping(s[:ends_at].gteq(date_range.first).and(s[:ends_at].lteq(date_range.last))))
.or(s.grouping(s[:starts_at].lteq(date_range.first).and(s[:ends_at].gteq(date_range.last))))
.or(s.grouping(s[:starts_at].gteq(date_range.first).and(s[:ends_at].lteq(date_range.last))))
).where.not(id: excluded_sales)
}
Maybe there is a simplier way to achieve my goal?

well as far as I understood, you wish to select all record, which range have an intersection to the specified one? If so, just select records, which have start_at field values that are before the end of range, and have finish_at field values that are after the begin of range. So with arel it should be:
scope :in_range, proc do |begin_time, end_time, excluded_sales|
where(arel_table[:start_at].lteq(end_time)
.and(arel_table[:finish_at].gteq(begin_time))
.and(arel_table[:id].not_eq(excluded_sales)))
end

Related

Comparison of datetime in where condition

I need to add a date comparison in a query.
The field is column delivery_date timestamp without time zone
The condition should be delivery_date <= today.
I have tried :
"delivery_date < ?", Date.today
delivery_date.lt(Date.today)
"delivery_date" <= time.now [Error : NameError - undefined local variable or method `time' for ]
"delivery_date" <= Time.now [Error : ArgumentError - comparison of String with DateTime failed: ]
but I am getting different errors with all.
Here is the query where I need to add my condition :
datas: tab.project
.active
.where("delivery_date" <= date.today, step_id: Step::OPENED, test: {typess: type})
.joins(:test, :account)
.group(:'account.name')
.order('count(tab.id) DESC')
.count(:id)
Any idea how it should be done?
Try:
tab.project.active.where("delivery_date <= ?", Date.today)
or
tab.project.active.where("delivery_date <= :date", date: Date.today)
In both cases we use Date.today that returns (suprisingly :-) ) the date of today. We use two ways of adding param to a query -
array condition where ? is replaced by next where arguments (in this case the Date.today)
placeholder condition where a symbol is replaced by a hash value for this symbol (date: Date.today)

How to check if params present in scope rails

scope :created_after, ->(start_time,end_time) { where("start_time > ? and end_time <?", start_time,end_time) }
I want to add if condition if start time present then only start_time query run if end_time then its query run
I think created_between is better name for what you want to do with your created_after scope.
You can create two helper scopes to filter results using start_time and end_time column.
scope :created_after, ->(start_time) do
where("start_time > ?", start_time)
end
scope :created_before, ->(end_time) do
where("end_time < ?", end_time)
end
Then you can combine them in a way you wanted:
scope :created_between, ->(start_time, end_time) do
query = self.all
query = query.created_after(start_time) if start_time
query = query.created_before(end_time) if end_time
query
end
If you execute query giving nil for any scope argument it will ignore them. For example: created_between(nil, nil) will result in no additional query.
You can just chain it in the condition
scope :created_after, ->(start_time, end_time) { start_time && end_time && where("start_time > ? and end_time <?", start_time, end_time) }
This will add the where condition only if start_time and end_time is present

What is the difference theses two Ruby on Rails logical sentences?

I have an rails application and there is a filtering function. One of the filtering criteria is time value and when I try to filter by time criterion it keeps showing wrong results.
**There was an mistake in variable name so I edited
start_time = DateTime.new(2000, 1, 1, 6, 0, 0)
end_time = DateTime.new(2000, 1, 1, 9, 0, 0)
When I execute the filtering function, the result is:
Item.where("(start_at >= ? AND start_at < ?) OR (end_at > ? AND end_at <= ?) OR (start_at < ? AND end_at > ?)", start_time, end_time, start_time, end_time, start_time, end_time)
=> Item id: 5931
But with this result I execute the same sentence the result is:
(Item.find(5931).start_at >= start_time AND Item.find(5931).start_at < end_time)
=> false
(Item.find(5931).end_at > start_time AND Item.find(5931).end_at <= end_time)
=> false
(Item.find(5931).start_at < start_time AND Item.find(5931).end_at > end_time)
=> false
(Item.find(5931).start_at >= start_time AND Item.find(5931).start_at < end_time) or (Item.find(5931).end_at > start_time AND Item.find(5931).end_at <= end_time) or (Item.find(5931).start_at < start_time AND Item.find(5931).end_at > end_time)
=> false
Is there anyone to find the point?
If I missed any information needed please tell me.
To query for data between two times use the SQL BETWEEN keyword:
Item.where(start_at: start_time..end_time)
# => SELECT "items".* FROM "items" WHERE ("items"."start_at" BETWEEN $1 AND $2) [["start_at", 2000-01-01 06:00:00 UTC], ["start_at", 2000-01-01 09:00:00 UTC]]
.. in start_time..end_time is a special syntax that creates a Range object.
To add an OR clause you can use the .or method in Rails 5:
Item.where(start_at: start_time..end_time)
.or(Item.where(end_at: start_time..end_time))
In Rails 4 you need to write the SQL string:
Item.where(
'(items.start_at BETWEEN :s AND :e) OR (items.end_at BETWEEN :s AND :e)',
s: start_time, e: end_time
)

ActiveRecord multiple where date range

Is there a more ActiveRecord idiomatic way to find which records have a start_at or end_at within a certain date_range? (Basically, need to find the records that start or end in a given time frame). Here's what I'm currently doing:
Project.where('(start_at >= ? AND start_at <= ?) OR (end_at >= ? AND end_at <= ?)', start_at, end_at, start_at, end_at)
You can pass ranges to where and use or:
time_range = (start_at..end_at)
Project.where(start_at: time_range).or(Project.where(end_at: time_range))
For Rails 4 you could use BETWEEN and hash params:
Project.where(
"(start_at BETWEEN :start_at AND :end_at) OR (end_at BETWEEN :start_at AND :end_at)",
start_at: start_at.beginning_of_day, end_at: end_at.end_of_day)

Rails scopes, filtering all by date and child data

I have a model called "Campaign" and it belongs_to a "Schedule".
A Campaign has a start and end date, and the Schedule contains information such as "Every Monday", "Every day", etc.
I'm wanting to select all the Campaigns that are valid for a specific date.
# campaign.rb
scope :active, -> { where(status: true) }
scope :inactive, -> { where(status: false) }
def valid_for(date)
includes(:schedules).where("
( start_at >= ? AND (end_at IS NULL OR end_at <= ? ) )
AND schedules.days_of_week = ?",
date,
date,
date.wday
)
end
And in my Campaign spec (the Campaign and Schedule does exist in the test):
expect(Campaign.active.valid_for("2015-06-15".to_date).size).to eq 1
The error I'm getting is:
undefined method `valid_for' for #<ActiveRecord::Relation []>
I cannot use a class method for self.valid_for because I need to be able to access the attributes start_at and end_at, as well as the parent schedule. However, it doesn't seem to be working as an instance method either.
Make it a scope and use a scope argument:
scope :valid_for, ->(date) {
includes(:schedules).where("
( start_at >= ? AND (end_at IS NULL OR end_at <= ? ) )
AND schedules.days_of_week = ?,
start_at,
end_at,
date.wday
")
}
This will limit your access to the start_at and end_at though. You'll need to get those from the database or pass them in as args to your scope.

Resources