fix: mysql column in where clause is ambiguous (#756)

* fix: mysql column in where clause is ambiguous

* feat: add test case

* fix: sqlite error
This commit is contained in:
chenos 2022-08-20 23:24:02 +08:00 committed by GitHub
parent a1dc139cf4
commit fdfa79cf8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 89 additions and 15 deletions

View File

@ -0,0 +1,66 @@
import { mockDatabase } from '../';
import { Database } from '../../database';
describe('array field', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('should not be ambiguous', async () => {
db.collection({
name: 'a',
fields: [
{
type: 'array',
name: 'arr',
},
{
type: 'belongsToMany',
name: 'b',
target: 'b',
},
],
});
db.collection({
name: 'b',
fields: [
{
type: 'array',
name: 'arr',
},
],
});
await db.sync();
const repository = db.getRepository('a');
await repository.find({
filter: {
'arr.$match': ['aa'],
},
appends: ['b'],
});
await repository.find({
filter: {
'arr.$notMatch': ['aa'],
},
appends: ['b'],
});
await repository.find({
filter: {
'arr.$anyOf': ['aa'],
},
appends: ['b'],
});
await repository.find({
filter: {
'arr.$noneOf': ['aa'],
},
appends: ['b'],
});
});
});

View File

@ -124,12 +124,13 @@ export default class FilterParser {
skipPrefix = origins.join('.'); skipPrefix = origins.join('.');
const queryValue = lodash.get(unflatten(originalFiler), skipPrefix); const queryValue = lodash.get(unflatten(originalFiler), skipPrefix);
const [fieldName, fullName] = this.getFieldNameFromQueryPath(skipPrefix);
value = opKey(queryValue, { value = opKey(queryValue, {
app: this.context.app, app: this.context.app,
db: this.database, db: this.database,
path: skipPrefix, path: skipPrefix,
fieldName: this.getFieldNameFromQueryPath(skipPrefix), fullName,
fieldName,
model: this.model, model: this.model,
}); });
break; break;
@ -230,14 +231,14 @@ export default class FilterParser {
private getFieldNameFromQueryPath(queryPath: string) { private getFieldNameFromQueryPath(queryPath: string) {
const paths = queryPath.split('.'); const paths = queryPath.split('.');
let fieldName; let fieldName;
let fullPaths = [];
for (const path of paths) { for (const path of paths) {
if (path.startsWith('$') || !lodash.isNaN(parseInt(path))) { if (path.startsWith('$') || !lodash.isNaN(parseInt(path))) {
continue; continue;
} }
fullPaths.push(path);
fieldName = path; fieldName = path;
} }
return [fieldName, fullPaths.join('.')];
return fieldName;
} }
} }

View File

@ -1,5 +1,5 @@
import { Op, Sequelize } from 'sequelize'; import { Op, Sequelize } from 'sequelize';
import { isPg, isMySQL } from './utils'; import { isMySQL, isPg } from './utils';
const getFieldName = (ctx) => { const getFieldName = (ctx) => {
const fieldName = ctx.fieldName; const fieldName = ctx.fieldName;
@ -13,10 +13,11 @@ const escape = (value, ctx) => {
const sqliteExistQuery = (value, ctx) => { const sqliteExistQuery = (value, ctx) => {
const fieldName = getFieldName(ctx); const fieldName = getFieldName(ctx);
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
const sqlArray = `(${value.map((v) => `'${v}'`).join(', ')})`; const sqlArray = `(${value.map((v) => `'${v}'`).join(', ')})`;
const subQuery = `exists (select * from json_each(${fieldName}) where json_each.value in ${sqlArray})`; const subQuery = `exists (select * from json_each(${name}) where json_each.value in ${sqlArray})`;
return subQuery; return subQuery;
}; };
@ -53,7 +54,8 @@ export default {
value = escape(JSON.stringify(value.sort()), ctx); value = escape(JSON.stringify(value.sort()), ctx);
if (isMySQL(ctx)) { if (isMySQL(ctx)) {
return Sequelize.literal(`JSON_CONTAINS(${fieldName}, ${value}) AND JSON_CONTAINS(${value}, ${fieldName})`); const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
return Sequelize.literal(`JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name})`);
} }
return { return {
@ -66,11 +68,13 @@ export default {
value = escape(JSON.stringify(value), ctx); value = escape(JSON.stringify(value), ctx);
if (isPg(ctx)) { if (isPg(ctx)) {
return Sequelize.literal(`not (${fieldName} <@ ${value}::JSONB and ${fieldName} @> ${value}::JSONB)`); const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
return Sequelize.literal(`not (${name} <@ ${value}::JSONB and ${name} @> ${value}::JSONB)`);
} }
if (isMySQL(ctx)) { if (isMySQL(ctx)) {
return Sequelize.literal(`not (JSON_CONTAINS(${fieldName}, ${value}) AND JSON_CONTAINS(${value}, ${fieldName}))`); const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
return Sequelize.literal(`not (JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name}))`);
} }
return { return {
[Op.ne]: Sequelize.literal(`json(${value})`), [Op.ne]: Sequelize.literal(`json(${value})`),
@ -81,8 +85,9 @@ export default {
const fieldName = getFieldName(ctx); const fieldName = getFieldName(ctx);
if (isPg(ctx)) { if (isPg(ctx)) {
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
return Sequelize.literal( return Sequelize.literal(
`${fieldName} ?| ${escape( `${name} ?| ${escape(
value.map((i) => `${i}`), value.map((i) => `${i}`),
ctx, ctx,
)}`, )}`,
@ -91,8 +96,8 @@ export default {
if (isMySQL(ctx)) { if (isMySQL(ctx)) {
value = escape(JSON.stringify(value), ctx); value = escape(JSON.stringify(value), ctx);
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
return Sequelize.literal(`JSON_OVERLAPS(${fieldName}, ${value})`); return Sequelize.literal(`JSON_OVERLAPS(${name}, ${value})`);
} }
const subQuery = sqliteExistQuery(value, ctx); const subQuery = sqliteExistQuery(value, ctx);
@ -105,9 +110,10 @@ export default {
if (isPg(ctx)) { if (isPg(ctx)) {
const fieldName = getFieldName(ctx); const fieldName = getFieldName(ctx);
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
// pg single quote // pg single quote
where = Sequelize.literal( where = Sequelize.literal(
`not (${fieldName} ?| ${escape( `not (${name} ?| ${escape(
value.map((i) => `${i}`), value.map((i) => `${i}`),
ctx, ctx,
)})`, )})`,
@ -115,7 +121,8 @@ export default {
} else if (isMySQL(ctx)) { } else if (isMySQL(ctx)) {
const fieldName = getFieldName(ctx); const fieldName = getFieldName(ctx);
value = escape(JSON.stringify(value), ctx); value = escape(JSON.stringify(value), ctx);
where = Sequelize.literal(`NOT JSON_OVERLAPS(${fieldName}, ${value})`); const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
where = Sequelize.literal(`NOT JSON_OVERLAPS(${name}, ${value})`);
} else { } else {
const subQuery = sqliteExistQuery(value, ctx); const subQuery = sqliteExistQuery(value, ctx);