Using upsert in postgres with a json file in nodejs - sql

I have a large json file of data which I want to put into my database. Some of the object are duplicates, so I want to update the data in case the row is already in the database. Here is my code:
const FILE_PATH = path.join(__dirname, "../../files/apps.json");
const columns = [
"name",
"description",
"ext_id"
];
const myFile = fs.readFileSync(FILE_PATH, { encoding: "utf-8" });
const appData = await models.sequelize.query(
`
INSERT INTO data (${columns.join(", ")})
SELECT ${columns.join(", ")}
FROM (:path)
ON CONFLICT (ext_id)
DO UPDATE SET
${columns.map(col => `${col} = EXCLUDED.${col}`).join(", ")}
RETURNING ext_id;
`,
{ replacements: { path: FILE_PATH } }
);
As you can see, I want to read the file directly and put it into the database. I use a mapper called sequelize, but use a raw query in this case. My immediate problem is that I get this error:
syntax error at or near "'/home/blub/filePath'"
I don't really know how I should specify the path. I tried to parse it in directly, but then the program complained about the /. Any help here? In addition, I am also not sure whether the query is syntactically correct.

Here is a solution using CTE of postgres.
Versions:
"sequelize": "^5.21.3"
postgres:9.6
apps.json:
[
{
"name": "app-a",
"description": "app a desc",
"ext_id": 1
},
{
"name": "app-b",
"description": "app b desc",
"ext_id": 2
},
{
"name": "app-c",
"description": "app c desc",
"ext_id": 3
}
]
index.ts:
import { sequelize } from '../../db';
import { Model, DataTypes, QueryTypes } from 'sequelize';
import fs from 'fs';
import path from 'path';
class Data extends Model {}
Data.init(
{
name: DataTypes.STRING,
description: DataTypes.STRING,
ext_id: {
type: DataTypes.INTEGER,
unique: true,
},
},
{ sequelize, tableName: 'data' },
);
(async function test() {
try {
await sequelize.sync({ force: true });
const FILE_PATH = path.join(__dirname, './apps.json');
const columns = ['name', 'description', 'ext_id'];
const myFile = fs.readFileSync(FILE_PATH, { encoding: 'utf-8' });
const appData = await sequelize.query(
`
with app_json(doc) as (
values ('${myFile}'::json)
)
insert into data (${columns.join(', ')})
select ${columns.join(', ')}
from app_json l
cross join lateral json_populate_recordset(null::data, doc) as p
on conflict (ext_id) do update
set ${columns.map((col) => `${col} = EXCLUDED.${col}`).join(', ')}
returning ext_id;
`,
{ type: QueryTypes.INSERT },
);
console.log(appData);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
The execution result:
Executing (default): DROP TABLE IF EXISTS "data" CASCADE;
Executing (default): DROP TABLE IF EXISTS "data" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "data" ("id" SERIAL , "name" VARCHAR(255), "description" VARCHAR(255), "ext_id" INTEGER UNIQUE, 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 = 'data' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): with app_json(doc) as (
values ('[
{
"name": "app-a",
"description": "app a desc",
"ext_id": 1
},
{
"name": "app-b",
"description": "app b desc",
"ext_id": 2
},
{
"name": "app-c",
"description": "app c desc",
"ext_id": 3
}
]'::json)
)
insert into data (name, description, ext_id)
select name, description, ext_id
from app_json l
cross join lateral json_populate_recordset(null::data, doc) as p
on conflict (ext_id) do update
set name = EXCLUDED.name, description = EXCLUDED.description, ext_id = EXCLUDED.ext_id
returning ext_id;
[ [ { ext_id: 1 }, { ext_id: 2 }, { ext_id: 3 } ], 3 ]
Check the data records in the database:
id name description ext_id
1 app-a app a desc 1
2 app-b app b desc 2
3 app-c app c desc 3

Related

Apply where clause on associated table leads to sequelize bad field error

I have 3 tables sale,company and saleItem with the following relations:
Sale.belongsTo(Company);
Company.hasMany(Sale);
Sale.hasMany(SaleItem, { as: "items" });
SaleItem.belongsTo(Sale);
I want to apply a filter on the company's name I saw that to do that we have to use $ at the start and end but it isnt working. Any ideas where I am going wrong?
When I try to execute the below code I get the error:
SqlError: (conn=201, no: 1054, SQLState: 42S22) Unknown column 'company.name' in 'where clause'
sql: SELECT `sale`.*, `company`.`id` AS `company.id`, `company`.`name` AS `company.name`, `items`.`id` AS `items.id`, `items`.`quantity` AS `items.quantity`, `items`.`price` AS `items.price`, `items`.`description` AS `items.description`, `items`.`margin` AS `items.margin`, `items`.`gst` AS `items.gst`, `items`.`createdAt` AS `items.createdAt`, `items`.`updatedAt` AS `items.updatedAt`, `items`.`saleId` AS `items.saleId`, `items`.`itemId` AS `items.itemId` FROM (SELECT `sale`.`id`, `sale`.`date`, `sale`.`type`, `sale`.`description`, `sale`.`poNumber`, `sale`.`poDate`, `sale`.`paymentType`, `sale`.`gst`, `sale`.`discount`, `sale`.`freight`, `sale`.`status`, `sale`.`saleStatus`, `sale`.`referenceNumber`, `sale`.`ftn`, `sale`.`quotationNumber`, `sale`.`showGST`, `sale`.`invoiceDate`, `sale`.`hasWithholdingTax`, `sale`.`serialNumber`, `sale`.`currency`, `sale`.`createdAt`, `sale`.`updatedAt`, `sale`.`companyId`, `sale`.`customerId` FROM `sale` AS `sale` WHERE `company`.`name` LIKE '%%' AND `sale`.`customerId` = 1 AND `sale`.`status` = 'ACTIVE' ORDER BY `id` DESC LIMIT 0, 15) AS `sale` LEFT OUTER JOIN `company` AS `company` ON `sale`.`companyId` = `company`.`id` LEFT OUTER JOIN `saleItem` AS `items` ON `sale`.`id` = `items`.`saleId` ORDER BY `id` DESC;
It works if I dont include the SaleItem table in query
Here is the code
await Sale.findAndCountAll({
include: [
{
model: Company,
attributes: ["name"],
as: "company",
},
{ model: SaleItem, as: "items" },
],
distinct: true,
where: {
"$company.name$": { [Op.like]: `%${search}%` },
customerId:1,
status: "ACTIVE",
},
})
If you see the generated SQL, company.name WHERE clause is incorrectly added to a subquery, so you can either turn off the subquery or you can add your where option within the include.
Option 1:
await Sale.findAndCountAll({
...,
subQuery: false
})
Option 2:
await Sale.findAndCountAll({
include: [
{
model: Company,
attributes: ["name"],
as: "company",
where: {
name: { [Op.like]: `%${search}%` }
}
},
{ model: SaleItem, as: "items" },
],
distinct: true,
where: {
customerId:1,
status: "ACTIVE",
},
})

How to remove object by value from a JSONB type array?

I want to remove a JSONB object by their unique 'id' value from a JSONB array. I am no expert at writing SQL code, but I managed to write the concatenate function.
For an example: Remove this object from an array below.
{
"id": "ad26e2be-19fd-4862-8f84-f2f9c87b582e",
"title": "Wikipedia",
"links": [
"https://en.wikipedia.org/1",
"https://en.wikipedia.org/2"
]
},
Schema:
CREATE TABLE users (
url text not null,
user_id SERIAL PRIMARY KEY,
name VARCHAR,
list_of_links jsonb default '[]'
);
list_of_links format:
[
{
"id": "ad26e2be-19fd-4862-8f84-f2f9c87b582e",
"title": "Wikipedia",
"links": [
"https://en.wikipedia.org/1",
"https://en.wikipedia.org/2"
]
},
{
"id": "451ac172-b93e-4158-8e53-8e9031cfbe72",
"title": "Russian Wikipedia",
"links": [
"https://ru.wikipedia.org/wiki/",
"https://ru.wikipedia.org/wiki/"
]
},
{
"id": "818b99c8-479b-4846-ac15-4b2832ec63b5",
"title": "German Wikipedia",
"links": [
"https://de.wikipedia.org/any",
"https://de.wikipedia.org/any"
]
},
...
]
The concatenate function:
update users set list_of_links=(
list_of_links || (select *
from jsonb_array_elements(list_of_links)
where value->>'id'='ad26e2be-19fd-4862-8f84-f2f9c87b582e'
)
)
where url='test'
returning *
;
Your json data is structured so you have to unpack it, operate on the unpacked data, and then repack it again:
SELECT u.url, u.user_id, u.name,
jsonb_agg(
jsonb_build_object('id', l.id, 'title', l.title, 'links', l.links)
) as list_of_links
FROM users u
CROSS JOIN LATERAL jsonb_to_recordset(u.list_of_links) AS l(id uuid, title text, links jsonb)
WHERE l.id != 'ad26e2be-19fd-4862-8f84-f2f9c87b582e'::uuid
GROUP BY 1, 2, 3
The function jsonb_to_recordset is a set-returning function so you have to use it as a row source, joined to its originating table with the LATERAL clause so that the list_of_links column is available to the function to be unpacked. Then you can delete the records you are not interested in using the WHERE clause, and finally repack the structure by building the record fields into a jsonb structure and then aggregating the individual records back into an array.
I wrote this on JS but that does not matter to how it works. Essentially, its getting all the items from the array, then finding the matching id which returns an index. And using that index, I use "-" operator which takes the index and removes it from the array. Sorry if my grammar is bad.
//req.body is this JSON object
//{"url":"test", "id": "ad26e2be-19fd-4862-8f84-f2f9c87b582e"}
var { url, id } = req.body;
pgPool.query(
`
select list_of_links
from users
where url=$1;
`,
[url],
(error, result) => {
//block code executing further if error is true
if (error) {
res.json({ status: "failed" });
return;
}
if (result) {
// this function just returns the index of the array element where the id matches from request's id
// 0, 1, 2, 3, 4, 5
var index_of_the_item = result.rows.list_of_links
.map(({ id: db_id }, index) =>
db_id === id ? index : false
)
.filter((x) => x !== false)[0];
//remove the array element by it's index
pgPool.query(
`
update users
set list_of_links=(
list_of_links - $1::int
)
where url=$2
;
`,
[index_of_the_item, url], (e, r) => {...}
);
}
}
);

Select from multiple tables in sequelize

I'm struggling in how to select from two tables using the Sequelize.
Actually I'm trying to do it:
SELECT * FROM users, clients WHERE user.id = clients.user_id
I have no idea how to user two tables as I described, the only thing I did that got some results were:
const clients = await Client.findAll({
attributes: ["user_id"],
});
const users = [];
for (const client of clients) {
let user = await User.findAll({
where: {
id: {
[Op.eq]: client.user_id
}
}
});
users.push(user);
}
Which return me something:
[
[
{
"id": 1,
"first_name": "Velda",
"middle_name": "Zboncak",
"last_name": "Kris",
"email": "vkris10#hotmail.com",
"created_at": "2020-02-07T20:09:29.484Z",
"updated_at": "2020-02-07T20:09:29.484Z"
}
]
];
Model and Assossiation
First of all, you need to create the correct associations in the model of your table. In this case for the User and the Client, it's supposed to be an Client.belongsTo(...)
Take a look at User model:
const { Model, DataTypes } = require("sequelize");
class User extends Model {
static init(sequelize) {
super.init({
first_name: DataTypes.STRING,
middle_name: DataTypes.STRING,
last_name: DataTypes.STRING,
email: DataTypes.STRING
}, { sequelize });
}
}
module.exports = User;
Take a look at Client model:
const { Model, DataTypes } = require("sequelize");
class Client extends Model {
static init(sequelize) {
super.init({
user_id: DataTypes.INTEGER // The foreign key
}, { sequelize });
}
static associate(models) {
Client.belongsTo(models.User, {
foreignKey: "id", // Column name of associated table
as: "user" // Alias for the table
});
}
}
module.exports = Client;
When associating tables you need to have in mind those values inside the associate method, being the foreignKey: "id" the column name inside the models.ModelName, which will be used to make the joins, and the as: "user" which are used as an alias for the table like SELECT t.column1 FROM table AS t;
Controller
Okay, now you have set your models, you need to set your controller, where the magic happens. As you said you want to fetch results using:
SELECT * FROM users, clients WHERE user.id = clients.user_id
But to achieve the same result you can follow the sql join method to fetch the results from db, so it will be something like this:
SELECT
"user"."first_name", "user"."middle_name", "user"."last_name", "user"."email"
FROM "clients" AS "client"
LEFT JOIN "users" AS "user"
ON "client"."id" = "user"."id";
Knowing that we can talk about including tables in sequelize, which is the same as associations
const Client = require("./path/to/models/Client");
module.exports = {
async fetchAll(req, res) {
const results = await Client.findAll({
limit: 25,
include: [
{
association: "user",
attributes: ["first_name", "middle_name", "last_name", "email"]
}
]
});
return res.json(results);
},
};
Now lets talk about what is going on in the code:
The Model.findAll({}) will fetch all the result inside the specified table, in this case clients table.
The limit: 25 will limit your results in only 25 rows, you are free to remove or edit as you need.
The include: [], it will do the joins through the tables you specify, as you need only the users table, we are going to use only one object, so the assossiation: "user" will make this connection between tables, you must use the same alias you set inside the model. And at least the attributes: ["columns"] is where you set all the fields you want to fetch.
And that's it, you make you request, and the result of this will be exactly the same join as I mentioned. And the results will be:
[
{
"id": 1,
"user_id": 1,
"user": {
"first_name": "John",
"middle_name": "Ironsight",
"last_name": "Doe",
"email": "johndoe#example.com"
}
}, {...}
]
Can use where in include. Find the document at here
let user_id = client.user_id;
users = await User.findAll({
include: [
{
model: Client,
as: 'client',
where: {
user_id: user_id
}
}
]
});

How to Make a Lookup connection between two Collection

Goal:
This sql and its result should be the same result from mongoDB's query code.
In order words, same result but for mongoDB.
Problem:
How to you make a lookup connection in relation to People and Role in Mongo DB's query code?
Info:
I'm new in mongo DB
SQL code
SELECT
a.*,
'.' AS '.',
b.*,
'.' AS '.',
c.*
FROM
[db1].[dbo].[People_Course_Grade] a
INNER JOIN [db1].[dbo].[People] b on a.PeopleId = b.PeopleId
INNER JOIN [db1].[dbo].[Role] c on b.RoleId = c.RoleId
Json data:
Role:
[{"RoleId":1,"Name":"Student"},{"RoleId":2,"Name":"Teacher"}]
People_Course_Grade:
[{"People_Course_GradeId":1,"PeopleId":1,"CourseId":1},
{"People_Course_GradeId":2,"PeopleId":2,"CourseId":1},
{"People_Course_GradeId":3,"PeopleId":3,"CourseId":2},
{"People_Course_GradeId":4,"PeopleId":1,"CourseId":2}]
Course:
[{"CourseId":1,"Name":"Java"},{"CourseId":2,"Name":"Java II"},
{"CourseId":3,"Name":"Statistik 1"}]
db.People_Course_Grade.aggregate([
{
$lookup:{
from: "People",
localField: "people_id",
foreignField: "_id",
as: "people"
}
},
{ $unwind:"$people" },
{
$project:{
course_id : 1,
people_id : 1,
// grade_id : 1,
Name : "$people.Name",
}
}
]);
You need to start with double $lookup since you have three collections. Then you can use $arrayElemAt to always get single element from lookup's result. To flatten your structure you can use $replaceRoot with $mergeObjects (promotes all the fields from people and course to root level.
db.People_Course_Grade.aggregate([
{
$lookup:{
from: "Role",
localField: "PeopleId",
foreignField: "RoleId",
as: "people"
}
},
{
$lookup:{
from: "Course",
localField: "CourseId",
foreignField: "CourseId",
as: "course"
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$$ROOT",
{ $arrayElemAt: [ "$people", 0 ] },
{ $arrayElemAt: [ "$course", 0 ] },
]
}
}
},
{
$project: {
people: 0,
course: 0
}
}
])
Mongo Playground
$arrayElemAt can always be replaced with $unwind like you tried. You also have a naming conflict on name field so probably you need to run $project to rename one of those fields - otherwise you'll get only one of them in final result.

Sequelize - Count related data and separate by column

I have two tables defined "posts" and "comments". I want to get all posts and a number of there total comments, split by the comment type. Currently I can get the count, but cannot separate by comment type
const Sequelize = require('sequelize');
const sequelize = new Sequelize('postgres://username#localhost:5432/test');
const posts = sequelize.define('posts', {
name: Sequelize.STRING,
})
const comments = sequelize.define('comments', {
title: Sequelize.STRING,
type: Sequelize.STRING
})
posts.hasMany(comments);
comments.belongsTo(posts);
const importData = async () => {
// Insert test data
await sequelize.sync({ force: true });
await posts.create({ id: 1, name: 'Hello World' })
await comments.create({ postId: 1, title: 'This is great', type: 'text' })
await comments.create({ postId: 1, title: 'Thanks', type: 'text' })
await comments.create({ postId: 1, title: 'Oh Yeah', type: 'image' })
await comments.create({ postId: 1, title: 'Oh Yeah', type: 'video' })
// Fetch data
const post = await posts.findAll({
where: { id: 1 },
attributes: ['id', 'name', [sequelize.fn('COUNT', 'comments.id'), 'commentCount']],
include: [{ model: comments, attributes: [] }],
group: ['posts.id']
})
console.log(JSON.stringify(post, null, 4))
}
importData();
Output is
[
{
"id": 1,
"name": "Hello World",
"commentCount": "4"
}
]
Desired Output
[
{
"id": 1,
"name": "Hello World",
"commentCount": { "text": 2, "image": 1, "video": 1 }
}
]
Can this be done through Sequelize, or even raw SQL?
Raw SQL something like:
SELECT P.ID, C.Comment_Count, C.Type
FROM POSTS P
LEFT JOIN (SELECT count(*) Comment_Count, PostID, type
FROM Comments
GROUP BY POSTID, type) C
on P.ID = C.PostID
change
PostID to the column name in comments that is the FK to posts
change ID to the PK in posts.