Skip to content

Commit 981f643

Browse files
authored
perf: avoid unnecessary post-mutation reads (#325)
1 parent d475599 commit 981f643

File tree

8 files changed

+260
-113
lines changed

8 files changed

+260
-113
lines changed

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
UpdateResult,
88
type Compilable,
99
type IsolationLevel,
10+
type QueryResult,
1011
type SelectQueryBuilder,
1112
} from 'kysely';
1213
import { nanoid } from 'nanoid';
@@ -248,6 +249,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
248249
data: any,
249250
fromRelation?: FromRelationContext<Schema>,
250251
creatingForDelegate = false,
252+
returnFields?: string[],
251253
): Promise<unknown> {
252254
const modelDef = this.requireModel(model);
253255

@@ -339,12 +341,15 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
339341
}
340342

341343
const updatedData = this.fillGeneratedAndDefaultValues(modelDef, createFields);
342-
const idFields = requireIdFields(this.schema, model);
344+
345+
// return id fields if no returnFields specified
346+
returnFields = returnFields ?? requireIdFields(this.schema, model);
347+
343348
const query = kysely
344349
.insertInto(model)
345350
.$if(Object.keys(updatedData).length === 0, (qb) => qb.defaultValues())
346351
.$if(Object.keys(updatedData).length > 0, (qb) => qb.values(updatedData))
347-
.returning(idFields as any)
352+
.returning(returnFields as any)
348353
.modifyEnd(
349354
this.makeContextComment({
350355
model,
@@ -661,6 +666,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
661666
input: { data: any; skipDuplicates?: boolean },
662667
returnData: ReturnData,
663668
fromRelation?: FromRelationContext<Schema>,
669+
fieldsToReturn?: string[],
664670
): Promise<Result> {
665671
if (!input.data || (Array.isArray(input.data) && input.data.length === 0)) {
666672
// nothing todo
@@ -763,8 +769,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
763769
const result = await this.executeQuery(kysely, query, 'createMany');
764770
return { count: Number(result.numAffectedRows) } as Result;
765771
} else {
766-
const idFields = requireIdFields(this.schema, model);
767-
const result = await query.returning(idFields as any).execute();
772+
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
773+
const result = await query.returning(fieldsToReturn as any).execute();
768774
return result as Result;
769775
}
770776
}
@@ -899,6 +905,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
899905
fromRelation?: FromRelationContext<Schema>,
900906
allowRelationUpdate = true,
901907
throwIfNotFound = true,
908+
fieldsToReturn?: string[],
902909
): Promise<unknown> {
903910
if (!data || typeof data !== 'object') {
904911
throw new InternalError('data must be an object');
@@ -1044,12 +1051,12 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
10441051
// nothing to update, return the filter so that the caller can identify the entity
10451052
return combinedWhere;
10461053
} else {
1047-
const idFields = requireIdFields(this.schema, model);
1054+
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
10481055
const query = kysely
10491056
.updateTable(model)
10501057
.where(() => this.dialect.buildFilter(model, model, combinedWhere))
10511058
.set(updateFields)
1052-
.returning(idFields as any)
1059+
.returning(fieldsToReturn as any)
10531060
.modifyEnd(
10541061
this.makeContextComment({
10551062
model,
@@ -1058,16 +1065,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
10581065
);
10591066

10601067
const updatedEntity = await this.executeQueryTakeFirst(kysely, query, 'update');
1061-
1062-
// try {
1063-
// updatedEntity = await this.executeQueryTakeFirst(kysely, query, 'update');
1064-
// } catch (err) {
1065-
// const { sql, parameters } = query.compile();
1066-
// throw new QueryError(
1067-
// `Error during update: ${err}, sql: ${sql}, parameters: ${parameters}`
1068-
// );
1069-
// }
1070-
10711068
if (!updatedEntity) {
10721069
if (throwIfNotFound) {
10731070
throw new NotFoundError(model);
@@ -1214,6 +1211,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
12141211
limit: number | undefined,
12151212
returnData: ReturnData,
12161213
filterModel?: GetModels<Schema>,
1214+
fieldsToReturn?: string[],
12171215
): Promise<Result> {
12181216
if (typeof data !== 'object') {
12191217
throw new InternalError('data must be an object');
@@ -1302,8 +1300,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
13021300
const result = await this.executeQuery(kysely, query, 'update');
13031301
return { count: Number(result.numAffectedRows) } as Result;
13041302
} else {
1305-
const idFields = requireIdFields(this.schema, model);
1306-
const finalQuery = query.returning(idFields as any);
1303+
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
1304+
const finalQuery = query.returning(fieldsToReturn as any);
13071305
const result = await this.executeQuery(kysely, finalQuery, 'update');
13081306
return result.rows as Result;
13091307
}
@@ -1861,7 +1859,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
18611859
expectedDeleteCount = deleteConditions.length;
18621860
}
18631861

1864-
let deleteResult: { count: number };
1862+
let deleteResult: QueryResult<unknown>;
18651863
let deleteFromModel: GetModels<Schema>;
18661864
const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field);
18671865

@@ -1926,7 +1924,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
19261924
}
19271925

19281926
// validate result
1929-
if (throwForNotFound && expectedDeleteCount > deleteResult.count) {
1927+
if (throwForNotFound && expectedDeleteCount > deleteResult.rows.length) {
19301928
// some entities were not deleted
19311929
throw new NotFoundError(deleteFromModel);
19321930
}
@@ -1944,7 +1942,8 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
19441942
where: any,
19451943
limit?: number,
19461944
filterModel?: GetModels<Schema>,
1947-
): Promise<{ count: number }> {
1945+
fieldsToReturn?: string[],
1946+
): Promise<QueryResult<unknown>> {
19481947
filterModel ??= model;
19491948

19501949
const modelDef = this.requireModel(model);
@@ -1957,7 +1956,9 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
19571956
return this.processBaseModelDelete(kysely, modelDef.baseModel, where, limit, filterModel);
19581957
}
19591958

1960-
let query = kysely.deleteFrom(model);
1959+
fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model);
1960+
let query = kysely.deleteFrom(model).returning(fieldsToReturn as any);
1961+
19611962
let needIdFilter = false;
19621963

19631964
if (limit !== undefined && !this.dialect.supportsDeleteWithLimit) {
@@ -1999,8 +2000,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
19992000
await this.processDelegateRelationDelete(kysely, modelDef, where, limit);
20002001

20012002
query = query.modifyEnd(this.makeContextComment({ model, operation: 'delete' }));
2002-
const result = await this.executeQuery(kysely, query, 'delete');
2003-
return { count: Number(result.numAffectedRows) };
2003+
return this.executeQuery(kysely, query, 'delete');
20042004
}
20052005

20062006
private async processDelegateRelationDelete(
@@ -2140,4 +2140,56 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
21402140
}
21412141
return result.rows[0];
21422142
}
2143+
2144+
protected mutationNeedsReadBack(model: string, args: any) {
2145+
if (this.hasPolicyEnabled) {
2146+
// TODO: refactor this check
2147+
// policy enforcement always requires read back
2148+
return { needReadBack: true, selectedFields: undefined };
2149+
}
2150+
2151+
if (args.include && typeof args.include === 'object' && Object.keys(args.include).length > 0) {
2152+
// includes present, need read back to fetch relations
2153+
return { needReadBack: true, selectedFields: undefined };
2154+
}
2155+
2156+
const modelDef = this.requireModel(model);
2157+
2158+
if (modelDef.baseModel || modelDef.isDelegate) {
2159+
// polymorphic model, need read back
2160+
return { needReadBack: true, selectedFields: undefined };
2161+
}
2162+
2163+
const allFields = Object.keys(modelDef.fields);
2164+
const relationFields = Object.values(modelDef.fields)
2165+
.filter((f) => f.relation)
2166+
.map((f) => f.name);
2167+
const computedFields = Object.values(modelDef.fields)
2168+
.filter((f) => f.computed)
2169+
.map((f) => f.name);
2170+
const omit = Object.entries(args.omit ?? {})
2171+
.filter(([, v]) => v)
2172+
.map(([k]) => k);
2173+
2174+
const allFieldsSelected: string[] = [];
2175+
2176+
if (!args.select || typeof args.select !== 'object') {
2177+
// all non-relation fields selected
2178+
allFieldsSelected.push(...allFields.filter((f) => !relationFields.includes(f) && !omit.includes(f)));
2179+
} else {
2180+
// explicit select
2181+
allFieldsSelected.push(
2182+
...Object.entries(args.select)
2183+
.filter(([k, v]) => v && !omit.includes(k))
2184+
.map(([k]) => k),
2185+
);
2186+
}
2187+
2188+
if (allFieldsSelected.some((f) => relationFields.includes(f) || computedFields.includes(f))) {
2189+
// relation or computed field selected, need read back
2190+
return { needReadBack: true, selectedFields: undefined };
2191+
} else {
2192+
return { needReadBack: false, selectedFields: allFieldsSelected };
2193+
}
2194+
}
21432195
}

packages/runtime/src/client/crud/operations/create.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,27 @@ export class CreateOperationHandler<Schema extends SchemaDef> extends BaseOperat
2424
}
2525

2626
private async runCreate(args: CreateArgs<Schema, GetModels<Schema>>) {
27+
// analyze if we need to read back the created record, or just return the create result
28+
const { needReadBack, selectedFields } = this.mutationNeedsReadBack(this.model, args);
29+
2730
// TODO: avoid using transaction for simple create
2831
const result = await this.safeTransaction(async (tx) => {
29-
const createResult = await this.create(tx, this.model, args.data);
30-
return this.readUnique(tx, this.model, {
31-
select: args.select,
32-
include: args.include,
33-
omit: args.omit,
34-
where: getIdValues(this.schema, this.model, createResult) as WhereInput<
35-
Schema,
36-
GetModels<Schema>,
37-
false
38-
>,
39-
});
32+
const createResult = await this.create(tx, this.model, args.data, undefined, false, selectedFields);
33+
34+
if (needReadBack) {
35+
return this.readUnique(tx, this.model, {
36+
select: args.select,
37+
include: args.include,
38+
omit: args.omit,
39+
where: getIdValues(this.schema, this.model, createResult) as WhereInput<
40+
Schema,
41+
GetModels<Schema>,
42+
false
43+
>,
44+
});
45+
} else {
46+
return createResult;
47+
}
4048
});
4149

4250
if (!result && this.hasPolicyEnabled) {
@@ -62,16 +70,24 @@ export class CreateOperationHandler<Schema extends SchemaDef> extends BaseOperat
6270
return [];
6371
}
6472

73+
// analyze if we need to read back the created record, or just return the create result
74+
const { needReadBack, selectedFields } = this.mutationNeedsReadBack(this.model, args);
75+
6576
// TODO: avoid using transaction for simple create
6677
return this.safeTransaction(async (tx) => {
67-
const createResult = await this.createMany(tx, this.model, args, true);
68-
return this.read(tx, this.model, {
69-
select: args.select,
70-
omit: args.omit,
71-
where: {
72-
OR: createResult.map((item) => getIdValues(this.schema, this.model, item) as any),
73-
} as any, // TODO: fix type
74-
});
78+
const createResult = await this.createMany(tx, this.model, args, true, undefined, selectedFields);
79+
80+
if (needReadBack) {
81+
return this.read(tx, this.model, {
82+
select: args.select,
83+
omit: args.omit,
84+
where: {
85+
OR: createResult.map((item) => getIdValues(this.schema, this.model, item) as any),
86+
} as any, // TODO: fix type
87+
});
88+
} else {
89+
return createResult;
90+
}
7591
});
7692
}
7793
}

packages/runtime/src/client/crud/operations/delete.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,42 @@ export class DeleteOperationHandler<Schema extends SchemaDef> extends BaseOperat
1818
}
1919

2020
async runDelete(args: DeleteArgs<Schema, Extract<keyof Schema['models'], string>>) {
21-
const existing = await this.readUnique(this.kysely, this.model, {
22-
select: args.select,
23-
include: args.include,
24-
omit: args.omit,
25-
where: args.where,
26-
});
21+
// analyze if we need to read back the deleted record, or just return delete result
22+
const { needReadBack, selectedFields } = this.mutationNeedsReadBack(this.model, args);
2723

2824
// TODO: avoid using transaction for simple delete
29-
await this.safeTransaction(async (tx) => {
30-
const result = await this.delete(tx, this.model, args.where);
31-
if (result.count === 0) {
25+
const result = await this.safeTransaction(async (tx) => {
26+
let preDeleteRead: any = undefined;
27+
if (needReadBack) {
28+
preDeleteRead = await this.readUnique(tx, this.model, {
29+
select: args.select,
30+
include: args.include,
31+
omit: args.omit,
32+
where: args.where,
33+
});
34+
}
35+
const deleteResult = await this.delete(tx, this.model, args.where, undefined, undefined, selectedFields);
36+
if (deleteResult.rows.length === 0) {
3237
throw new NotFoundError(this.model);
3338
}
39+
return needReadBack ? preDeleteRead : deleteResult.rows[0];
3440
});
3541

36-
if (!existing && this.hasPolicyEnabled) {
42+
if (!result && this.hasPolicyEnabled) {
3743
throw new RejectedByPolicyError(
3844
this.model,
3945
RejectedByPolicyReason.CANNOT_READ_BACK,
4046
'result is not allowed to be read back',
4147
);
4248
}
4349

44-
return existing;
50+
return result;
4551
}
4652

4753
async runDeleteMany(args: DeleteManyArgs<Schema, Extract<keyof Schema['models'], string>> | undefined) {
4854
return await this.safeTransaction(async (tx) => {
4955
const result = await this.delete(tx, this.model, args?.where, args?.limit);
50-
return result;
56+
return { count: result.rows.length };
5157
});
5258
}
5359
}

0 commit comments

Comments
 (0)