Skip to content

Commit b14c10a

Browse files
committed
Save Direct updated
1 parent d02f1ab commit b14c10a

File tree

8 files changed

+290
-17
lines changed

8 files changed

+290
-17
lines changed

src/drivers/base/BaseDriver.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import EntityAccessError from "../../common/EntityAccessError.js";
44
import "../../common/IDisposable.js";
55

66
import QueryCompiler from "../../compiler/QueryCompiler.js";
7+
import { IColumn } from "../../decorators/IColumn.js";
78
import EntityType from "../../entity-query/EntityType.js";
89
import Migrations from "../../migrations/Migrations.js";
9-
import ChangeEntry from "../../model/changes/ChangeEntry.js";
10+
import ChangeEntry, { IChange } from "../../model/changes/ChangeEntry.js";
1011
import { BinaryExpression, Constant, DeleteStatement, ExistsExpression, Expression, ExpressionAs, Identifier, InsertStatement, NotExits, NumberLiteral, ReturnUpdated, SelectStatement, TableLiteral, UnionAllStatement, UpdateStatement, UpsertStatement, ValuesStatement } from "../../query/ast/Expressions.js";
1112

1213
export interface IRecord {
@@ -229,6 +230,97 @@ export abstract class BaseDriver {
229230
/** Must dispose ObjectPools */
230231
abstract dispose();
231232

233+
abstract insertQuery(type: EntityType, entity): { text: string, values: any[] };
234+
235+
updateQuery(type: EntityType, entity: any, changes?: Map<IColumn, IChange>): { text: string; values: any[]; } {
236+
let where = "";
237+
let setParams = "";
238+
let returning = "";
239+
const values = [];
240+
let i = 1;
241+
if (changes) {
242+
for (const [iterator, value] of changes.entries()) {
243+
if (iterator.computed) {
244+
if (returning) {
245+
returning += ",";
246+
}
247+
returning += iterator.columnName + " as " + this.compiler.quote(iterator.name);
248+
continue;
249+
}
250+
if (setParams) {
251+
setParams += ",\r\n\t\t";
252+
}
253+
setParams += `${iterator.columnName} = $${i++}`;
254+
values.push(value.newValue);
255+
}
256+
for (const iterator of type.keys) {
257+
if(where) {
258+
where += "\r\n\t\tAND ";
259+
}
260+
where += `${iterator.columnName} = $${i++}`;
261+
values.push(entity[iterator.name]);
262+
continue;
263+
264+
}
265+
} else {
266+
for (const iterator of type.columns) {
267+
if (iterator.key) {
268+
if(where) {
269+
where += "\r\n\t\tAND ";
270+
}
271+
where += `${iterator.columnName} = $${i++}`;
272+
values.push(entity[iterator.name]);
273+
continue;
274+
}
275+
if (setParams) {
276+
setParams += ",\r\n\t\t";
277+
}
278+
setParams += `${iterator.columnName} = $${i++}`;
279+
values.push(entity[iterator.name]);
280+
}
281+
}
282+
const text = `UPDATE ${type.fullyQualifiedTableName}\r\n\tSET ${setParams}\r\n\tWHERE ${where}`;
283+
return { text, values };
284+
}
285+
286+
deleteQuery(type: EntityType, entity: any): { text: string; values: any[]; } {
287+
let where = "";
288+
const values = [];
289+
let i = 1;
290+
for (const iterator of type.keys) {
291+
if(where) {
292+
where += "\r\n\t\tAND ";
293+
}
294+
where += `${iterator.columnName} = $${i++}`;
295+
values.push(entity[iterator.name]);
296+
}
297+
const text = `DELETE FROM ${type.fullyQualifiedTableName}\r\n\tWHERE ${where}`;
298+
return { text, values };
299+
}
300+
301+
selectQueryWithKeys(type: EntityType, entity) {
302+
let where = "";
303+
let columns = "";
304+
const values = [];
305+
let i = 1;
306+
for (const iterator of type.columns) {
307+
if (iterator.key) {
308+
if(where) {
309+
where += "\r\n\t\tAND ";
310+
}
311+
where += `${iterator.columnName} = $${i++}`;
312+
values.push(entity[iterator.name]);
313+
continue;
314+
}
315+
if (columns) {
316+
columns += "\r\n\t\t";
317+
}
318+
columns += `${iterator.columnName} as ${this.compiler.escapeLiteral(iterator.name)}`;
319+
}
320+
const text = `SELECT ${columns}\r\n\tFROM ${type.fullyQualifiedTableName}\r\n\tWHERE ${where}`;
321+
return { text, values };
322+
}
323+
232324
createSelectWithKeysExpression(type: EntityType, check: any, returnFields: Expression[] ) {
233325
let where = null as Expression;
234326
for (const key in check) {

src/drivers/postgres/PostgreSqlDriver.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable no-console */
22
import ObjectPool, { IPooledObject } from "../../common/ObjectPool.js";
33
import QueryCompiler from "../../compiler/QueryCompiler.js";
4+
import EntityType from "../../entity-query/EntityType.js";
45
import Migrations from "../../migrations/Migrations.js";
56
import PostgresAutomaticMigrations from "../../migrations/postgres/PostgresAutomaticMigrations.js";
67
import { BaseConnection, BaseDriver, EntityTransaction, IDbConnectionString, IDbReader, IQuery, toQuery } from "../base/BaseDriver.js";
@@ -109,6 +110,38 @@ export default class PostgreSqlDriver extends BaseDriver {
109110
});
110111
}
111112

113+
insertQuery(type: EntityType, entity: any): { text: string; values: any[]; } {
114+
let fields = "";
115+
let valueParams = "";
116+
let returning = "";
117+
const values = [];
118+
let i = 1;
119+
for (const iterator of type.columns) {
120+
if (iterator.generated || iterator.computed) {
121+
if (returning) {
122+
returning += ",";
123+
} else {
124+
returning = "RETURNING ";
125+
}
126+
returning += iterator.columnName + " as " + this.compiler.quote(iterator.name);
127+
continue;
128+
}
129+
const value = entity[iterator.name];
130+
if (value === void 0) {
131+
continue;
132+
}
133+
if (fields) {
134+
fields += ",";
135+
valueParams += ",";
136+
}
137+
fields += iterator.columnName;
138+
valueParams += `$${i++}`;
139+
values.push(value);
140+
}
141+
const text = `INSERT INTO ${type.fullyQualifiedTableName}(${fields}) VALUES (${valueParams}) ${returning}`;
142+
return { text, values };
143+
}
144+
112145
dispose() {
113146
this.pool?.dispose().catch(console.error);
114147
}

src/drivers/sql-server/SqlServerDriver.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import SqlServerQueryCompiler from "./SqlServerQueryCompiler.js";
77
import SqlServerAutomaticMigrations from "../../migrations/sql-server/SqlServerAutomaticMigrations.js";
88
import { SqlServerLiteral } from "./SqlServerLiteral.js";
99
import TimedCache from "../../common/cache/TimedCache.js";
10+
import EntityType from "../../entity-query/EntityType.js";
1011

1112
export type ISqlServerConnectionString = IDbConnectionString & sql.config;
1213

@@ -25,6 +26,39 @@ export default class SqlServerDriver extends BaseDriver {
2526
config.server = config.host;
2627
}
2728

29+
insertQuery(type: EntityType, entity: any): { text: string; values: any[]; } {
30+
let fields = "";
31+
let valueParams = "";
32+
const values = [];
33+
let returning = "";
34+
let i = 1;
35+
for (const iterator of type.columns) {
36+
if (iterator.generated || iterator.computed) {
37+
if (returning) {
38+
returning += ",";
39+
} else {
40+
returning = "OUTPUT ";
41+
}
42+
returning += "INSERTED." + iterator.columnName + " as " + this.compiler.quote(iterator.name);
43+
continue;
44+
}
45+
const field = iterator.columnName;
46+
const value = entity[iterator.name];
47+
if (value === void 0) {
48+
continue;
49+
}
50+
values.push(value);
51+
if (fields) {
52+
fields += ",";
53+
valueParams += ",";
54+
}
55+
fields += field;
56+
valueParams += `$${i++}`;
57+
}
58+
const text = `INSERT INTO ${type.fullyQualifiedTableName}(${fields}) ${returning} VALUES (${valueParams})`;
59+
return { text, values };
60+
}
61+
2862
dispose() {
2963
// do nothing
3064
}

src/entity-query/EntityType.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default class EntityType {
4040
public readonly keys: IColumn[] = [];
4141

4242
public readonly nonKeys: IColumn[] = [];
43+
public readonly fullyQualifiedTableName: string;
4344

4445
@InstanceCache
4546
public get fullyQualifiedName() {
@@ -67,6 +68,7 @@ export default class EntityType {
6768
this.schema = original.schema ? (namingConvention ? namingConvention(original.schema) : original.schema) : original.schema;
6869
this.entityName = original.entityName;
6970
this.doNotCreate = original.doNotCreate;
71+
this.fullyQualifiedTableName = this.schema ? `${this.schema}.${this.name}` : this.name;
7072
}
7173

7274
[addOrCreateColumnSymbol](name: string): IColumn {

src/model/EntityContext.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -230,31 +230,36 @@ export default class EntityContext {
230230
}
231231

232232
private async saveChangesInternalWithoutEvents(options?: ISaveOptions) {
233+
const signal = options?.signal;
233234
const copy = Array.from(this.changeSet.getChanges()) as ChangeEntry[];
235+
const { connection } = this;
234236
for (const iterator of copy) {
235237
switch (iterator.status) {
236238
case "inserted":
237239
// const insert = iterator.type.getInsertStatement();
238240
// we are choosing not to create one complicated SQL as generation
239241
// of insert requires checking if value is supplied or not, if not, we have to choose
240242
// default value
241-
const insert = this.driver.createInsertExpression(iterator.type, iterator.entity);
242-
const r = await this.executeExpression(insert, options);
243-
iterator.apply(r);
243+
// const insert = this.driver.createInsertExpression(iterator.type, iterator.entity);
244+
// const r = await this.executeExpression(insert, options);
245+
const insert = this.driver.insertQuery(iterator.type, iterator.entity);
246+
const r = await connection.executeQuery(insert, signal);
247+
iterator.apply(r.rows[0]);
244248
break;
245249
case "modified":
246250
// this will update the modified map
247251
iterator.detect();
248252
if (iterator.modified.size > 0) {
249-
const update = this.driver.createUpdateExpression(iterator);
250-
const r1 = await this.executeExpression(update, options);
251-
iterator.apply(r1 ?? {});
253+
// const update = this.driver.createUpdateExpression(iterator);
254+
const update = this.driver.updateQuery(iterator.type, iterator.entity, iterator.modified);
255+
const r1 = await connection.executeQuery(update, signal);
256+
iterator.apply(r1.rows?.[0] ?? {});
252257
}
253258
break;
254259
case "deleted":
255-
const deleteQuery = this.driver.createDeleteExpression(iterator.type, iterator.entity);
260+
const deleteQuery = this.driver.deleteQuery(iterator.type, iterator.entity);
256261
if (deleteQuery) {
257-
await this.executeExpression(deleteQuery, options);
262+
await connection.executeQuery(deleteQuery, signal);
258263
}
259264
iterator.apply({});
260265
break;

src/model/EntitySource.ts

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,111 @@ export type ISaveDirect<T> = {
5555
changes?: never
5656
};
5757

58-
export class EntitySource<T = any> {
58+
export class EntityStatements<T = any> {
59+
60+
private readonly model: EntityType;
61+
private readonly context: EntityContext;
62+
63+
constructor(private source: EntitySource<T>) {
64+
this.model = source[modelSymbol];
65+
this.context = source[contextSymbol];
66+
}
67+
68+
async select(keys: Partial<T>, loadChangeEntry = false) {
69+
const q = this.context.driver.selectQueryWithKeys(this.model, keys);
70+
const r = await this.context.connection.executeQuery(q);
71+
const result = r.rows[0];
72+
if (loadChangeEntry) {
73+
const ce = this.context.changeSet.getEntry(result);
74+
ce.apply(result);
75+
return ce.entity;
76+
}
77+
return result;
78+
}
79+
80+
async insert(entity: Partial<T>, loadChangeEntry = false) {
81+
const q = this.context.driver.insertQuery(this.model, entity);
82+
const r = await this.context.connection.executeQuery(q);
83+
const result = r.rows[0];
84+
if (loadChangeEntry) {
85+
const ce = this.context.changeSet.getEntry(result);
86+
ce.apply(result);
87+
return ce.entity;
88+
}
89+
return r.rows[0];
90+
}
91+
92+
async update(entity: Partial<T>, loadChangeEntry = false) {
93+
const q = this.context.driver.updateQuery(this.model, entity);
94+
const r = await this.context.connection.executeQuery(q);
95+
const result = r.rows?.[0];
96+
if (loadChangeEntry) {
97+
const ce = this.context.changeSet.getEntry(result);
98+
ce.apply(result ?? {});
99+
return ce.entity;
100+
}
101+
return r.rows[0];
102+
}
103+
104+
async selectOrInsert(entity: Partial<T>, retry = 3) {
105+
const tx = this.context.connection.currentTransaction;
106+
let tid: string;
107+
if (tx) {
108+
tid = `txp_${Date.now()}`;
109+
await tx.save(tid);
110+
}
111+
112+
try {
113+
const r = await this.select(entity);
114+
if (r) {
115+
return r;
116+
}
117+
return await this.insert(entity);
118+
} catch (error) {
119+
retry --;
120+
if(retry > 0) {
121+
if (tid) {
122+
await tx.rollbackTo(tid);
123+
}
124+
await sleep(300);
125+
return await this.selectOrInsert(entity, retry);
126+
}
127+
throw error;
128+
}
129+
}
130+
131+
async upsert(entity: Partial<T>, retry = 3) {
132+
133+
const tx = this.context.connection.currentTransaction;
134+
let tid: string;
135+
if (tx) {
136+
tid = `txp_${Date.now()}`;
137+
await tx.save(tid);
138+
}
59139

60-
public statements = {
140+
try {
141+
const r = await this.update(entity);
142+
if (r) {
143+
return r;
144+
}
145+
return await this.insert(entity);
146+
} catch (error) {
147+
retry --;
148+
if(retry > 0) {
149+
if (tid) {
150+
await tx.rollbackTo(tid);
151+
}
152+
await sleep(300);
153+
return await this.upsert(entity, retry);
154+
}
155+
throw error;
156+
}
157+
}
158+
}
159+
160+
export class EntitySource<T = any> {
61161

62-
};
162+
public statements = new EntityStatements<T>(this);
63163

64164
get [modelSymbol]() {
65165
return this.model;

0 commit comments

Comments
 (0)