Skip to content

Commit e135620

Browse files
committed
fix(sql): limit with joins
json type for object literals. fixes #160
1 parent a6f5734 commit e135620

File tree

6 files changed

+78
-79
lines changed

6 files changed

+78
-79
lines changed

packages/mysql/src/mysql-platform.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export class MySQLPlatform extends DefaultPlatform {
2929
this.nativeTypeInformation.set('longblob', { needsIndexPrefix: true, defaultIndexSize: 767 });
3030

3131
this.addType(ReflectionKind.class, 'json');
32+
this.addType(ReflectionKind.objectLiteral, 'json');
3233
this.addType(ReflectionKind.array, 'json');
3334
this.addType(ReflectionKind.union, 'json');
3435

packages/orm-integration/src/bookstore.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@ import { isArray } from '@deepkit/core';
77
import { Group } from './bookstore/group';
88
import { DatabaseFactory } from './test';
99

10-
class BookModeration {
11-
locked: boolean = false;
10+
interface BookModeration {
11+
locked: boolean;
1212

1313
maxDate?: Date;
1414

1515
admin?: User;
1616

17-
moderators: User[] = [];
17+
moderators: User[];
1818
}
1919

2020
@entity.name('book')
2121
class Book {
2222
public id?: number & PrimaryKey & AutoIncrement;
2323

24-
moderation: BookModeration = new BookModeration;
24+
moderation: BookModeration = { locked: false, moderators: [] };
2525

2626
constructor(
2727
public author: User & Reference,
@@ -431,14 +431,12 @@ export const bookstoreTests = {
431431
const book1DB = await database.query(Book).filter({ author: peter }).findOne();
432432
expect(book1DB.title).toBe('Peters book');
433433
expect(book1DB.moderation === book1.moderation).toBe(false);
434-
expect(book1DB.moderation).toBeInstanceOf(BookModeration);
435434
expect(book1DB.moderation.locked).toBe(true);
436435
}
437436

438437
{
439438
const book2DB = await database.query(Book).filter({ author: herbert }).findOne();
440439
expect(book2DB.title).toBe('Herberts book');
441-
expect(book2DB.moderation).toBeInstanceOf(BookModeration);
442440
expect(book2DB.moderation.locked).toBe(false);
443441
expect(book2DB.moderation.admin).toBeInstanceOf(User);
444442
expect(book2DB.moderation.admin?.name).toBe('Admin');

packages/orm-integration/src/users.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class User {
1010

1111
posts?: Post[] & BackReference;
1212

13-
groups?: Group[] & BackReference<{via: typeof UserGroup}>;
13+
groups?: Group[] & BackReference<{via: UserGroup}>;
1414

1515
constructor(public username: string) {
1616
}
@@ -117,4 +117,34 @@ export const usersTests = {
117117
}
118118
database.disconnect();
119119
},
120+
121+
async limitWithJoins(databaseFactory: DatabaseFactory) {
122+
const database = await databaseFactory(entities);
123+
124+
const user1 = new User('User1');
125+
const user2 = new User('User2');
126+
127+
const post1 = new Post(user1, 'Post 1');
128+
const post2 = new Post(user1, 'Post 2');
129+
const post3 = new Post(user1, 'Post 3');
130+
131+
await database.persist(user1, user2, post1, post2, post3);
132+
133+
{
134+
const user = await database.query(User).joinWith('posts').findOne();
135+
expect(user).toBeInstanceOf(User);
136+
expect(user.posts!.length).toBe(3);
137+
expect(user.posts![0]).toBeInstanceOf(Post);
138+
}
139+
140+
{
141+
const users = await database.query(User).joinWith('posts').limit(1).find();
142+
expect(users.length).toBe(1);
143+
const user = users[0]!;
144+
145+
expect(user).toBeInstanceOf(User);
146+
expect(user.posts!.length).toBe(3);
147+
expect(user.posts![0]).toBeInstanceOf(Post);
148+
}
149+
}
120150
};

packages/postgres/src/postgres-platform.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class PostgresPlatform extends DefaultPlatform {
6767
this.addType(ReflectionKind.boolean, 'boolean');
6868

6969
this.addType(ReflectionKind.class, 'jsonb');
70+
this.addType(ReflectionKind.objectLiteral, 'jsonb');
7071
this.addType(ReflectionKind.array, 'jsonb');
7172
this.addType(ReflectionKind.union, 'jsonb');
7273

packages/sql/src/sql-adapter.ts

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -259,51 +259,17 @@ export class SQLQueryResolver<T extends OrmEntity> extends GenericQueryResolver<
259259
if (formatterFrame) formatterFrame.end();
260260

261261
return results;
262-
} catch (error) {
263-
throw error;
264-
throw new DatabaseError(`Could not query ${this.classSchema.getClassName()} due to SQL error ${error}.\nSQL: ${sql.sql}\nParams: ${JSON.stringify(sql.params)}`);
262+
} catch (error: any) {
263+
throw new DatabaseError(`Could not query ${this.classSchema.getClassName()} due to SQL error ${error}.\nSQL: ${sql.sql}\nParams: ${JSON.stringify(sql.params)}. Error: ${error}`);
265264
} finally {
266265
connection.release();
267266
}
268267
}
269268

270269
async findOneOrUndefined(model: SQLQueryModel<T>): Promise<T | undefined> {
271-
const sqlBuilderFrame = this.session.stopwatch ? this.session.stopwatch.start('SQL Builder') : undefined;
272-
const sqlBuilder = new SqlBuilder(this.platform);
273-
const sql = sqlBuilder.select(this.classSchema, model);
274-
if (sqlBuilderFrame) sqlBuilderFrame.end();
275-
276-
const connectionFrame = this.session.stopwatch ? this.session.stopwatch.start('Connection acquisition') : undefined;
277-
const connection = await this.connectionPool.getConnection(this.session.logger, this.session.assignedTransaction, this.session.stopwatch);
278-
if (connectionFrame) connectionFrame.end();
279-
let row: any;
280-
281-
try {
282-
row = await connection.execAndReturnSingle(sql.sql, sql.params);
283-
if (!row) return;
284-
} catch (error) {
285-
throw new DatabaseError(`Could not query ${this.classSchema.getClassName()} due to SQL error ${error}.\nSQL: ${sql.sql}\nParams: ${JSON.stringify(sql.params)}`);
286-
} finally {
287-
connection.release();
288-
}
289-
290-
if (model.isAggregate() || model.sqlSelect) {
291-
//when aggregate the field types could be completely different, so don't normalize
292-
return row;
293-
}
294-
295-
const formatterFrame = this.session.stopwatch ? this.session.stopwatch.start('Formatter') : undefined;
296-
try {
297-
const formatter = this.createFormatter(model.withIdentityMap);
298-
if (model.hasJoins()) {
299-
const [converted] = sqlBuilder.convertRows(this.classSchema, model, [row]);
300-
return formatter.hydrate(model, converted);
301-
} else {
302-
return formatter.hydrate(model, row);
303-
}
304-
} finally {
305-
if (formatterFrame) formatterFrame.end();
306-
}
270+
//when joins are used, it's important to fetch all rows
271+
const items = await this.find(model);
272+
return items[0];
307273
}
308274

309275
async has(model: SQLQueryModel<T>): Promise<boolean> {

packages/sql/src/sql-builder.ts

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ export class SqlBuilder {
8282
protected appendHavingSQL(sql: Sql, schema: ReflectionClass<any>, model: DatabaseQueryModel<any>, tableName: string) {
8383
if (!model.having) return;
8484

85-
// tableName = tableName || this.platform.getTableIdentifier(schema);
8685
const filter = getSqlFilter(schema, model.having, model.parameters, this.platform.serializer);
8786
const builder = this.platform.createSqlFilterBuilder(schema, tableName);
8887
builder.placeholderStrategy.offset = sql.params.length;
@@ -314,13 +313,45 @@ export class SqlBuilder {
314313
}
315314
}
316315

317-
public build<T>(schema: ReflectionClass<any>, model: SQLQueryModel<T>, head: string, withRange: boolean = true): Sql {
316+
public build<T>(schema: ReflectionClass<any>, model: SQLQueryModel<T>, head: string): Sql {
318317
const tableName = this.platform.getTableIdentifier(schema);
319-
const sql = new Sql(`${head} FROM ${tableName}`, this.params);
318+
319+
const sql = new Sql(`${head} FROM`, this.params);
320+
321+
const withRange = model.limit !== undefined || model.skip !== undefined;
322+
if (withRange && model.hasJoins()) {
323+
//wrap FROM table => FROM (SELECT * FROM table LIMIT x OFFSET x)
324+
325+
sql.append(`(SELECT * FROM ${tableName}`);
326+
this.platform.applyLimitAndOffset(sql, model.limit, model.skip);
327+
sql.append(`) as ${tableName}`);
328+
} else {
329+
sql.append(tableName);
330+
}
331+
320332
this.appendJoinSQL(sql, model, tableName);
321333
this.appendWhereSQL(sql, schema, model);
322334

323-
if (withRange) {
335+
if (model.groupBy.size) {
336+
const groupBy: string[] = [];
337+
for (const g of model.groupBy.values()) {
338+
groupBy.push(`${tableName}.${this.platform.quoteIdentifier(g)}`);
339+
}
340+
341+
sql.append('GROUP BY ' + groupBy.join(', '));
342+
}
343+
344+
this.appendHavingSQL(sql, schema, model, tableName);
345+
346+
const order: string[] = [];
347+
if (model.sort) {
348+
for (const [name, sort] of Object.entries(model.sort)) {
349+
order.push(`${tableName}.${this.platform.quoteIdentifier(name)} ${sort}`);
350+
}
351+
if (order.length) sql.append(' ORDER BY ' + (order.join(', ')));
352+
}
353+
354+
if (withRange && !model.hasJoins()) {
324355
this.platform.applyLimitAndOffset(sql, model.limit, model.skip);
325356
}
326357

@@ -351,34 +382,6 @@ export class SqlBuilder {
351382
}
352383
}
353384

354-
const tableName = this.platform.getTableIdentifier(schema);
355-
356-
const order: string[] = [];
357-
if (model.sort) {
358-
for (const [name, sort] of Object.entries(model.sort)) {
359-
order.push(`${tableName}.${this.platform.quoteIdentifier(name)} ${sort}`);
360-
}
361-
}
362-
363-
const sql = this.build(schema, model, 'SELECT ' + (manualSelect || this.sqlSelect).join(', '), false);
364-
365-
if (model.groupBy.size) {
366-
const groupBy: string[] = [];
367-
for (const g of model.groupBy.values()) {
368-
groupBy.push(`${tableName}.${this.platform.quoteIdentifier(g)}`);
369-
}
370-
371-
sql.append('GROUP BY ' + groupBy.join(', '));
372-
}
373-
374-
this.appendHavingSQL(sql, schema, model, tableName);
375-
376-
if (order.length) {
377-
sql.append(' ORDER BY ' + (order.join(', ')));
378-
}
379-
380-
this.platform.applyLimitAndOffset(sql, model.limit, model.skip);
381-
382-
return sql;
385+
return this.build(schema, model, 'SELECT ' + (manualSelect || this.sqlSelect).join(', '));
383386
}
384387
}

0 commit comments

Comments
 (0)