fix(database): column name in array field (#4110)

* fix: column name in array field

* chore: test

* fix: test

* fix: test

* fix: test
This commit is contained in:
ChengLei Shao 2024-04-24 12:39:15 +08:00 committed by GitHub
parent e96e9aea6e
commit f8067c6550
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 188 additions and 33 deletions

View File

@ -170,6 +170,23 @@ describe('find with associations', () => {
});
it('should filter by association array field', async () => {
const Group = db.collection({
name: 'groups',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'hasMany',
name: 'users',
},
{
type: 'array',
name: 'tagFields',
},
],
});
const User = db.collection({
name: 'users',
fields: [
@ -181,6 +198,120 @@ describe('find with associations', () => {
type: 'hasMany',
name: 'posts',
},
{
type: 'belongsTo',
name: 'group',
},
{
type: 'array',
name: 'tagFields',
},
],
});
const Post = db.collection({
name: 'posts',
fields: [
{
type: 'array',
name: 'tagFields',
},
{
type: 'string',
name: 'title',
},
],
});
await db.sync();
await Group.repository.create({
values: [
{
name: 'g1',
users: [
{
name: 'u1',
tagFields: ['u1'],
posts: [
{
tagFields: ['p1'],
title: 'u1p1',
},
],
},
],
tagFields: ['g1'],
},
],
});
// zero nested
const posts = await Post.repository.find({
filter: {
tagFields: {
$match: ['p1'],
},
},
});
expect(posts.length).toEqual(1);
const filter0 = {
$and: [
{
posts: {
tagFields: {
$match: ['p1'],
},
},
},
],
};
const userFindResult = await User.repository.find({
filter: filter0,
});
expect(userFindResult.length).toEqual(1);
const filter = {
$and: [
{
users: {
posts: {
tagFields: {
$match: ['p1'],
},
},
},
},
],
};
const results = await Group.repository.find({
filter,
});
expect(results[0].get('name')).toEqual('g1');
});
it('should filter by array not empty', async () => {
const User = db.collection({
name: 'users',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'hasMany',
name: 'posts',
},
{
type: 'array',
name: 'tags',
},
],
});
@ -204,6 +335,7 @@ describe('find with associations', () => {
values: [
{
name: 'u1',
tags: ['u1-tag', 'u2-tag'],
posts: [
{
tags: ['t1'],
@ -214,33 +346,17 @@ describe('find with associations', () => {
],
});
const posts = await Post.repository.find({
const posts = await User.repository.find({
filter: {
tags: {
$match: ['t1'],
'posts.tags': {
$noneOf: ['t2'],
},
},
});
expect(posts.length).toEqual(1);
const filter = {
$and: [
{
posts: {
tags: {
$match: ['t1'],
},
},
},
],
};
const results = await User.repository.find({
filter,
});
expect(results[0].get('name')).toEqual('u1');
expect(posts[0].get('name')).toEqual('u1');
});
it('should filter with append', async () => {

View File

@ -3,7 +3,37 @@ import { Op, Sequelize } from 'sequelize';
import { isMySQL, isPg } from './utils';
const getFieldName = (ctx) => {
return ctx.model.rawAttributes[ctx.fieldName]?.field || ctx.fieldName;
const fullNameSplit = ctx.fullName.split('.');
const fieldName = ctx.fieldName;
let columnName = fieldName;
const associationPath = [];
if (fullNameSplit.length > 1) {
for (let i = 0; i < fullNameSplit.length - 1; i++) {
associationPath.push(fullNameSplit[i]);
}
}
const getModelFromAssociationPath = () => {
let model = ctx.model;
for (const association of associationPath) {
model = model.associations[association].target;
}
return model;
};
const model = getModelFromAssociationPath();
if (model.rawAttributes[fieldName]) {
columnName = model.rawAttributes[fieldName].field || fieldName;
}
if (associationPath.length > 0) {
const association = associationPath.join('->');
columnName = `${association}.${columnName}`;
}
return columnName;
};
const escape = (value, ctx) => {
@ -18,7 +48,9 @@ const getQueryInterface = (ctx) => {
const sqliteExistQuery = (value, ctx) => {
const fieldName = getFieldName(ctx);
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
const queryInterface = getQueryInterface(ctx);
const name = queryInterface.quoteIdentifiers(fieldName);
const sqlArray = `(${value.map((v) => `'${v}'`).join(', ')})`;
@ -44,7 +76,7 @@ const emptyQuery = (ctx, operator: '=' | '>') => {
const queryInterface = getQueryInterface(ctx);
return `(select ${ifNull}(${funcName}(${queryInterface.quoteIdentifier(fieldName)}), 0) ${operator} 0)`;
return `(select ${ifNull}(${funcName}(${queryInterface.quoteIdentifiers(fieldName)}), 0) ${operator} 0)`;
};
export default {
@ -53,7 +85,7 @@ export default {
const fieldName = getFieldName(ctx);
if (isPg(ctx)) {
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
const name = queryInterface.quoteIdentifiers(fieldName);
const queryValue = escape(JSON.stringify(value), ctx);
return Sequelize.literal(`${name} @> ${queryValue}::JSONB AND ${name} <@ ${queryValue}::JSONB`);
}
@ -61,7 +93,7 @@ export default {
value = escape(JSON.stringify(value.sort()), ctx);
if (isMySQL(ctx)) {
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
const name = queryInterface.quoteIdentifiers(fieldName);
return Sequelize.literal(`JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name})`);
}
@ -71,16 +103,16 @@ export default {
},
$notMatch(value, ctx) {
const fieldName = getFieldName(ctx);
const queryInterface = getQueryInterface(ctx);
value = escape(JSON.stringify(value), ctx);
if (isPg(ctx)) {
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
return Sequelize.literal(`not (${name} <@ ${value}::JSONB and ${name} @> ${value}::JSONB)`);
}
if (isMySQL(ctx)) {
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
return Sequelize.literal(`not (JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name}))`);
}
return {
@ -91,9 +123,10 @@ export default {
$anyOf(value, ctx) {
const fieldName = getFieldName(ctx);
value = _.castArray(value);
const queryInterface = getQueryInterface(ctx);
if (isPg(ctx)) {
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
return Sequelize.literal(
`${name} ?| ${escape(
value.map((i) => `${i}`),
@ -104,7 +137,7 @@ export default {
if (isMySQL(ctx)) {
value = escape(JSON.stringify(value), ctx);
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
return Sequelize.literal(`JSON_OVERLAPS(${name}, ${value})`);
}
@ -117,9 +150,10 @@ export default {
let where;
value = _.castArray(value);
const queryInterface = getQueryInterface(ctx);
if (isPg(ctx)) {
const fieldName = getFieldName(ctx);
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
// pg single quote
where = Sequelize.literal(
`not (${name} ?| ${escape(
@ -130,7 +164,7 @@ export default {
} else if (isMySQL(ctx)) {
const fieldName = getFieldName(ctx);
value = escape(JSON.stringify(value), ctx);
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
where = Sequelize.literal(`NOT JSON_OVERLAPS(${name}, ${value})`);
} else {
const subQuery = sqliteExistQuery(value, ctx);

View File

@ -10,4 +10,9 @@ const isMySQL = (ctx) => {
return getDialect(ctx) === 'mysql' || getDialect(ctx) === 'mariadb';
};
const getQueryInterface = (ctx) => {
const sequelize = ctx.db.sequelize;
return sequelize.getQueryInterface();
};
export { getDialect, isPg, isMySQL };