Skip to content

Commit

Permalink
feat: compatible subquery on join
Browse files Browse the repository at this point in the history
  • Loading branch information
JimmyDaddy committed Mar 23, 2023
1 parent a5812ec commit 21b12ee
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 12 deletions.
21 changes: 21 additions & 0 deletions src/spell.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,21 @@ class Spell {
return { ...parseExpr(text), __expr: true };
}

#emptySpell() {
Object.assign(this, {
columns: [],
whereConditions: [],
groups: [],
orders: [],
havingConditions: [],
joins: {},
skip: 0,
subqueryIndex: 0,
rowCount: 0,
skip: 0,
});
}

get unscoped() {
const spell = this.dup;
spell.scopes = [];
Expand Down Expand Up @@ -790,6 +805,12 @@ class Spell {
* @returns {Spell}
*/
$with(...qualifiers) {
if (this.rowCount > 0 || this.skip > 0) {
const spell = this.dup;
this.#emptySpell();
this.table = { type: 'subquery', value: spell };
}

for (const qualifier of qualifiers) {
if (isPlainObject(qualifier)) {
for (const key in qualifier) {
Expand Down
146 changes: 134 additions & 12 deletions test/integration/suite/associations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ describe('=> Associations', function() {
"Now you'll join him"
];

const comments1 = [
'Long may the sunshine!',
'Despicable outlanders!',
];

const tagNames = ['npc', 'boss', 'player'];
const topicNames = ['nephalem', 'archangel', 'demon'];

Expand Down Expand Up @@ -55,6 +60,9 @@ describe('=> Associations', function() {
await Promise.all(comments.map(content => {
return Comment.create({ content, articleId: posts[0].id });
}));
await Promise.all(comments1.map(content => {
return Comment.create({ content, articleId: posts[1].id });
}));
await mapTags(posts[0], tags.slice(0, 2));
await mapTags(posts[0], topics.slice(2, 3));
await mapTags(posts[1], tags.slice(2, 3));
Expand Down Expand Up @@ -89,10 +97,17 @@ describe('=> Associations', function() {
});

it('Bone.hasMany', async function() {
const posts = await Post.from(Post.first).with('comments');
expect(posts[0].comments.length).to.be.above(0);
expect(posts[0].comments[0]).to.be.a(Comment);
expect(posts[0].comments.map(comment => comment.content).sort()).to.eql(comments.sort());
let post = await Post.first.with('comments');
expect(post.comments.length).to.be.above(0);
expect(post.comments[0]).to.be.a(Comment);
expect(post.comments.map(comment => comment.content).sort()).to.eql(comments.sort());
post = await Post.first.offset(1).with('comments');
expect(post.comments.length).to.be.above(0);
expect(post.comments[0]).to.be.a(Comment);
expect(post.comments.map(comment => comment.content).sort()).to.eql(comments1.sort());
const posts = await Post.find().limit(100).offset(1).with('comments').limit(2);
assert.equal(posts.length, 1);
expect(posts[0].comments.map(comment => comment.content).sort()).to.eql(comments1.sort());
});

it('Bone.hasMany through', async function() {
Expand All @@ -116,19 +131,19 @@ describe('=> Associations', function() {
});

it('.with({ ...names })', async function() {
const posts = await Post.from(Post.first).with({
const posts = await Post.first.with({
attachment: {},
comments: { select: 'id, content' },
tags: {}
});
expect(posts[0].tags[0]).to.be.a(Tag);
expect(posts[0].tagMaps[0]).to.be.a(TagMap);
expect(posts[0].attachment).to.be.a(Attachment);
expect(posts[0].comments.length).to.be.above(0);
expect(posts[0].comments[0].id).to.be.ok();
expect(posts.tags[0]).to.be.a(Tag);
expect(posts.tagMaps[0]).to.be.a(TagMap);
expect(posts.attachment).to.be.a(Attachment);
expect(posts.comments.length).to.be.above(0);
expect(posts.comments[0].id).to.be.ok();
// because createdAt is not selected
assert.deepEqual(posts[0].comments[0].createdAt, undefined);
expect(posts[0].comments.map(comment => comment.content).sort()).to.eql(comments.sort());
assert.deepEqual(posts.comments[0].createdAt, undefined);
expect(posts.comments.map(comment => comment.content).sort()).to.eql(comments.sort());
});

it('.with(...names).select()', async function() {
Expand All @@ -155,6 +170,8 @@ describe('=> Associations', function() {

describe('=> Associations order / offset / limit', function() {
before(async function() {
await Post.remove({}, true);
await Comment.remove({}, true);
const post1 = await Post.create({ title: 'New Post' });
await Comment.create({ content: 'Abandon your foolish request!', articleId: post1.id });
const post2 = await Post.create({ title: 'New Post 2' });
Expand Down Expand Up @@ -203,4 +220,109 @@ describe('=> Associations order / offset / limit', function() {
const result = await Post.include('comments').select('content as cnt').order('cnt', 'desc').limit(1);
assert.equal(result.length, 1);
});

describe('should limit/offset subquery if limit/offset is set', function() {
it('on root query', async function() {
/*
sample data in db query all result(mysql):
[
{ id: 1, title: 'New Post', comments: [ { id: 2, content: 'Now you\'ll join them } ] },
{ id: 1, title: 'New Post', comments: [ { id: 1, content: 'Abandon your foolish request!' } ] },
{ id: 2, title: 'New Post 2', comments: [ { id: 2, content: 'You are too late to save the child!' } ] },
]
*/
let posts = await Post.include('comments').limit(1);
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post');
assert.equal(posts[0].comments.length, 1);

posts = await Post.include('comments').limit(1).offset(1);
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post');
assert.equal(posts[0].comments.length, 1);

posts = await Post.include('comments').limit(1).offset(2);
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post 2');
assert.equal(posts[0].comments.length, 1);
assert.equal(posts[0].comments[0].content, 'You are too late to save the child!');

/*
sample data in db query all result(mysql):
[
{ id: 2, title: 'New Post 2', comments: [ { id: 2, content: 'You are too late to save the child!' } ] },
{ id: 1, title: 'New Post', comments: [ { id: 2, content: 'Now you\'ll join them } ] },
{ id: 1, title: 'New Post', comments: [ { id: 1, content: 'Abandon your foolish request!' } ] },
]
*/
posts = await Post.include('comments').limit(1).offset(0).order('id', 'desc');
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post 2');
assert.equal(posts[0].comments.length, 1);
assert.equal(posts[0].comments[0].content, 'You are too late to save the child!');

/*
sample data in db query all result(mysql):
[
{ id: 2, title: 'New Post 2', comments: [ { id: 2, content: 'You are too late to save the child!' } ] },
{ id: 1, title: 'New Post', comments: [ { id: 1, content: 'Now you\'ll join them } ] },
{ id: 1, title: 'New Post', comments: [ { id: 1, content: 'Abandon your foolish request!' } ] },
]
*/
posts = await Post.include('comments').order('id', 'desc');
assert.equal(posts.length, 2);
assert.equal(posts[0].title, 'New Post 2');
assert.equal(posts[0].comments.length, 1);
assert.equal(posts[0].comments[0].content, 'You are too late to save the child!');
});

it('on root query order', async function() {
/*
sample data in db query all result(mysql):
[
{ id: 1, title: 'New Post', comments: [ { id: 2, content: 'Now you\'ll join them' } ] },
{ id: 2, title: 'New Post 2', comments: [ { id: 2, content: 'You are too late to save the child!' } ] },
{ id: 1, title: 'New Post', comments: [ { id: 1, content: 'Abandon your foolish request!' } ] },
]
*/
let posts = await Post.include('comments').limit(1).order('comments.id', 'desc');
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post');
assert.equal(posts[0].comments.length, 1);
assert.equal(posts[0].comments[0].content, 'Now you\'ll join them');

posts = await Post.include('comments').limit(1).offset(1).order('comments.id', 'desc');
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post 2');
assert.equal(posts[0].comments.length, 1);
assert.equal(posts[0].comments[0].content, 'You are too late to save the child!');

posts = await Post.include('comments').limit(1).offset(2).order('comments.id', 'desc');
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post');
assert.equal(posts[0].comments.length, 1);
assert.equal(posts[0].comments[0].content, 'Abandon your foolish request!');

});

it('on subquery', async function() {
let post = await Post.first.with('comments');
assert.equal(post.title, 'New Post');
assert.equal(post.comments.length, 2);
post = await Post.first.offset(1).with('comments');
assert.equal(post.title, 'New Post 2');
assert.equal(post.comments.length, 1);
});

it('on subquery with order', async function() {
let posts = await Post.all.limit(1).order('id', 'desc').with('comments');
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post 2');
assert.equal(posts[0].comments.length, 1);
posts = await Post.all.limit(1).order('id', 'asc').with('comments');
assert.equal(posts.length, 1);
assert.equal(posts[0].title, 'New Post');
assert.equal(posts[0].comments.length, 2);
});
});
});
65 changes: 65 additions & 0 deletions test/unit/spell.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,71 @@ describe('=> Spell', function() {
*/}));
});

describe('make OFFSET and LIMIT on left table takes effect while use limit/offset on the left of join', function () {
it('should work', function () {
assert.equal(Post.where({
title: {
$like: '%yoxi%',
}
}).limit(1).with('comments').where({
'comments.content': { $like: '%oo1%' },
id: {
$gte: 1
}
}).limit(1).toSqlString(), heresql(function () {
/*
SELECT `posts`.*, `comments`.*
FROM (SELECT * FROM `articles` WHERE `title` LIKE '%yoxi%' AND `gmt_deleted` IS NULL LIMIT 1) AS `posts`
LEFT JOIN `comments` AS `comments` ON `posts`.`id` = `comments`.`article_id` AND `comments`.`gmt_deleted` IS NULL
WHERE `comments`.`content` LIKE '%oo1%'
AND `posts`.`id` >= 1
LIMIT 1
*/}));
});

it('should work with offset', function () {
assert.equal(Post.where({
title: {
$like: '%yoxi%',
}
}).limit(1).offset(1).with('comments').where({
'comments.content': { $like: '%oo1%' },
id: {
$gte: 1
}
}).limit(1).toSqlString(), heresql(function () {
/*
SELECT `posts`.*, `comments`.*
FROM (SELECT * FROM `articles` WHERE `title` LIKE '%yoxi%' AND `gmt_deleted` IS NULL LIMIT 1 OFFSET 1) AS `posts`
LEFT JOIN `comments` AS `comments` ON `posts`.`id` = `comments`.`article_id` AND `comments`.`gmt_deleted` IS NULL
WHERE `comments`.`content` LIKE '%oo1%'
AND `posts`.`id` >= 1
LIMIT 1
*/}));
});

it('should work with order in subquery', function () {
assert.equal(Post.where({
title: {
$like: '%yoxi%',
}
}).limit(1).order('id', 'desc').with('comments').where({
'comments.content': { $like: '%oo1%' },
id: {
$gte: 1
}
}).limit(1).toSqlString(), heresql(function () {
/*
SELECT `posts`.*, `comments`.*
FROM (SELECT * FROM `articles` WHERE `title` LIKE '%yoxi%' AND `gmt_deleted` IS NULL ORDER BY `id` DESC LIMIT 1) AS `posts`
LEFT JOIN `comments` AS `comments` ON `posts`.`id` = `comments`.`article_id` AND `comments`.`gmt_deleted` IS NULL
WHERE `comments`.`content` LIKE '%oo1%'
AND `posts`.`id` >= 1
LIMIT 1
*/}));
});
});

it('select as', function() {
assert.equal(
Post.select("IFNULL(title, 'foo') AS title").order('title', 'desc').toString(),
Expand Down

0 comments on commit 21b12ee

Please sign in to comment.