Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(select): hasura-lateral-joins-style-mysql 6497 #1

Open
wants to merge 12 commits into
base: v7
Choose a base branch
from
29 changes: 18 additions & 11 deletions src/dialects/abstract/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1205,16 +1205,7 @@ export class AbstractQueryGenerator {
options.includeAliases = new Map();
}

// resolve table name options
if (options.tableAs) {
mainTable.as = this.quoteIdentifier(options.tableAs);
} else if (!Array.isArray(mainTable.name) && mainTable.model) {
mainTable.as = this.quoteIdentifier(mainTable.model.name);
}

mainTable.quotedName = !Array.isArray(mainTable.name) ? this.quoteTable(mainTable.name) : tableName.map(t => {
return Array.isArray(t) ? this.quoteTable(t[0], t[1]) : this.quoteTable(t, true);
}).join(', ');
this.resolveTableNameOptions(tableName, options, mainTable);

if (subQuery && attributes.main) {
for (const keyAtt of mainTable.model.primaryKeyAttributes) {
Expand Down Expand Up @@ -1397,7 +1388,7 @@ export class AbstractQueryGenerator {

// Add GROUP BY to sub or main query
if (options.group) {
options.group = Array.isArray(options.group) ? options.group.map(t => this.aliasGrouping(t, model, mainTable.as, options)).join(', ') : this.aliasGrouping(options.group, model, mainTable.as, options);
options.group = this.normalizeGrouping(model, mainTable, options);

if (subQuery && options.group) {
subQueryItems.push(` GROUP BY ${options.group}`);
Expand Down Expand Up @@ -1473,6 +1464,22 @@ export class AbstractQueryGenerator {
return `${query};`;
}

resolveTableNameOptions(tableName, options, mainTable) {
if (options.tableAs) {
mainTable.as = this.quoteIdentifier(options.tableAs);
} else if (!Array.isArray(mainTable.name) && mainTable.model) {
mainTable.as = this.quoteIdentifier(mainTable.model.name);
}

mainTable.quotedName = !Array.isArray(mainTable.name) ? this.quoteTable(mainTable.name) : tableName.map(t => {
return Array.isArray(t) ? this.quoteTable(t[0], t[1]) : this.quoteTable(t, true);
}).join(', ');
}

normalizeGrouping(model, mainTable, options) {
return Array.isArray(options.group) ? options.group.map(t => this.aliasGrouping(t, model, mainTable.as, options)).join(', ') : this.aliasGrouping(options.group, model, mainTable.as, options);
}

aliasGrouping(field, model, tableName, options) {
const src = Array.isArray(field) ? field[0] : field;

Expand Down
79 changes: 79 additions & 0 deletions src/dialects/mysql/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,85 @@ export class MySqlQueryGenerator extends AbstractQueryGenerator {
]);
}

_buildJsonObjectSql(tableName, attributes) {
return `json_object(${attributes.map(([_, alias]) => `'${alias}', (SELECT \`${tableName}\`.\`${alias}\` AS \`${alias}\`)`).join(', ')})`;
}

// returns an array of pair aliased attribute with alias. ie [['YEAR(`createdAt`)', 'creationYear']]
_getAttributesWithAliases(attributes, options, mainTable) {
const escapedAttributes = this.escapeAttributes(attributes, options, mainTable.as);

// replace does this 'YEAR(`createdAt`) AS `creationYear`' -> ['YEAR(`createdAt`)', 'creationYear']
return attributes.map((attr, i) => (typeof attr === 'string' ? [attr, attr] : escapedAttributes[i].replace(/(.*?) AS `(.*?)`/g, (_, b, c) => [b, c]).split(',')));
}

selectQuery(tableName, options, model) {

if (!options?.json === true) {
return super.selectQuery(tableName, options, model);
}

// TODO move up as constant
const rootSelectSql = 'SELECT coalesce(JSON_ARRAYAGG(`root`), json_array()) AS `root`';

const { where } = options;
let { attributes } = options;

if (Array.isArray(attributes[0]) && attributes[0][1] === 'count') {
return `SELECT count(*) AS \`count\` FROM \`${tableName}\`;`;
}

const mainTable = {
name: tableName,
quotedName: null,
as: null,
model,
};

super.resolveTableNameOptions(tableName, options, mainTable);
attributes = this._getAttributesWithAliases(attributes, options, mainTable);

let groupBySql = '';
if (options.group) {
options.group = super.normalizeGrouping(model, mainTable, options);
if (options.group) {
groupBySql = ` GROUP BY ${options.group}`;
}
}

let orderBySql = '';
if (options.order) {
const orders = super.getQueryOrders(options, model);
if (orders.mainQueryOrder.length > 0) {
orderBySql = ` ORDER BY ${orders.mainQueryOrder.join(', ')}`;
}
}

let havingSql = '';
if (options.having) {
const conditions = super.getWhereConditions(options.having, tableName, model, options, false);
if (conditions) {
havingSql = ` HAVING ${conditions}`;
}
}

return `
${rootSelectSql}
FROM
(SELECT ${this._buildJsonObjectSql('_0_root.base', attributes)} AS \`root\`
FROM
(SELECT ${groupBySql ? attributes.map(([attr, alias]) => `${attr} AS \`${alias}\``).join(', ') : '*'}
FROM \`${tableName}\`
${super.whereQuery(where)}
${groupBySql}
${havingSql}
${orderBySql}
${super.addLimitAndOffset(options, model)}
) AS \`_0_root.base\`
) AS \`_1_root\`;
`.replace(/\s\s+/g, ' ').trim();
}

handleSequelizeMethod(smth, tableName, factory, options, prepend) {
if (smth instanceof Utils.Json) {
// Parse nested object
Expand Down
32 changes: 31 additions & 1 deletion test/integration/model/findAll.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ describe(Support.getTestDialectTeaser('Model'), () => {
aBool: DataTypes.BOOLEAN,
binary: DataTypes.STRING(16, true),
});
this.Project = this.sequelize.define('Project', {
name: DataTypes.STRING,
});

this.User.hasMany(this.Project);
this.Project.belongsTo(this.User);

await this.User.sync({ force: true });
await this.sequelize.sync({ force: true });
});

describe('findAll', () => {
Expand Down Expand Up @@ -887,6 +893,12 @@ describe(Support.getTestDialectTeaser('Model'), () => {
beforeEach(async function () {
const user = await this.User.create({ username: 'barfooz' });
this.user = user;

const project = await this.Project.create({
name: 'Investigate',
});

await user.setProjects([project]);
});

it('should return a DAO when queryOptions are not set', async function () {
Expand All @@ -910,6 +922,24 @@ describe(Support.getTestDialectTeaser('Model'), () => {
expect(users[0]).to.be.instanceOf(Object);
}
});

it('should return DAO data when json is not specified', async function () {
const users = await this.User.findAll({ where: { username: 'barfooz' }, include: [this.Project] });
for (const user of users) {
expect(user).to.be.instanceOf(this.User);
expect(user.Projects[0]).to.be.instanceOf(this.Project);
}
});

it.skip('should return json data when json is true', async function () {
const users = await this.User.findAll({ where: { username: 'barfooz' }, json: true, include: [this.Project] });
for (const user of users) {
expect(user).to.not.be.instanceOf(this.User);
expect(user).to.be.instanceOf(Object);
expect(user.Projects[0]).to.not.be.instanceOf(this.Project);
expect(user.Projects[0]).to.be.instanceOf(Object);
}
});
});

describe('include all', () => {
Expand Down
Loading