Rails where condition with timestamp - sql

I have query, that would return me data depending on its created_at timestamp
my query looks like
condition[:created_at] = " > #{Time.now - 2.days}"
model.where(condition)
and this return me following sql
...WHERE `model`.`created_at` = ' > 2000-01-01T02:00:00+02:00'
so here timestamp looks different from what in db
So how do i pass correct timestamp to match AR format?

ActiveSupport's #ago will help:
model.where("created_at > ?", 2.days.ago)
Also, I wrote a gem to contain common scopes for created_at queries and others: https://github.com/neighborland/scopy

Related

How to compare the month parts of two dates?

I am trying to query query the current month, here is my query:
$clients = $this->Clients;
$query = $clients->find();
if($this->Auth->user('role') !== 'admin'){
$query->where(['user_id =' => $this->Auth->user('id')]);
$query->where(['MONTH(dob) = ' => 'EXTRACT(month FROM (NOW()))']);
$query->order(['dob' => 'ASC']);
}
It returns 0 records (my field is a date type), however this query in phpmyadmin works:
SELECT * FROM `clients` WHERE MONTH(dob) = EXTRACT(month FROM (NOW()))
What am I doing wrong?
Just look at the actual generated query (check out your DBMS query log, or try DebugKit), it will look different, as the right hand side value in a key => value condition set is subject to parameter-binding/casting/quoting/escaping. In your case it will be treated as a string, so the condition will finally look something like:
WHERE MONTH(dob) = 'EXTRACT(month FROM (NOW()))'
That will of course not match anything.
You could pass the whole SQL snippet as a single array value, or as an expression object, that way it would be inserted into the query as is (do not insert user values that way, that would create an SQL injection vulnerability!), but I'd suggest to use portable function expressions instead.
CakePHP ships with functions expressions for EXTRACT and NOW, so you can simply do something like:
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\ORM\Query;
// ...
$query->where(function (QueryExpression $exp, Query $query) {
return $exp->eq(
$query->func()->extract('MONTH', new IdentifierExpression('dob')),
$query->func()->extract('MONTH', $query->func()->now())
);
});
Looks a bit complicated, but it's worth it, it's cross DBMS portable as well as auto-quoting compatible. The generated SQL will look something like
WHERE EXTRACT(MONTH FROM (dob)) = (EXTRACT(MONTH FROM (NOW())))
See also
Cookbook > Database Access & ORM > Query Builder > Advanced Conditions
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
API > \Cake\Database\Expression\QueryExpression::eq()
API > \Cake\Database\FunctionsBuilder::extract()
API > \Cake\Database\FunctionsBuilder::now()

Eloquent complex query

I need sql query, but i need to use Eloquent:
"SELECT * FROM Pass WHERE serial_number=$registered_serial_numbers AND (updated_at IS NULL OR updated_at >= $passesUpdatedSince)"
I try but have error:
$registered_passes = Pass::where('serial_number', $registered_serial_numbers)->where("updated_at IS NULL OR updated_at >= $passesUpdatedSince")->get();
How should i do it in Eloquent?
Two ways of doing it:
1) using whereRaw()
$registered_passes = Pass::whereIn('serial_number', $registered_serial_numbers)->whereRaw('updated_at IS NULL OR updated_at >= ?', $passesUpdatedSince)->get();
2) using a closure to do the AND (OR)
$registered_passes = Pass::whereIn('serial_number', $registered_serial_numbers)->where(function($query){
$query->whereNull('updated_at')->orWhere('updated_at', '>=', $passesUpdatedSince);})->get();
https://laravel.com/docs/5.3/queries#where-clauses
Have a look at the "Parameter Grouping" section
The query should be something like this:
$registered_passes = Pass::where("updated_at", $passesUpdatedSince)->whereIn('serial_number',$registered_serial_numbers)->get();
You use whereIn method for where in clause and make sure $registered_serial_numbers is an array of values.
My bad, misread it. If you are fine with serial number, just do a whereRaw like this:
$registered_passes = Pass::where('serial_number',$registered_serial_numbers)->whereRaw('updated_at IS NULL OR updated_at >= ?', $passesUpdatedSince)->get();

Rails / Postgres: “must appear in the GROUP BY clause or be used in an aggregate function”

I'm using this method:
def self.lines_price_report(n)
Income.group('date(filled_at)').having("date(filled_at) > ?", Date.today - n).sum(:lines_price)
end
I'm getting this error in Heroku:
PG::Error: ERROR: column "incomes.filled_at" must appear in the GROUP BY clause
or be used in an aggregate function
How can I fix this? Thank you.
Executed query:
SELECT SUM("incomes"."lines_price") AS sum_lines_price, date(filled_at)
AS date_filled_at FROM "incomes"
HAVING (date(filled_at) > '2012-12-04')
GROUP BY date(filled_at) ORDER BY filled_at ASC
Expected result
[["2012-12-04", SUM_FOR_DATE], ["2012-12-05", SUM_FOR_DATE], ...]
Your mistake was to use filled_at in order by probably in default scope.
You can fix it using unscoped to eliminate default scopes:
Income.unscoped
.group('date(filled_at)')
.having("date(filled_at) > ?", Date.today - n)
.sum(:lines_price)
or
Income.unscoped
.group('date(filled_at)')
.having("date(filled_at) > ?", Date.today - n)
.sum(:lines_price)
.order('date(filled_at) ASC')
but I think that better will be to use where instead of having
Income.unscoped
.where("date(filled_at) > TIMESTAMP ?", Date.today - n)
.group('date(filled_at)')
.sum(:lines_price)
.order('date(filled_at) ASC')
SQLFiddle
You have to be careful about using TIMESTAMP because 2012-12-04 will become 2012-12-04 00:00:00 so if you don't want this day in result use Date.today - (n - 1)
If you create index on filled_at column
create index incomes_filled_at on incomes(filled_at);
migration:
add_index :incomes, :filled_at
and you have a lot of data in this table index will be used in filtering. So query should be much faster.
So just write both and test which is faster (you have to create index on filled_at if you don't have one).
I guess this is because you use date(filled_at) in GROUP BY but just filled at in ORDER. As I guess order is taken from default scope you need to overwrite it by reorder. I would suggest:
Income.sum(:lines_price).
group('date(filled_at)').
having("date(filled_at) > ?", Date.today - n).
reorder("date(filled_at) ASC")
When you want to use Group By on PostgreSQL, The select option should be required on the group by.
Income.select('filled_at').group('date(filled_at)').having("date(filled_at) > ?", Date.today - n).sum(:lines_price)

Rails 3 query in multiple date ranges

Suppose we have some date ranges, for example:
ranges = [
[(12.months.ago)..(8.months.ago)],
[(7.months.ago)..(6.months.ago)],
[(5.months.ago)..(4.months.ago)],
[(3.months.ago)..(2.months.ago)],
[(1.month.ago)..(15.days.ago)]
]
and a Post model with :created_at attribute.
I want to find posts where created_at value is in this range, so the goal is to create a query like:
SELECT * FROM posts WHERE created_at
BETWEEN '2011-04-06' AND '2011-08-06' OR
BETWEEN '2011-09-06' AND '2011-10-06' OR
BETWEEN '2011-11-06' AND '2011-12-06' OR
BETWEEN '2012-01-06' AND '2012-02-06' OR
BETWEEN '2012-02-06' AND '2012-03-23';
If you have only one range like this:
range = (12.months.ago)..(8.months.ago)
we can do this query:
Post.where(:created_at => range)
and query should be:
SELECT * FROM posts WHERE created_at
BETWEEN '2011-04-06' AND '2011-08-06';
Is there a way to make this query using a notation like this Post.where(:created_at => range)?
And what is the correct way to build this query?
Thank you
It gets a little aggressive with paren, but prepare to dive down the arel rabbit hole
ranges = [
((12.months.ago)..(8.months.ago)),
((7.months.ago)..(6.months.ago)),
((5.months.ago)..(4.months.ago)),
((3.months.ago)..(2.months.ago)),
((1.month.ago)..(15.days.ago))
]
table = Post.arel_table
query = ranges.inject(table) do |sum, range|
condition = table[:created_at].in(range)
sum.class == Arel::Table ? condition : sum.or(condition)
end
Then, query.to_sql should equal
(((("sessions"."created_at" BETWEEN '2011-06-05 12:23:32.442238' AND '2011-10-05 12:23:32.442575' OR "sessions"."created_at" BETWEEN '2011-11-05 12:23:32.442772' AND '2011-12-05 12:23:32.442926') OR "sessions"."created_at" BETWEEN '2012-01-05 12:23:32.443112' AND '2012-02-05 12:23:32.443266') OR "sessions"."created_at" BETWEEN '2012-03-05 12:23:32.443449' AND '2012-04-05 12:23:32.443598') OR "sessions"."created_at" BETWEEN '2012-05-05 12:23:32.443783' AND '2012-05-21 12:23:32.443938')
And you should be able to just do Post.where(query)
EDIT
You could also do something like:
range_conditions = ranges.map{|r| table[:created_at].in(r)}
query = range_conditions.inject(range_conditions.shift, &:or)
to keep it a little more terse
I suggest you try the pure string form:
# e.g. querying those in (12.months.ago .. 8.months.ago) or in (7.months.ago .. 6.months.ago)
Post.where("(created_at <= #{12.months.ago} AND created_at >= #{8.months.ago} ) OR " +
"(created_at <= #{7.months.ago} AND created_at >= #{6.months.ago} )" )
In your case, I would suggest to use mysql IN clause
Model.where('created_at IN (?)', ranges)

Is there a way to update an entire array or named_scope without using a loop in ruby on rails?

I create the following array using searchlogic named_scopes:
todos = Todo.asset_is("Email").asset_id_is(self.id)
For each value in the array, there is an attribute called original_date and current_date.
I need to make changes to those with some logic, such as:
difference = (original_date - date_entered) - self.days
original_date = date_entered + self.days
current_date = current_date - different
What I do not want to do is do an each do-loop. But I don't know if there's an alternative -- something like the "update" in SQL (but without needing to use SQL -- like using searchlogic)
Todo.update_all(["original_date = date_entered + %d, current_date = ... + %d",
self.days, self.days], ["id in (?)", todos.map(&:id)])