How to select DB Expression as value using knex/Bookshelf - mysql

I'm trying to execute the following query using knex.js and MySql
SELECT
m.id,
TIME(date_created) AS `timestamp`,
u.username,
m.`message`
FROM
`messages` AS m
INNER JOIN users AS u ON u.id = m.user_id
WHERE
m.game_id IS NULL
AND m.`date_created` > DATE_SUB(
CURRENT_TIMESTAMP (),
INTERVAL 12 HOUR
)
ORDER BY
m.`date_created` ASC
LIMIT 50
For proper handling expressions in where closure such as DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 12 HOUR) according to documentation there is whereRow() method in knex.
I tried to use select() method as
select('messages.id', 'TIME(date_created) AS timestamp', 'users.username', 'messages.message')
But knex masks TIME(date_created) expression in a way it should to be a column name. Does anybody know a way to use a custom expressions in selects?

I did not found exact answer on my question but I've found a solution which allows me to move forward. I created separate model which uses standard Bookshelf(knex) export:
var Bookshelf = require('bookshelf')(knex);
module.exports.DB = Bookshelf;
And created separate method in that model where I could use DB.knex.raw() for masking DB expressions in SELECT. So I became able to write the query above in the following way:
var DB = require('./../db').DB;
var Messages = DB.Model.extend({
tableName: 'messages',
hasTimestamps: false,
idAttribute: 'id'
},
{
getMessagesHistory: function (gameId) {
return this.query().select('messages.id', DB.knex.raw('TIME(date_created) AS timestamp'), 'users.username', 'messages.message')
.innerJoin('users', 'messages.user_id', 'users.id')
.whereRaw("messages.game_id " + (gameId == 0 ? "IS NULL" : "= " + gameId))
.andWhereRaw("messages.date_created > DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 12 HOUR)")
.orderBy('messages.date_created', 'ASC')
.limit(50);
}
}
);
module.exports = Messages;

You can wrap any argument with knex.raw() to tell the framework that it's a raw piece of SQL:
select(
'messages.id',
knex.raw('TIME(date_created) AS timestamp'),
'users.username',
'messages.message',
)
See http://knexjs.org/#Raw-Bindings

Related

nested queries in laravel

Here is my sql, i am struggling to translate this to laravel 5.8 query builder syntax.
SELECT CC AS CountOfVisits, Count(CC) AS Users
FROM
(
SELECT user_id, count(user_id) AS CC FROM mytable
GROUP BY user_id
) AS CC
GROUP BY CC;
I tried this:
$frequency= DB::connection('mysql2')
->select(DB::raw('CC AS CountOfVisits'), DB::raw('Count(CC) AS Users'))
->from(function($sq)
{
$sq->select(DB::raw('user_id, count(user_id) AS CC FROM '))
->from('mytable')
->groupBy('user_id');
})
->groupBy('CC')
->get();
it errors with
Argument 1 passed to Illuminate\Database\Connection::prepareBindings()
must be of the type array, object given, called in
/var/app/portal/vendor/laravel/framework/src/Illuminate/Database/Connection.php
on line 665
Any help would be great thanks
I think the problem you're having is that the returned connection is expecting string for the full query with additional bindings on the second parameter. You can still build it using illuminate's query builder first if you like, then run the select using the generated string.
$query = DB::query()
->select('CC AS CountOfVisits', DB::raw('count(CC) AS Users'))
->fromSub(function (\Illuminate\Database\Query\Builder $query) {
$query->select('user_id', DB::raw('count(user_id) AS CC '))
->from('mytable')
->groupBy('user_id');
}, 'CC')
->groupBy('CC');
$result = DB::connection()->select($query->toSql());

Illuminate database query with date_sub

I'm struggling with a query using the Illuminate database query builder.
When I use the query the result is not as I expected.
When using the query from the querylog directly with mysql cli, I get the expected result.
With query builder:
->table('CompanyTools')
->select(
'CompanyTools.toolId',
$db->raw('COUNT(CompanyTools.toolId) as count')
)
->whereYear('CompanyTools.date', '>', 'YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))')
->groupBy('CompanyTools.toolId')
->orderBy('count', 'DESC')
->take(1)
->get();
Result:
Array ( [toolId] => 88 [count] => 55 )
With mysql cli:
select `CompanyTools`.`toolId`, COUNT(CompanyTools.toolId) as count from `CompanyTools`
where year(`CompanyTools`.`date`) > YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))
group by `CompanyTools`.`toolId`
order by `count` desc
limit 1
Result:
ToolId: 88
count: 17
If I (in the query builder) replace 'YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))'with 2013 I get:
Array ( [toolId] => 88 [count] => 17 )
Somehow the date_sub get ignored so the result includes all years
I tried with ->whereYear('CompanyTools.date', '>', $db->raw('YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))')) without any luck.
I guess I could use php to calculate the desired year, but I would rather get the query right.
Thx in advance
/ j
UPDATE
Replacing
->whereYear('CompanyTools.date', '>', 'YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))')
with
->where($db->raw('YEAR(CompanyTools.date)'), '>', $db->raw('YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))'))
solves it. Not clever enough to figure out why, but perhaps the whereYear function is supposed to be used diffently
As you already found out using
->where($db->raw('YEAR(CompanyTools.date)'), '>', $db->raw('YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))'))
Or alternatively
->whereRaw('YEAR(CompanyTools.date) > YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))')
solves the problem.
But why is that?
For every "normal" query, Laravel uses bindings. Obviously SQL functions like YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR)) don't work with bindings.
Normally, you can use DB::raw('YEAR(DATE_SUB(CURDATE(), INTERVAL 1 YEAR))') and the Laravel won't use bindings. For example in where() (Expression is the class DB::raw() returns)
if ( ! $value instanceof Expression)
{
$this->addBinding($value, 'where');
}
But the whereYear() function doesn't do such a thing. It uses addDateBasedWhere() and just adds a binding without checking if the value is an instance of Expression
protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and')
{
$this->wheres[] = compact('column', 'type', 'boolean', 'operator', 'value');
$this->addBinding($value, 'where');
return $this;
}
This means the query will use bindings and therefore NOT execute the date calculation at all.

SQLAlchemy MySQL IF Statement

I'm in the middle of converting an old legacy PHP system to Flask + SQLAlchemy and was wondering how I would construct the following:
I have a model:
class Invoice(db.Model):
paidtodate = db.Column(DECIMAL(10,2))
fullinvoiceamount = db.Column(DECIMAL(10,2))
invoiceamount = db.Column(DECIMAL(10,2))
invoicetype = db.Column(db.String(10))
acis_cost = db.Column(DECIMAL(10,2))
The query I need to run is:
SELECT COUNT(*) AS the_count, sum(if(paidtodate>0,paidtodate,if(invoicetype='CPCN' or invoicetype='CPON' or invoicetype='CBCN' or invoicetype='CBON' or invoicetype='CPUB' or invoicetype='CPGU' or invoicetype='CPSO',invoiceamount,
fullinvoiceamount))) AS amount,
SUM(acis_cost) AS cost, (SUM(if(paidtodate>0,paidtodate,invoiceamount))-SUM(acis_cost)) AS profit FROM tblclientinvoices
Is there an SQLAlchemyish way to construct this query? - I've tried googling for Mysql IF statments with SQlAlchemy but drew blanks.
Many thanks!
Use func(documentation) to generate SQL function expression:
qry = select([
func.count().label("the_count"),
func.sum(func.IF(
Invoice.paidtodate>0,
Invoice.paidtodate,
# #note: I prefer using IN instead of multiple OR statements
func.IF(Invoice.invoicetype.in_(
("CPCN", "CPON", "CBCN", "CBON", "CPUB", "CPGU", "CPSO",)
),
Invoice.invoiceamount,
Invoice.fullinvoiceamount)
)
).label("amount"),
func.sum(Invoice.acis_cost).label("Cost"),
(func.sum(func.IF(
Invoice.paidtodate>0,
Invoice.paidtodate,
Invoice.invoiceamount
))
- func.sum(Invoice.acis_cost)
).label("Profit"),
],
)
rows = session.query(qry).all()
for row in rows:
print row

how to translate a very long mysql query with select and join to zend framework 1.11 model

I have this mysql query:
SELECT
freeAnswers.*,
(SELECT `districtCode`
FROM `geodatas`
WHERE `zipCode` = clients.zipCode
GROUP BY `zipCode`
LIMIT 0, 1) as districtCode,
clients.zipCode,
clients.gender,
clients.startAge,
clients.endAge,
clients.mail,
clients.facebook,
surveys.customerId,
surveys.activityId,
surveys.name as surveyName,
customers.companyName,
activities.name as activityName
FROM freeAnswers,
clients,
surveys,
customers,
activities
WHERE freeAnswers.surveyId = surveys.id
AND surveys.customerId = customers.id
AND activities.id = surveys.activityId
AND clients.id = freeAnswers.clientId
AND customers.id = 1
ORDER BY activityName asc
LIMIT 0, 10
the query is correct on my mysql server but when I try to use it in Zend Framework 1.11 model
I get this error: Mysqli prepare error: Operand should contain 1 column(s)
Please, could anyone help me to make it run well?
Best Regards,
Elaidon
Here is some code that should work. Zend_Db_Select doesn't really provide a way to select from multiple tables in the FROM clause without using a JOIN so this feels a bit hackish to me in regards to one small part of the query. Your best bet will probably be to rewrite the query using JOINs where appropriate.
$subselect = $db->select()
->from('geodatas', 'districtCode')
->where('zipCode = clients.zipCode')
->group('zipCode')
->limit(1, 0);
$from = $db->quoteIdentifier('freeAnswers') . ', ' .
$db->quoteIdentifier('clients') . ', ' .
$db->quoteIdentifier('surveys') . ', ' .
$db->quoteIdentifier('customers') . ', ' .
$db->quoteIdentifier('activities');
$select = $db->select()
->from(array('activities' => new Zend_Db_Expr($from)),
array('freeanswers.*',
'districtCode' =>
new Zend_Db_Expr('(' . $subselect . ')'),
'clients.zipCode', 'clients.gender', 'clients.startAge',
'clients.endAge', 'clients.mail', 'clients.facebook',
'clients.customerId', 'clients.activityId',
'surveyName' => 'surveys.name', 'customers.companyName',
'activityName' => 'activities.name'))
->where('freeAnswers.surveyId = surveys.id')
->where('surveys.customerId = customers.id')
->where('activities.id = surveys.activityId')
->where('clients.id = freeAnswers.clientId')
->where('customers.id = ?', 1)
->order('activityName ASC')
->limit(10, 0);
The only reason I say it is hackish is because of the line:
->from(array('activities' => new Zend_Db_Expr($from)),
Since from() really only works with one table, I create a Zend_Db_Expr and specify the correlation as the last table name in the expression. If you don't pass a Zend_Db_Expr, it will either quote your comma separated table name incorrectly, or if you pass an array of table names, it just uses the first. When you pass a Zend_Db_Expr with no name, it defaults to use AS t which also doesn't work in your case. That is why I put it as is.
That returns the exact SQL you provided except for the last thing mentioned. Here is actually what it returns:
SELECT
`freeanswers`.*,
(SELECT `geodatas`.`districtCode`
FROM `geodatas`
WHERE (zipCode = clients.zipCode)
GROUP BY `zipCode`
LIMIT 1) AS `districtCode`,
`clients`.`zipCode`,
`clients`.`gender`,
`clients`.`startAge`,
`clients`.`endAge`,
`clients`.`mail`,
`clients`.`facebook`,
`clients`.`customerId`,
`clients`.`activityId`,
`surveys`.`name` AS `surveyName`,
`customers`.`companyName`,
`activities`.`name` AS `activityName`
FROM `freeAnswers`,
`clients`,
`surveys`,
`customers`,
`activities` AS `activities`
WHERE (freeAnswers.surveyId = surveys.id)
AND (surveys.customerId = customers.id)
AND (activities.id = surveys.activityId)
AND (clients.id = freeAnswers.clientId)
AND (customers.id = 1)
ORDER BY `activityName` ASC
LIMIT 10
So that will work but eventually you will want to rewrite it using JOIN instead of specifying most of the WHERE clauses.
When dealing with subqueries and Zend_Db_Select, I find it easy to write each subquery as their own queries before writing the final query, and just insert the subqueries where they need to go and Zend_Db handles the rest.
Hope that helps.

Simplify sql query to obtain one line per id

I have a multi-table SQL query.
My need is: The query should I generate a single line by 'etablissement_id' ... and all information that I want to be back in the same query.
The problem is that this query is currently on a table where "establishment" may have "multiple photos" and suddenly, my query I currently generates several lines for the same id...
I want the following statement - LEFT JOINetablissementContenuMultimediaON etablissement.etablissement_id = etablissementContenuMultimedia.etablissementContenuMultimedia_etablissementId - only a single multimedia content is displayed. Is it possible to do this in the query below?
Here is the generated query.
SELECT DISTINCT `etablissement`. * , `etablissementContenuMultimedia`. * , `misEnAvant`. * , `quartier`. *
FROM `etablissement`
LEFT JOIN `etablissementContenuMultimedia` ON etablissement.etablissement_id = etablissementContenuMultimedia.etablissementContenuMultimedia_etablissementId
LEFT JOIN `misEnAvant` ON misEnAvant.misEnAvant_etablissementId = etablissement.etablissement_id
LEFT JOIN `quartier` ON quartier_id = etablissement_quartierId
WHERE (
misEnAvant_typeMisEnAvantId =1
AND (
misEnAvant_dateDebut <= CURRENT_DATE
AND CURRENT_DATE <= misEnAvant_dateFin
)
)
AND (
etablissement_isActive =1
)
ORDER BY `etablissement`.`etablissement_id` ASC
LIMIT 0 , 30
Here is the code used ZF
public function find (){
$db = Zend_Db_Table::getDefaultAdapter();
$oSelect = $db->select();
$oSelect->distinct()
->from('etablissement')
->joinLeft('etablissementContenuMultimedia', 'etablissement.etablissement_id = etablissementContenuMultimedia.etablissementContenuMultimedia_etablissementId')
->joinLeft('misEnAvant', 'misEnAvant.misEnAvant_etablissementId = etablissement.etablissement_id')
->joinLeft('quartier', 'quartier_id = etablissement_quartierId ')
->where ('misEnAvant_typeMisEnAvantId = 1 AND (misEnAvant_dateDebut <= CURRENT_DATE AND CURRENT_DATE <= misEnAvant_dateFin) ')
->where ('etablissement_isActive = 1')
->order(new Zend_Db_Expr('RAND()'));
$zSql = $oSelect->__toString();
if(isset($_GET['debug']) AND $_GET['debug'] == 1)
echo $zSql ;
//die();
$oResultEtablissement = $db->fetchAll($oSelect);
return $oResultEtablissement ;
}
Can you help me?
Sincerely,
If you are looking to have only one of the media displayed out of many regardless of which it may be then you can just add a limit to the query? After that you can tweak the query for ASCending or DESCending perhaps?
Is this query supposed to have images (or image as it were) for one establishment, or one image each for each active establishment? I see you have a limit 0,30 which means you're likely paginating....
If the result you want is a search for only one establishment, and the first image it comes to would work fine .. just use "limit 1" and you'll only get one result.
I took the time to redo the whole model of the database ... and now it works. There was no solution for a system as flawed