How to do `UPDATE...SET...FROM` using knex? - sql

What's the most terse way I can express an UPDATE...SET...FROM SQL statement using knex? This is what I've got currently:
const query =
knex('user_subscriptions').update(subscription).toQuery() +
knex.raw(
' from plans p where customer_id = ? and p.id = us.plan_id ' +
'returning us.*, p.name',
[customer_id]
);
The reason I'm doing this is that I want to efficiently return a field from a related table (JOIN style) without needing a separate query.

As instructed in the official site: knexjs.org/#Builder-update
knex('user_subscriptions')
.returning(['us.*', 'plans.name', 'customer_id'])
.where({
customer_id: '?',
plans.id: us.plan_id
})
.update({
subscription : '?
})
Does:
update `user_subscriptions` set `subscription ` = '?' where `customer_id` = '?' and 'plans.id' = 'us.plan_id'
Returns:
[ us.*: ..., plans.name: ..., customer_id: ... ]

Related

How to count number of user id in a list without duplicate in sequelize [duplicate]

I am trying to get a distinct count of a particular column using sequelize. My initial attempt is using the 'count' method of my model, however it doesn't look like this is possible.
The DISTINCT feature is needed because I am joining other tables and filtering the rows of the parent based on the related tables.
here's the query I would like:
SELECT COUNT(DISTINCT Product.id) as `count`
FROM `Product`
LEFT OUTER JOIN `Vendor` AS `vendor` ON `vendor`.`id` = `Product`.`vendorId`
WHERE (`vendor`.`isEnabled`=true );
using the following query against my Product model:
Product.count({
include: [{model: models.Vendor, as: 'vendor'}],
where: [{ 'vendor.isEnabled' : true }]
})
Generates the following query:
SELECT COUNT(*) as `count`
FROM `Product`
LEFT OUTER JOIN `Vendor` AS `vendor` ON `vendor`.`id` = `Product`.`vendorId`
WHERE (`vendor`.`isEnabled`=true );
UPDATE: New version
There are now separate distinct and col options. The docs for distinct state:
Apply COUNT(DISTINCT(col)) on primary key or on options.col.
You want something along the lines of:
MyModel.count({
include: ...,
where: ...,
distinct: true,
col: 'Product.id'
})
.then(function(count) {
// count is an integer
});
Original Post
(As mentioned in the comments, things have changed since my original post, so you probably want to ignore this part.)
After looking at Model.count method in lib/model.js, and tracing some code, I found that when using Model.count, you can just add any kind of aggregate function arguments supported by MYSQL to your options object. The following code will give you the amount of different values in MyModel's someColumn:
MyModel.count({distinct: 'someColumn', where: {...}})
.then(function(count) {
// count is an integer
});
That code effectively generates a query of this kind: SELECT COUNT(args) FROM MyModel WHERE ..., where args are all properties in the options object that are not reserved (such as DISTINCT, LIMIT and so on).
The Sequelize documentation on count links to a count method that doesn't let you specify which column to get the count of distinct values:
Model.prototype.count = function(options) {
options = Utils._.clone(options || {});
conformOptions(options, this);
Model.$injectScope(this.$scope, options);
var col = '*';
if (options.include) {
col = this.name + '.' + this.primaryKeyField;
expandIncludeAll.call(this, options);
validateIncludedElements.call(this, options);
}
Utils.mapOptionFieldNames(options, this);
options.plain = options.group ? false : true;
options.dataType = new DataTypes.INTEGER();
options.includeIgnoreAttributes = false;
options.limit = null;
options.offset = null;
options.order = null;
return this.aggregate(col, 'count', options);
};
Basically SELECT COUNT(DISTINCT(*)) or SELECT COUNT(DISTINCT(primaryKey)) if you've got a primary key defined.
To do the Sequelize equivalent of SELECT category, COUNT(DISTINCT(product)) as 'countOfProducts' GROUP BY category, you'd do:
model.findAll({
attributes: [
'category',
[Sequelize.literal('COUNT(DISTINCT(product))'), 'countOfProducts']
],
group: 'category'
})
Looks like this is now supported in Sequelize versions 1.7.0+.
the count and findAndCountAll methods of a model will give you 'real' or 'distinct' count of your parent model.
I was searching for SELECT COUNT(0) query for sequelize, below is the answer for that.
let existingUsers = await Users.count({
where: whereClouser,
attributes: [[sequelize.fn('COUNT', 0), 'count']]
});
This helped me to get distinct count from another table rows,
dataModel.findAll({
attributes: {
include: [[Sequelize.literal("COUNT(DISTINCT(history.data_id))"), "historyModelCount"]]
},
include: [{
model: historyModel, attributes: []
}],
group: ['data.id']
});
Ref 1, Ref 2.
With respect to your question in order to get the distinct counts of products based on the id of product
you just need to pass the key 'distinct' with value 'id' to your count object , Here is the example
To generate this sql query as you asked
SELECT COUNT(DISTINCT(`Product`.`id`)) as `count`
FROM `Product`
LEFT OUTER JOIN `Vendor` AS `vendor` ON `vendor`.`id` = `Product`.`vendorId`
WHERE (`vendor`.`isEnabled`=true );
Add 'distinct' key in your Sequelize query
Product.count({
include: [{model: models.Vendor, as: 'vendor'}],
where: [{ 'vendor.isEnabled' : true }],
distinct: 'id' // since count is applied on Product model and distinct is directly passed to its object so Product.id will be selected
});
This way of using 'distinct' key to filter out distinct counts or rows , I tested in Sequelize Version 6.
Hope this will help you or somebody else!

Can Laravel automatically switch between column = ? and column IS NULL depending on value?

When building a complex SQL query for Laravel, using ? as placeholders for parameters is great. However when the value is null, the SQL syntax needs to be changed from = ? to IS NULL. Plus, since the number of parameters is one less, I need to pass a different array.
To get it to work, I have written it like this, but there must be a better way:
if ($cohortId === null) {
// sql should be: column IS NULL
$sqlCohortString = "IS NULL";
$params = [
Carbon::today()->subDays(90),
// no cohort id here
];
} else {
// sql should be: column = ?
$sqlCohortString = "= ?";
$params = [
Carbon::today()->subDays(90),
$cohortId
];
}
$query = "SELECT items.`name`,
snapshots.`value`,
snapshots.`taken_at`,
FROM snapshots
INNER JOIN (
SELECT MAX(id) AS id, item_id
FROM snapshots
WHERE `taken_at` > ?
AND snapshots.`cohort_id` $sqlCohortString
GROUP BY item_id
) latest
ON latest.`id` = snapshots.`id`
INNER JOIN items
ON items.`id` = snapshots.`item_id`
ORDER by media_items.`slug` ASC
";
$chartData = DB::select($query, $params);
My question is: does Laravel have a way to detect null values and replace ? more intelligently?
PS: The SQL is for a chart, so I need the single highest snapshot value for each item.
You can use ->when to create a conditional where clause:
$data = DB::table('table')
->when($cohortId === null, function ($query) {
return $query->whereNull('cohort_id');
}, function ($query) use ($cohortId) {
// the "use" keyword provides access to "outer" variables
return $query->where('cohort_id', '=', $cohortId);
})
->where('taken_at', '>', $someDate)
->toSql();

Upsert in KnexJS

I have an upsert query in PostgreSQL like:
INSERT INTO table
(id, name)
values
(1, 'Gabbar')
ON CONFLICT (id) DO UPDATE SET
name = 'Gabbar'
WHERE
table.id = 1
I need to use knex to this upsert query. How to go about this?
So I solved this using the following suggestion from Dotnil's answer on Knex Issues Page:
var data = {id: 1, name: 'Gabbar'};
var insert = knex('table').insert(data);
var dataClone = {id: 1, name: 'Gabbar'};
delete dataClone.id;
var update = knex('table').update(dataClone).whereRaw('table.id = ' + data.id);
var query = `${ insert.toString() } ON CONFLICT (id) DO UPDATE SET ${ update.toString().replace(/^update\s.*\sset\s/i, '') }`;
return knex.raw(query)
.then(function(dbRes){
// stuff
});
Hope this helps someone.
As of knex#v0.21.10+ a new method onConflict was introduced.
Official documentation says:
Implemented for the PostgreSQL, MySQL, and SQLite databases. A
modifier for insert queries that specifies alternative behaviour in
the case of a conflict. A conflict occurs when a table has a PRIMARY
KEY or a UNIQUE index on a column (or a composite index on a set of
columns) and a row being inserted has the same value as a row which
already exists in the table in those column(s). The default behaviour
in case of conflict is to raise an error and abort the query. Using
this method you can change this behaviour to either silently ignore
the error by using .onConflict().ignore() or to update the existing
row with new data (perform an "UPSERT") by using
.onConflict().merge().
So in your case, the implementation would be:
knex('table')
.insert({
id: id,
name: name
})
.onConflict('id')
.merge()
I've created a function for doing this and described it on the knex github issues page (along with some of the gotchas for dealing with composite unique indices).
const upsert = (params) => {
const {table, object, constraint} = params;
const insert = knex(table).insert(object);
const update = knex.queryBuilder().update(object);
return knex.raw(`? ON CONFLICT ${constraint} DO ? returning *`, [insert, update]).get('rows').get(0);
};
Example usage:
const objToUpsert = {a:1, b:2, c:3}
upsert({
table: 'test',
object: objToUpsert,
constraint: '(a, b)',
})
A note about composite nullable indices
If you have a composite index (a,b) and b is nullable, then values (1, NULL) and (1, NULL) are considered mutually unique by Postgres (I don't get it either).
Yet another approach I could think of!
exports.upsert = (t, tableName, columnsToRetain, conflictOn) => {
const insert = knex(tableName)
.insert(t)
.toString();
const update = knex(tableName)
.update(t)
.toString();
const keepValues = columnsToRetain.map((c) => `"${c}"=${tableName}."${c}"`).join(',');
const conflictColumns = conflictOn.map((c) => `"${c.toString()}"`).join(',');
let insertOrUpdateQuery = `${insert} ON CONFLICT( ${conflictColumns}) DO ${update}`;
insertOrUpdateQuery = keepValues ? `${insertOrUpdateQuery}, ${keepValues}` : insertOrUpdateQuery;
insertOrUpdateQuery = insertOrUpdateQuery.replace(`update "${tableName}"`, 'update');
insertOrUpdateQuery = insertOrUpdateQuery.replace(`"${tableName}"`, tableName);
return Promise.resolve(knex.raw(insertOrUpdateQuery));
};
very simple.
Adding onto Dorad's answer, you can choose specific columns to upsert using merge keyword.
knex('table')
.insert({
id: id,
name: name
})
.onConflict('id')
.merge(['name']); // put column names inside an array which you want to merge.

Node-postgres: named parameters query (nodejs)

I used to name my parameters in my SQL query when preparing it for practical reasons like in php with PDO.
So can I use named parameters with node-postgres module?
For now, I saw many examples and docs on internet showing queries like so:
client.query("SELECT * FROM foo WHERE id = $1 AND color = $2", [22, 'blue']);
But is this also correct?
client.query("SELECT * FROM foo WHERE id = :id AND color = :color", {id: 22, color: 'blue'});
or this
client.query("SELECT * FROM foo WHERE id = ? AND color = ?", [22, 'blue']);
I'm asking this because of the numbered parameter $n that doesn't help me in the case of queries built dynamically.
There is a library for what you are trying to do. Here's how:
var sql = require('yesql').pg
client.query(sql("SELECT * FROM foo WHERE id = :id AND color = :color")({id: 22, color: 'blue'}));
QueryConvert to the rescue. It will take a parameterized sql string and an object and converts it to pg conforming query config.
type QueryReducerArray = [string, any[], number];
export function queryConvert(parameterizedSql: string, params: Dict<any>) {
const [text, values] = Object.entries(params).reduce(
([sql, array, index], [key, value]) => [sql.replace(`:${key}`, `$${index}`), [...array, value], index + 1] as QueryReducerArray,
[parameterizedSql, [], 1] as QueryReducerArray
);
return { text, values };
}
Usage would be as follows:
client.query(queryConvert("SELECT * FROM foo WHERE id = :id AND color = :color", {id: 22, color: 'blue'}));
Not exactly what the OP is asking for. But you could also use:
import SQL from 'sql-template-strings';
client.query(SQL`SELECT * FROM unicorn WHERE color = ${colorName}`)
It uses tag functions in combination with template literals to embed the values
I have been working with nodejs and postgres. I usually execute queries like this:
client.query("DELETE FROM vehiculo WHERE vehiculo_id= $1", [id], function (err, result){ //Delete a record in de db
if(err){
client.end();//Close de data base conection
//Error code here
}
else{
client.end();
//Some code here
}
});

(Perl) Create query for DBI with SQL::Abstract

I want to add new record to table1 on SQLite
use SQL::Abstract;
my %data = (
id => \'max(id)', # it is doesn't work so which variant is right?
record => 'Something'
);
my $sql = SQL::Abstract->new;
my ($stmt, #bind) = $sql->insert('table1', \%data);
...
my $sth = $dbh->prepare($stmt);
If I used DBIx::Class in Catalyst app I would written like so:
id => $c->model('Model')->get_column('id')->max()
and it will work fine.
So how can I reach the same aim but using just SQL::Abstract which is used in DBIx::Class as well.
Could someone fixed it? Thanks.
This is a piece of code. As you can see, first you need to get the max id+1 and then do the insert command. I have to notice you this is not safe, because in a multi-(user,process,thread) environment, a second process can execute the same code and get race conditions.
But I assume you are just learning the SQL::Abstract api, and that problem doesn't matter
use DBI;
use SQL::Abstract;
#create table TEST(ID integer, NAME varchar);
my $dbh = DBI->connect('dbi:SQLite:dbname=test.db', '', '', {AutoCommit=>1});
my $sql = SQL::Abstract->new;
my($stmt, #bind) = $sql->select("TEST", [ 'max(ID)+1 as ID' ] );
my $sth = $dbh->prepare($stmt);
$sth->execute(#bind);
my ($id) = $sth->fetchrow_array // 1;
print "Select ID: $id", "\n";
$sth->finish;
($stmt, #bind) = $sql->insert("TEST", { ID=>$id, NAME=>"test-name"} );
$sth = $dbh->prepare($stmt);
$sth->execute(#bind);
$dbh->disconnect;