PostgreSQL / TypeORM: String array type, how to use LIKE in query? - sql

My backend database is PostgreSQL
I have a TypeORM object simplified to:
#Entity()
#Index(['name'], {unique: true}
export class Foo extends BaseEntity
{
#PrimaryGeneratedColumn('uuid')
id: string;
#Column()
name: string;
#Column('varchar', { array: true })
bar: string[];
}
I'm creating an API query handler that can handle searches. I can easily do a LIKE query on the name like this:
let qs = Foo.createQueryBuilder('foo');
qs.andWhere('foo.name ILIKE :name', {
name:'%${name}%'
});
I'd like to also search for e.g. any "bar" LIKE %myqueryterm% but I can't seem to find anything on that.
I see a bunch of docs on how to exactly match a search term in bar, but no soft comparison.
What I essentially want to do is that I have a data set
[
{id: 1, name: 'whatever', bar: ['apple','bananna','yeti','woo']},
{id: 2, name: 'something else', bar: ['red','blue','green', 'boo']},
{id: 3, name: 'i dunno', bar: ['ford','chevy']},
]
and I'd like to let the user to be able to query e.g. "%oo% and return the first 2 records based on bar strings containing that substring.

Postgres provides array functions and operators that you can use to create any complex query.
In your case, a clean way of doing this would be to
Convert the array to a string and then
Perform the LIKE operation on that string
Something like this should work:
.createQueryBuilder('foo')
.where("array_to_string(foo.bar, ',') LIKE :bar", {
bar: '%aa%',
})
.getMany();

I don't know typeorm. but based on https://github.com/typeorm/typeorm/issues/881
The sql query would be like:
WITH cte (
id,
name,
bar
) AS (
VALUES (1, 'whatever', ARRAY['apple', 'bananna', 'yeti', 'woo']),
(2, 'something else', ARRAY['red', 'blue', 'green', 'boo']),
(3, 'i dunno', ARRAY['ford', 'chevy'])
),
cte1 AS (
SELECT
json_agg(row_to_json(cte.*)) AS json_all
FROM
cte
)
SELECT
value
FROM
cte1,
json_array_elements(json_all)
WHERE
value ->> 'bar' ~ 'oo';
Based on the github page, it would be like:
getConnection().query("
with cte(id,name,bar) as (values
(1,'whatever',array ['apple','bananna','yeti','woo'])
,(2,'something else',array ['red','blue','green', 'boo'])
,(3,'i dunno',array ['ford','chevy'])
),cte1 AS
(select json_agg(row_to_json(cte.*)) as json_all from cte)
select value
from cte1,json_array_elements(json_all)
where value->>'bar' ~ #0", ['oo']);
case insensitively match would be value->>'bar' ~* #0"

Related

How to flatten out nested array of strings in json column?

I have the following table:
id
contents
123
{ blocks: [{ text: "abc" }, { text: "123" }] }
foo
{ blocks: [{ text: "bar" }, { text: "moretext" }, { text: "ok" }] }
I want to write a view of the above that looks like:
id
contents
raw_text
123
{blocks: [{text: "abc"}, {text: "123"}]}
abc, 123
foo
{blocks: [{text: "bar"}, {text: "moretext"}, { text: "ok"}]}
bar, moretext, ok
This was the query I tried running:
select post.id, array_to_string(array_agg(jsonb_array_elements(post.contents -> 'blocks') ->> 'text')) as paragraphs from post group by id
But it results in the error
aggregate function calls cannot contain set-returning function calls.
If a JSON array of all the values is also acceptable, you can use a JSON path query:
select id, contents,
jsonb_path_query_array(contents, '$.blocks[*].text')
from post;
As there is no simply cast from a JSON array to a native Postgres array, and you do need that as a CSV string, you need to unnest and aggregate with a scalar sub-query:
select id, contents,
(select string_agg(x.item ->> 'text', ', ')
from jsonb_array_elements(contents -> 'blocks') as x(item)) as raw_text
from post;
The reason for your error is, that you are mixing nesting multiple aggregate and set returning function which simply isn't supported.

Laravel where, orWhereHas and whereNotIn

Hello great people of SO!
I hope you all have a good day and have a good health
Note: I'm not good at SQL
Sorry for bad english, but I will try my best to explain my issue
I'm using Laravel v8.x for my app, and after setting up model relationships, events, queues, etc, now I'm working for SQL
ATM, I have 2 Models,
User
Post
Relationships:
User hasMany Post
User belongsToMany User (Block)
User belongsToMany User (Follow)
Post belongsTo User
Database:
5 record for User
2 record for Block
3 records for Post
Table: (Using faker)
users
[
{ id: 1, name: 'Jonathan Beatrice', username: 'kiana.fay', ... },
{ id: 2, name: 'Lacey Kirlin', username: 'kenna.turner', ... },
{ id: 3, name: 'Alexander Schiller', username: 'cassandra95', ... },
{ id: 4, name: 'Daniel Wickozky', username: 'nkoepp', ... },
{ id: 5, name: 'Maymie Lehner', username: 'frami.felton', ... }
]
block
[
{ id: 1, by_id: 1, to_id: 2 }, // User #1 block user #2
{ id: 2, by_id: 4, to_id: 1 } // User #4 block user #1
]
posts
[
{ id: 1, user_id: 2, body: 'Test post', ... },
{ id: 2, user_id: 5, body: 'Lorem ipsum dolor sit amet ...', ... },
{ id: 3, user_id: 4, body: 'ABCD festival soon! ...', ... },
]
Everything works fine and smooth
Now that I want to implement search system, I have a problem, since I'm not good with SQL
Here's my code
SearchController.php
use ...;
use ...;
...
public function posts(Request $request)
{
// For testing purpose
$user = User::with(['userBlocks', 'blocksUser'])->find(1);
// Get all id of user that $user block
// return [2]
$user_blocks = $user->userBlocks->pluck('pivot')->pluck('to_id')->toArray();
// Get all id of user that block $user
// return [4]
$blocks_user = $user->blocksUser->pluck('pivot')->pluck('by_id')->toArray();
// Merge all ids above (must be unique())
// return [2, 4]
$blocks = array_merge($user_blocks, $blocks_user);
// .../search?q=xxx
$query = $request->query('q');
$sql = Post::query();
// Search for posts that has `posts`.`body` LIKE ? ($query)
$sql->where('body', 'LIKE', "%$query%");
// This is where I got confused
$sql->orWhereHas('user', function ($post_user) use ($blocks, $query) {
$post_user
->whereNotIn('id', $blocks) // Exclude posts that has user and their id not in (x, x, x, x, ... ; $block variable above)
->where('name', 'LIKE', "%$query%") // Find user that has name LIKE ? ($query)
->orWhere('username', 'LIKE', "%$query%"); // or Find user that has username LIKE ? ($query)
});
$sql->orderBy('created_at', 'DESC');
$sql->with(['user']);
$posts = $sql->simplePaginate(10, ['*'], 'p');
return $posts;
}
I run the code, .../search?q=e
Note:
All users has alphabet E in their names
And also all posts has alphabet E in their body
We (as User #1), block User #2, and User #4, block us (User #1)
Result: Controller returned all posts
This is the query when I use DB::enableQueryLog() and DB::getQueryLog()
SELECT
*
FROM
`posts`
WHERE `body` LIKE ?
AND EXISTS
(SELECT
*
FROM
`users`
WHERE `posts`.`user_id` = `users`.`id`
AND (
`id` NOT IN (?)
AND `username` LIKE ?
OR `name` LIKE ?
))
ORDER BY `created_at` ASC
LIMIT 11 OFFSET 0
Goal: Search all posts that has body LIKE ?, OR posts that has user; username LIKE ? or name LIKE ? (But also exclude the user we block and the user that block us
Thanks in advance
If there's any unclear explanation, I will edit it A.S.A.P
If I run on my recent laravel install, with my proposed change for one of your issues, version 7.19.1, I get this query:
SELECT
*
FROM
`posts`
WHERE `body` LIKE ?
OR EXISTS <- line of interest
(SELECT
*
FROM
`users`
WHERE `posts`.`user_id` = `users`.`id`
AND (
`id` NOT IN (?)
AND (`username` LIKE ?
OR `name` LIKE ?) <- extra brackets ive added
))
ORDER BY `created_at` ASC
LIMIT 11 OFFSET 0
Have a look at the line of interest, and compare it with the query your version of laravel is running. The AND EXISTS line is being incorrectly generated by laravel. OrWhereHas isnt behaving correctly in your version, I can't find the release number to see where it was fixed.
Id recommend upgrading to latest if possible, but thats not always an option. I've had a dig around, and it looks like the user in this question here encountered a similar problem:
WhereHas() / orWhereHas not constraining the query as expected
You can try moving your $sql->with(['user']); to before you OrWhereHas clause. I'm not sure if that will change it to OR, but its worth a try.
Second thing, I've added whereNested to your OR clause to ensure the precedence is correct, which adds the extra brackets in the query above, as in you dont want:
(`id` NOT IN (1, 2, 3)
AND `name` LIKE % test %)
OR `username` LIKE % test %
Since then it would include your blocked posts in the exists clause.
So final changes look like this, which I think fufills your description:
$sql->with(['user']); //deleted from original position and move here
$sql->where('body', 'LIKE', "%$query%")->whereNotIn('id', $blocks); //additional line
$sql->orWhereHas('ambience', function ($post_user) use ($blocks, $query) {
$post_user
->whereNotIn('id', $blocks);
$post_user->whereNested(function($post_user) use ($query) { //new bit
$post_user->where('name', 'LIKE', "%$query%")
->orWhere('username', 'LIKE', "%$query%");
});
});

Making a select with an array

Hello I have and Array with objects, each object have atributes that I need for an select:
In this case it is the result from another consult with typeorm
" const CompaniesRelation: Array = await getRepository(CompanyRelation).find({ where:{ UserId: data.UserId, IsActive: true} });"
Companies: Array = [{CompanyId="a"}{CompanyId="b"}{CompanyId="c"}];
I need to make an select of all the data that matches with the Ids that are into Companies so for that I need to make an SQL like it:
const CompanyData: Array = SELECT *
FROM Company
INNER JOIN Company.CompanyId = CompaniesRelation[].CompanyId;
but it throw me error in typing, ¿how can I acces to each objetc into the array for make that match?
At the final I should traduce it sql to typeOrm, but I new and solving first in SQL it should help me to traduce to typeorm
Okay great, let us consider what we have to work with right:
So first we have a statement that gets a list of companies like so:
const CompaniesRelation: Array = await getRepository(CompanyRelation).find({
where: {
UserId: data.UserId,
IsActive: true
}
});
which ends up with something like this:
[ { CompanyId: 'a' }, { CompanyId: 'b' }, { CompanyId: 'c' } ]
Now we want to get a list of companies from an SQL DB with these Company IDs.
So the query should look like this:
// so first we re map the relation to an array of strings...
const ids: Array<string> = CompaniesRelation.map(c => c.CompanyId);
// then use it in the query, note the string interpolation for the query
const query: string = `SELECT * FROM Company WHERE CompanyId IN(${JSON.stringify(ids).slice(1, -1)});`;
I don't think this will cover the scope of the problem you have, I hope it helps though...feel free to ask

How to select specific fields on FaunaDB Query Language?

I can't find anything about how to do this type of query in FaunaDB. I need to select only specifics fields from a document, not all fields. I can select one field using Select function, like below:
serverClient.query(
q.Map(
q.Paginate(q.Documents(q.Collection('products')), {
size: 12,
}),
q.Lambda('X', q.Select(['data', 'title'], q.Get(q.Var('X'))))
)
)
Forget the selectAll function, it's deprecated.
You can also return an object literal like this:
serverClient.query(
q.Map(
q.Paginate(q.Documents(q.Collection('products')), {
size: 12,
}),
q.Lambda(
'X',
{
title: q.Select(['data', 'title'], q.Get(q.Var('X')),
otherField: q.Select(['data', 'other'], q.Get(q.Var('X'))
}
)
)
)
Also you are missing the end and beginning quotation marks in your question at ['data, title']
One way to achieve this would be to create an index that returns the values required. For example, if using the shell:
CreateIndex({
name: "<name of index>",
source: Collection("products"),
values: [
{ field: ["data", "title"] },
{ field: ["data", "<another field name>"] }
]
})
Then querying that index would return you the fields defined in the values of the index.
Map(
Paginate(
Match(Index("<name of index>"))
),
Lambda("product", Var("product"))
)
Although these examples are to be used in the shell, they can easily be used in code by adding a q. in front of each built-in function.

knex insert multiple rows

I have a problem inserting many rows into postgres db with knex.
I have dynamic number of rows needed to be inserted. The result i expect is:
insert row four times (four is for an example. I dont know exact number of inserts as it comes dynamically from frontend):
field_id will be diffrent in every row: (1,2,3,4) - i have array of these ID's
id_of_product will be always the same
value will be always diffrent: (req.body[id] that comes from Frontend) - ID in brackets is same value as the field_id from an
array
How i can achieve that? I tried looping it with forEach, but it's async operation so i can't use .then() as it will be called four times
Here's what i tried. i dont know how to set field_id and req.body to take it dynamically.
fields = [1,2,3,4]
Expected result:
knex creates 4 inserts as follows:
field_id: 1,
product_id: some static id
value: frontValue[1]
ETC
knex('metadata').insert(
[{ field_id: fields,
product_id: product_id,
value: req.body[fields]
}]
)
If I understand correctly you want to insert 4 records to your metadata table:
{ field_id: 1, product_id: X, value: req.body[1] },
{ field_id: 2, product_id: X, value: req.body[2] },
{ field_id: 3, product_id: X, value: req.body[3] },
{ field_id: 4, product_id: X, value: req.body[4] }
To insert multiple records in the same statement they each need to be separate elements in the array that you supply to Knex (check out the insert docs for additional examples):
const product_id = X;
const fieldsToInsert = fields.map(field =>
({ field_id: field, product_id, value: req.body[field] }));
return knex('metadata').insert(fieldsToInsert)
.then(() => { /* handle success */ })
.catch(() => { /* handle failure */});