Using EXISTS with bound parameters in sub-query with Sequelize - node.js

Suppose I have a table of something like cars, where inside is a JSONB object specifying possible customizations:
+----+-----------------------------------------+
| id | customizations JSONB |
+----+-----------------------------------------+
| 1 | {"color": "blue", "lights": "led", ...} |
+----+-----------------------------------------+
| 2 | {"color": "red"} |
+----+-----------------------------------------+
| 3 | {} |
+----+-----------------------------------------+
If I want to query for a certain customization based on case-insensitive value or partial value (i.e., ILIKE), I can do something like:
SELECT * FROM "Cars" WHERE EXISTS (
SELECT 1 FROM JSONB_EACH_TEXT("customizations") WHERE "value" ~* 'BLU'
);
This pattern works fine in Postgres, but now I am trying to translate it over to Sequelize as best as I can. The important thing here is that the search term ('BLU' from the example) is passed as a parameter.
const cars = await cars.findAll({
where: // ???? Maybe something like db.fn('EXISTS', ...), but how do I bind the parameter?
});
How can I use an EXISTS query here, but bind a parameter? I know I could use db.literal(), but if I did that, I'd have to escape the search term string myself before interpolating it into the query. (Is there at least a proper method for doing this data escaping in Sequelize?)
Note that the JSONB object in customizations can have many keys, a single key, or even no keys.
Bounty Note: Answers using modern versions of Postgres are fine, but I would also like an answer for PostgreSQL v10.12, as that's all that is available with AWS Aurora Serverless. I'll happily assign a separate bounty to both answers!

If on postgresql 12+, it would be possible to use the json path expression to extract all values & cast to text in order to regex match
The raw query would be
SELECT *
FROM cars
WHERE jsonb_path_query_array(customizations, '$.*')::TEXT ~* 'BLU'
in sequelize:
where: Sequelize.where(
Sequelize.literal("jsonb_path_query_array(customizations, '$.*')::TEXT"),
Op.iRegexp,
'BLU'
)
On older versions, i would probably use a raw query with bound parameters that maps back to the Car object.
How can I use an EXISTS query here, but bind a parameter? I know I could use db.literal(), but if I did that, I'd have to escape the search term string myself before interpolating it into the query. (Is there at least a proper method for doing this data escaping in Sequelize?)
you can bind parameters to raw sql queries using either $1 or $name for positional (array) and named (object) arguments.
// cars.js
const Sequelize = require('sequelize');
const path = 'postgresql://hal:hal#localhost:5432/hal'
const sequelize = new Sequelize(path);
let Car = sequelize.define('cars', {
id: { type: Sequelize.INTEGER, primaryKey: true },
customizations: Sequelize.JSONB
});
let cars = [{ id: 1, customizations: {color: "blue", lights: "led"} },
{ id: 2, customizations: {color: "red"} },
{ id: 3, customizations: {} }]
const search_pat = 'BLU';
const stmt = `
select distinct cars.*
from cars, jsonb_each_text(customizations) kv(kk, vv)
where vv ~* $search_pattern
`;
sequelize
.sync()
.then(() => {
Car.bulkCreate(cars)
})
.then(() => {
sequelize.query(stmt, {
bind: {search_pattern: search_pat},
type: sequelize.QueryTypes.SELECT,
model: Car,
mapToModel: true
}).then(cars => {console.log(cars);})
});
executing this script (node cars.js) produces the following output:
Executing (default): CREATE TABLE IF NOT EXISTS "cars" ("id" INTEGER , "customizations" JSONB, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'cars' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "cars" ("id","customizations","createdAt","updatedAt") VALUES (1,'{"color":"blue","lights":"led"}','2021-04-14 04:42:50.446 +00:00','2021-04-14 04:42:50.446 +00:00'),(2,'{"color":"red"}','2021-04-14 04:42:50.446 +00:00','2021-04-14 04:42:50.446 +00:00'),(3,'{}','2021-04-14 04:42:50.446 +00:00','2021-04-14 04:42:50.446 +00:00') RETURNING "id","customizations","createdAt","updatedAt";
Executing (default): select distinct cars.*
from cars, jsonb_each_text(customizations) kv(kk, vv)
where vv ~* $1
[
cars {
dataValues: {
id: 1,
customizations: [Object],
createdAt: 2021-04-14T04:42:50.446Z,
updatedAt: 2021-04-14T04:42:50.446Z
},
_previousDataValues: {
id: 1,
customizations: [Object],
createdAt: 2021-04-14T04:42:50.446Z,
updatedAt: 2021-04-14T04:42:50.446Z
},
_changed: Set(0) {},
_options: {
isNewRecord: false,
_schema: null,
_schemaDelimiter: '',
raw: true,
attributes: undefined
},
isNewRecord: false
}
]
note that i've used an alternative (and likely less efficient select statement in the example above)
You can use your original query, which IMO is the most straight-forward query for the target postgresql version (10.x), i.e.
const stmt = `
SELECT *
FROM cars
WHERE EXISTS (
SELECT 1
FROM JSONB_EACH_TEXT(customizations) WHERE "value" ~* $search_pattern
)
`;

If you modify your condition like this:
SELECT * FROM "Cars" WHERE (
SELECT "value" FROM JSONB_EACH_TEXT("customizations")) ~* 'BLU'
then you can use Sequelize.where in a conjunction with Sequelize.literal:
where: Sequelize.where(
Sequelize.literal('(SELECT "value" FROM JSONB_EACH_TEXT("customizations"))'),
Op.iRegexp,
'BLU'
)
Upd.
This solution will work only if a subquery returns 1 record.

Related

How to condition sort on mongodb

storeId is null or ObjectId.
I want to sort like this :
if storeId is null : sort by name
if storeId is ObjectId: sort by createdAt
I'm using this code to sort, but it's not my exception :
sort: { storeId: 1, name: 1, createdAt: -1 },
Any one can help me

Rename column in sequelize findAll

I have the following findAll query:
let consumQuery = Manager.consumption.findAndCountAll({
where: query, attributes: ['ml', 'price', 'total', 'consumption_end', 'bar_id', 'sync', 'visit.device_id',
'visit.person.document','visit.person.person_id','visit.person.name','visit.person.surname',
'keg.product_id','keg.product.sku','keg.product.name'],
include: [
{
model: Manager.visit,
attributes: [],
include: [{
model: Manager.person,
attributes: [],
raw: true
}],
},
{
model: Manager.keg,
attributes: [],
include: [
{
model: Manager.product,
required: true,
attributes: []
}
]
}],
limit: PAGE_LIMIT_SIZE,
offset: offset,
raw: true,
order: [['consumption_start', 'ASC']]
}).then(({count, rows}) => {
I'm trying to rename the column keg.product.name to beer.
I've tried :
...'keg.product_id','keg.product.sku',['keg.product.name','beer']
But I got the error:
"column \"keg.product.name\" does not exist"
The sequelize output the following SQL:
sql: `SELECT "consumption"."ml", "consumption"."price", "consumption"."total", "consumption"."consumption_end", "consumption"."bar_id", "consumption"."sync", "visit"."device_id", "visit.person"."document", "visit.person"."person_id", "visit.person"."name", "visit.person"."surname", "keg"."product_id", "keg.product"."sku", "keg.product.name" AS "beer" FROM "public"."consumption" AS "consumption" LEFT OUTER JOIN "public"."visit" AS "visit" ON "consumption"."visit_id" = "visit"."visit_id" LEFT OUTER JOIN "public"."person" AS "visit.person" ON "visit"."person_id" = "visit.person"."person_id" LEFT OUTER JOIN "public"."keg" AS "keg" ON "consumption"."keg_id" = "keg"."keg_id" INNER JOIN "public"."product" AS "keg.product" ON "keg"."product_id" = "keg.product"."product_id" WHERE ("consumption"."bar_id" = '248' AND "consumption"."sync" = 'true' AND "consumption"."consumption_start" >= '2020-02-16 20:03:32.000 -03:00' AND "consumption"."consumption_end" <= '2020-09-17 20:03:32.000 -03:00') ORDER BY "consumption"."consumption_start" ASC LIMIT 500 OFFSET 0;},
The result is "keg.product.name" AS "beer", but I think should be something like that: "keg.product"."name" AS "beer"
Any ideia?
To rename an attribute you have to make it an array like [name_in_db, name_you_want].
Therefore your attributes value should be as it follows
attributes: ['ml', 'price', 'total', 'consumption_end', 'bar_id', 'sync', 'visit.device_id', visit.person.document', 'visit.person.person_id', 'visit.person.name', 'visit.person.surname', keg.product_id', 'keg.product.sku', **['keg.product.name', 'beer']**]
Source: https://sequelize.org/api/v6/class/src/model.js~model#static-method-findAll
"To rename an attribute, you can pass an array, with two elements - the first is the name of the attribute in the DB [...], and the second is the name you want the attribute to have in the returned instance"

how to using a db function in where sentence via sequelize

I want to find an average value of this week and group by dayofweek, so I writen this sql, the sql is work well, but how can I achieve the same purpose via sequelize.
select dayofweek(datetime), avg(value) from Temperatures where week(datetime) = week(now()) group by dayofweek(datetime);
And here is my Temperatures table:
| field | type |
|----------|-------------|
| uuid | char(36) |
| value | float |
| datetime | datetime |
I've read the document of sequelize, but it seems that there is no clear explanation of how to using a function after where sentence, the condition is a function but not a field.
I have tried under way, but it's not work
where: {
[Sequelize.Op.eq]: [
sequelize.fn('week', Temperature.sequelize.fn('now')),
sequelize.fn('week', Temperature.sequelize.col('datetime')),
]
},
Thank your for your help!
You can use sequelize.fn for that , something like this :
[sequelize.fn('dayofweek', sequelize.col('datetime')), 'datetime']
For more details DO READ
Finally, I found the following method works well
return Temperature.findAll({
attributes: [
[Temperature.sequelize.fn('dayofweek', Temperature.sequelize.col('datetime')), 'dayofweek'],
[Temperature.sequelize.fn('AVG', Temperature.sequelize.col('value')), 'value'],
],
where: sequelize.where(sequelize.fn('week', Temperature.sequelize.fn('now')), sequelize.fn('week', Temperature.sequelize.col('datetime'))),
group: sequelize.fn('dayofweek', Temperature.sequelize.col('datetime')),
});
and if I need more conditional filters, just add an [Op.and] operator
return Temperature.findAll({
attributes: [
[Temperature.sequelize.fn('dayofweek', Temperature.sequelize.col('datetime')), 'dayofweek'],
[Temperature.sequelize.fn('AVG', Temperature.sequelize.col('value')), 'value'],
],
where: {
[Sequelize.Op.and]: [
sequelize.where(sequelize.fn('week', Temperature.sequelize.fn('now')), sequelize.fn('week', Temperature.sequelize.col('datetime'))),
]
},
group: sequelize.fn('dayofweek', Temperature.sequelize.col('datetime')),
});
here is the sql generated
SELECT dayofweek(`datetime`) AS `dayofweek`, AVG(`value`) AS `value` FROM `Temperatures` AS `Temperature` WHERE (week(now())=week(`datetime`)) GROUP BY dayofweek(`datetime`);

Getting null while trying to traverse to depth 2 of graphql type using neo4j pattern comprehension in nodejs environment

I am using neo4j(3.1), GraphQL and Nodejs. For now, I have 3 graphql types namely Country, State and City and these types have following relation in Neo4j dB.
(City)-[:PRESENT_IN]->(State)-[:PRESENT_IN]->(Country)
My graphql Type is like below:
Type Country{
name: String,
states: [State]
}
Type State{
name: String,
cities: [City]
}
Type City{
name: String,
id: Int
}
And my Query schema is:
type Query{
countries(limit: Int): [Country]
states(limit: Int): [State]
}
So , when I ran the query "countries" in GraphiQl, I was hoping to get the cities too available in a particular state present in one country i.e. traversing to depth 2 , but I am getting null when it comes to array of "cities" field,
Although the data comes for field "states".
Here is the query execution done in GraphiQL:
countries(limit:1) {
name
states{
name
cities{
name
id
}
}
}
and Here is the execution result:
"countries": {
"name": "Country 1",
"states": [
{
"name": "State A",
"cities": null ---> Here it is coming null
},
]
}
What I wanted to return was:
"countries": {
"name": "Country 1",
"states": [
{
"name": "State A",
"cities": [
{
name: City 1
id: 1
},
]
},
]
}
For my cypher query, I have used pattern comprehension as in this link: https://neo4j.com/blog/cypher-graphql-neo4j-3-1-preview/
My cypher query is:
// For Query "countries"
MATCH (c:Country)
RETURN c{
.name,
states: [(s:State)-[:PRESENT_IN]->(c) | s{.*}]
} LIMIT $limit
//For Query "states"
MATCH (s:State)
RETURN s{
.name,
cities: [(ct:City)-[:PRESENT_IN]->(s) | ct{.*}]
} LIMIT $limit
So, could anyone tell me what I am doing wrong here ? I looked into the neo4j docs and other blogs, but I am unable to figure it out. Any help or insight would be really helpful !
AFAIK, you have to query for those cities in the countries query. Something like this:
MATCH (c:Country)
RETURN c {
.name,
states: [(s:State)-[:PRESENT_IN]->(c) | s {
.*,
cities: [(ct:City)-[:PRESENT_IN]->(s) | ct {.*}]
}]
} LIMIT $limit

where clause on include column using sequilize

I'm using this query:
db.Spectrum.findAll({
attributes: ['title', [db.Sequelize.fn('SUM', db.Sequelize.col('quantity') ), 'count']],
include: [
{model: db.Varietal,
attributes : ['specID', 'id'],
include : [
{ model : db.Wine,
required: false,
attributes : ['varietal_id', 'id'],
where : {restaurant_id : restaurant_id,
owner_id : null} }
],
}
],
group : ['specID']
})
Which gives me the correct results, the problem is because it ands the where clause to the left join it runs really slow (about 2 seconds).
The raw query of the left join looks like this.
LEFT OUTER JOIN `wines` AS `Varietals.Wines` ON `Varietals`.`id` = `Varietals.Wines`.`varietal_id` AND `Varietals.Wines`.`restaurant_id` = 16 AND `Varietals.Wines`.`owner_id` IS NULL
I would like the query to have the left join and a where clause like this..
LEFT OUTER JOIN `wines` AS `Varietals.Wines` ON `Varietals`.`id` = `Varietals.Wines`.`varietal_id`
WHERE `Varietals.Wines`.`restaurant_id` = 16 AND `Varietals.Wines`.`owner_id` IS NULL
But everything I try puts 'Spectrum' before it and then doesn't find the row. How is this suppose to be done? Or am I going to have to just use a raw query?
include.where will always add the condition to the join.
However in Sequelize v3.13.0 support for special nested keys were added.
It's now possible to add a where object to the toplevel object containing dot seperated keys wrapped in $ to specify it being a nested field.
Applying it to your code:
db.Spectrum.findAll({
include: [
{
model: db.Varietal,
include : [
{
model : db.Wine
}
]
}
],
where: {
'$Varietals.Wines.restaurant_id$': restaurant_id,
'$Varietals.Wines.owner_id$': null
},
group : ['specID']
});
(I removed a few statements to focus on the important part)
Include array
include = [{
model: db.caregiverCategory,
as: 'categories',
attributes: ['name'],
required: true,
attributes: ['name'],
include: [{
model: db.category,
as: 'category',
required: true
}]
}]
Where Clause
where[Op.or] = [{
'$categories.name$': { [Op.like]: '%' + query.category_name + '%' }
}, {
'$categories.category.name$': { [Op.like]: '%' + query.category_name + '%' }
}]
You need to add all fields like name (which are required in or) in attributes: ['name'] otherwise it will give unknowm columns error.

Resources