@nymphjs/driver-postgresql 1.0.0-beta.9 → 1.0.0-beta.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,22 @@
1
- import cp from 'child_process';
2
- import { Pool, PoolClient, PoolConfig, QueryResult } from 'pg';
1
+ import pg from 'pg';
2
+ import type { Pool, PoolClient, PoolConfig, QueryResult } from 'pg';
3
3
  import format from 'pg-format';
4
+ import Cursor from 'pg-cursor';
4
5
  import {
5
6
  NymphDriver,
6
- EntityConstructor,
7
- EntityData,
8
- EntityInterface,
9
- SerializedEntityData,
7
+ type EntityConstructor,
8
+ type EntityData,
9
+ type EntityInterface,
10
+ type EntityInstanceType,
11
+ type SerializedEntityData,
12
+ type FormattedSelector,
13
+ type Options,
14
+ type Selector,
15
+ EntityUniqueConstraintError,
10
16
  InvalidParametersError,
11
17
  NotConfiguredError,
12
18
  QueryFailedError,
13
19
  UnableToConnectError,
14
- FormattedSelector,
15
- Options,
16
- Selector,
17
20
  xor,
18
21
  } from '@nymphjs/nymph';
19
22
  import { makeTableSuffix } from '@nymphjs/guid';
@@ -21,7 +24,7 @@ import { makeTableSuffix } from '@nymphjs/guid';
21
24
  import {
22
25
  PostgreSQLDriverConfig,
23
26
  PostgreSQLDriverConfigDefaults as defaults,
24
- } from './conf';
27
+ } from './conf/index.js';
25
28
 
26
29
  type PostgreSQLDriverConnection = {
27
30
  client: PoolClient;
@@ -53,10 +56,44 @@ export default class PostgreSQLDriver extends NymphDriver {
53
56
  return format.literal(input);
54
57
  }
55
58
 
59
+ static escapeNullSequences(input: string) {
60
+ // Postgres doesn't support null bytes in `text`, and it converts strings
61
+ // in JSON to `text`, so we need to escape the escape sequences for null
62
+ // bytes.
63
+ return (
64
+ input
65
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
66
+ // n so that if there's already an escape, it turns into \n
67
+ // - so that it won't match a \uFFFD that got turned into \uFFFD\uFFFD
68
+ .replace(/\\u0000/g, () => 'nu\uFFFD-')
69
+ .replace(/\\x00/g, () => 'nx\uFFFD-')
70
+ );
71
+ }
72
+
73
+ static unescapeNullSequences(input: string) {
74
+ return input
75
+ .replace(/nu\uFFFD-/g, () => '\\u0000')
76
+ .replace(/nx\uFFFD-/g, () => '\\x00')
77
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
78
+ }
79
+
80
+ static escapeNulls(input: string) {
81
+ // Postgres doesn't support null bytes in `text`.
82
+ return input
83
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
84
+ .replace(/\x00/g, () => '-\uFFFD-');
85
+ }
86
+
87
+ static unescapeNulls(input: string) {
88
+ return input
89
+ .replace(/-\uFFFD-/g, () => '\x00')
90
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
91
+ }
92
+
56
93
  constructor(
57
94
  config: Partial<PostgreSQLDriverConfig>,
58
95
  link?: Pool,
59
- transaction?: PostgreSQLDriverTransaction
96
+ transaction?: PostgreSQLDriverTransaction,
60
97
  ) {
61
98
  super();
62
99
  this.config = { ...defaults, ...config };
@@ -91,18 +128,28 @@ export default class PostgreSQLDriver extends NymphDriver {
91
128
  return new PostgreSQLDriver(
92
129
  this.config,
93
130
  this.link,
94
- this.transaction ?? undefined
131
+ this.transaction ?? undefined,
95
132
  );
96
133
  }
97
134
 
98
- private getConnection(): Promise<PostgreSQLDriverConnection> {
99
- if (this.transaction != null && this.transaction.connection != null) {
135
+ private getConnection(
136
+ outsideTransaction = false,
137
+ ): Promise<PostgreSQLDriverConnection> {
138
+ if (
139
+ this.transaction != null &&
140
+ this.transaction.connection != null &&
141
+ !outsideTransaction
142
+ ) {
100
143
  return Promise.resolve(this.transaction.connection);
101
144
  }
102
145
  return new Promise((resolve, reject) =>
103
146
  this.link.connect((err, client, done) =>
104
- err ? reject(err) : resolve({ client, done })
105
- )
147
+ err
148
+ ? reject(err)
149
+ : client
150
+ ? resolve({ client, done })
151
+ : reject('No client returned from connect.'),
152
+ ),
106
153
  );
107
154
  }
108
155
 
@@ -118,8 +165,12 @@ export default class PostgreSQLDriver extends NymphDriver {
118
165
  const connection: PostgreSQLDriverConnection = await new Promise(
119
166
  (resolve, reject) =>
120
167
  this.link.connect((err, client, done) =>
121
- err ? reject(err) : resolve({ client, done })
122
- )
168
+ err
169
+ ? reject(err)
170
+ : client
171
+ ? resolve({ client, done })
172
+ : reject('No client returned from connect.'),
173
+ ),
123
174
  );
124
175
  await new Promise((resolve, reject) =>
125
176
  connection.client.query('SELECT 1;', [], (err, res) => {
@@ -127,7 +178,7 @@ export default class PostgreSQLDriver extends NymphDriver {
127
178
  reject(err);
128
179
  }
129
180
  resolve(0);
130
- })
181
+ }),
131
182
  );
132
183
  connection.done();
133
184
  }
@@ -138,7 +189,7 @@ export default class PostgreSQLDriver extends NymphDriver {
138
189
  // Connecting, selecting database
139
190
  if (!this.connected) {
140
191
  try {
141
- this.link = new Pool(this.postgresqlConfig);
192
+ this.link = new pg.Pool(this.postgresqlConfig);
142
193
  this.connected = true;
143
194
  } catch (e: any) {
144
195
  if (
@@ -148,7 +199,7 @@ export default class PostgreSQLDriver extends NymphDriver {
148
199
  this.postgresqlConfig.database === 'nymph'
149
200
  ) {
150
201
  throw new NotConfiguredError(
151
- "It seems the config hasn't been set up correctly."
202
+ "It seems the config hasn't been set up correctly.",
152
203
  );
153
204
  } else {
154
205
  throw new UnableToConnectError('Could not connect: ' + e?.message);
@@ -172,6 +223,9 @@ export default class PostgreSQLDriver extends NymphDriver {
172
223
  }
173
224
 
174
225
  public async inTransaction() {
226
+ if (this.transaction && this.transaction.count === 0) {
227
+ this.transaction = null;
228
+ }
175
229
  return !!this.transaction;
176
230
  }
177
231
 
@@ -190,203 +244,254 @@ export default class PostgreSQLDriver extends NymphDriver {
190
244
  * @param etype The entity type to create a table for. If this is blank, the default tables are created.
191
245
  * @returns True on success, false on failure.
192
246
  */
193
- private createTables(etype: string | null = null) {
247
+ private async createTables(etype: string | null = null) {
248
+ const connection = await this.getConnection(true);
194
249
  if (etype != null) {
195
250
  // Create the entity table.
196
- this.queryRunSync(
251
+ await this.queryRun(
197
252
  `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
198
- `${this.prefix}entities_${etype}`
253
+ `${this.prefix}entities_${etype}`,
199
254
  )} (
200
255
  "guid" BYTEA NOT NULL,
201
256
  "tags" TEXT[],
202
257
  "cdate" DOUBLE PRECISION NOT NULL,
203
258
  "mdate" DOUBLE PRECISION NOT NULL,
204
259
  PRIMARY KEY ("guid")
205
- ) WITH ( OIDS=FALSE );`
260
+ ) WITH ( OIDS=FALSE );`,
261
+ { connection },
206
262
  );
207
- this.queryRunSync(
263
+ await this.queryRun(
208
264
  `ALTER TABLE ${PostgreSQLDriver.escape(
209
- `${this.prefix}entities_${etype}`
210
- )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
265
+ `${this.prefix}entities_${etype}`,
266
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`,
267
+ { connection },
211
268
  );
212
- this.queryRunSync(
269
+ await this.queryRun(
213
270
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
214
- `${this.prefix}entities_${etype}_id_cdate`
215
- )};`
271
+ `${this.prefix}entities_${etype}_id_cdate`,
272
+ )};`,
273
+ { connection },
216
274
  );
217
- this.queryRunSync(
275
+ await this.queryRun(
218
276
  `CREATE INDEX ${PostgreSQLDriver.escape(
219
- `${this.prefix}entities_${etype}_id_cdate`
277
+ `${this.prefix}entities_${etype}_id_cdate`,
220
278
  )} ON ${PostgreSQLDriver.escape(
221
- `${this.prefix}entities_${etype}`
222
- )} USING btree ("cdate");`
279
+ `${this.prefix}entities_${etype}`,
280
+ )} USING btree ("cdate");`,
281
+ { connection },
223
282
  );
224
- this.queryRunSync(
283
+ await this.queryRun(
225
284
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
226
- `${this.prefix}entities_${etype}_id_mdate`
227
- )};`
285
+ `${this.prefix}entities_${etype}_id_mdate`,
286
+ )};`,
287
+ { connection },
228
288
  );
229
- this.queryRunSync(
289
+ await this.queryRun(
230
290
  `CREATE INDEX ${PostgreSQLDriver.escape(
231
- `${this.prefix}entities_${etype}_id_mdate`
291
+ `${this.prefix}entities_${etype}_id_mdate`,
232
292
  )} ON ${PostgreSQLDriver.escape(
233
- `${this.prefix}entities_${etype}`
234
- )} USING btree ("mdate");`
293
+ `${this.prefix}entities_${etype}`,
294
+ )} USING btree ("mdate");`,
295
+ { connection },
235
296
  );
236
- this.queryRunSync(
297
+ await this.queryRun(
237
298
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
238
- `${this.prefix}entities_${etype}_id_tags`
239
- )};`
299
+ `${this.prefix}entities_${etype}_id_tags`,
300
+ )};`,
301
+ { connection },
240
302
  );
241
- this.queryRunSync(
303
+ await this.queryRun(
242
304
  `CREATE INDEX ${PostgreSQLDriver.escape(
243
- `${this.prefix}entities_${etype}_id_tags`
305
+ `${this.prefix}entities_${etype}_id_tags`,
244
306
  )} ON ${PostgreSQLDriver.escape(
245
- `${this.prefix}entities_${etype}`
246
- )} USING gin ("tags");`
307
+ `${this.prefix}entities_${etype}`,
308
+ )} USING gin ("tags");`,
309
+ { connection },
247
310
  );
248
311
  // Create the data table.
249
- this.queryRunSync(
312
+ await this.queryRun(
250
313
  `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
251
- `${this.prefix}data_${etype}`
314
+ `${this.prefix}data_${etype}`,
252
315
  )} (
253
316
  "guid" BYTEA NOT NULL,
254
317
  "name" TEXT NOT NULL,
255
- "value" TEXT NOT NULL,
318
+ "value" CHARACTER(1) NOT NULL,
319
+ "json" JSONB,
320
+ "string" TEXT,
321
+ "number" DOUBLE PRECISION,
322
+ "truthy" BOOLEAN,
256
323
  PRIMARY KEY ("guid", "name"),
257
324
  FOREIGN KEY ("guid")
258
325
  REFERENCES ${PostgreSQLDriver.escape(
259
- `${this.prefix}entities_${etype}`
326
+ `${this.prefix}entities_${etype}`,
260
327
  )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
261
- ) WITH ( OIDS=FALSE );`
328
+ ) WITH ( OIDS=FALSE );`,
329
+ { connection },
262
330
  );
263
- this.queryRunSync(
331
+ await this.queryRun(
264
332
  `ALTER TABLE ${PostgreSQLDriver.escape(
265
- `${this.prefix}data_${etype}`
266
- )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
333
+ `${this.prefix}data_${etype}`,
334
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`,
335
+ { connection },
336
+ );
337
+ await this.queryRun(
338
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
339
+ `${this.prefix}data_${etype}_id_guid`,
340
+ )};`,
341
+ { connection },
342
+ );
343
+ await this.queryRun(
344
+ `CREATE INDEX ${PostgreSQLDriver.escape(
345
+ `${this.prefix}data_${etype}_id_guid`,
346
+ )} ON ${PostgreSQLDriver.escape(
347
+ `${this.prefix}data_${etype}`,
348
+ )} USING btree ("guid");`,
349
+ { connection },
350
+ );
351
+ await this.queryRun(
352
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
353
+ `${this.prefix}data_${etype}_id_guid_name`,
354
+ )};`,
355
+ { connection },
356
+ );
357
+ await this.queryRun(
358
+ `CREATE INDEX ${PostgreSQLDriver.escape(
359
+ `${this.prefix}data_${etype}_id_guid_name`,
360
+ )} ON ${PostgreSQLDriver.escape(
361
+ `${this.prefix}data_${etype}`,
362
+ )} USING btree ("guid", "name");`,
363
+ { connection },
267
364
  );
268
- this.queryRunSync(
365
+ await this.queryRun(
269
366
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
270
- `${this.prefix}data_${etype}_id_guid`
271
- )};`
367
+ `${this.prefix}data_${etype}_id_guid_name__user`,
368
+ )};`,
369
+ { connection },
272
370
  );
273
- this.queryRunSync(
371
+ await this.queryRun(
274
372
  `CREATE INDEX ${PostgreSQLDriver.escape(
275
- `${this.prefix}data_${etype}_id_guid`
373
+ `${this.prefix}data_${etype}_id_guid_name__user`,
276
374
  )} ON ${PostgreSQLDriver.escape(
277
- `${this.prefix}data_${etype}`
278
- )} USING btree ("guid");`
375
+ `${this.prefix}data_${etype}`,
376
+ )} USING btree ("guid") WHERE "name" = 'user'::text;`,
377
+ { connection },
279
378
  );
280
- this.queryRunSync(
379
+ await this.queryRun(
281
380
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
282
- `${this.prefix}data_${etype}_id_name`
283
- )};`
381
+ `${this.prefix}data_${etype}_id_guid_name__group`,
382
+ )};`,
383
+ { connection },
284
384
  );
285
- this.queryRunSync(
385
+ await this.queryRun(
286
386
  `CREATE INDEX ${PostgreSQLDriver.escape(
287
- `${this.prefix}data_${etype}_id_name`
387
+ `${this.prefix}data_${etype}_id_guid_name__group`,
288
388
  )} ON ${PostgreSQLDriver.escape(
289
- `${this.prefix}data_${etype}`
290
- )} USING btree ("name");`
389
+ `${this.prefix}data_${etype}`,
390
+ )} USING btree ("guid") WHERE "name" = 'group'::text;`,
391
+ { connection },
291
392
  );
292
- this.queryRunSync(
393
+ await this.queryRun(
293
394
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
294
- `${this.prefix}data_${etype}_id_guid_name__user`
295
- )};`
395
+ `${this.prefix}data_${etype}_id_name`,
396
+ )};`,
397
+ { connection },
296
398
  );
297
- this.queryRunSync(
399
+ await this.queryRun(
298
400
  `CREATE INDEX ${PostgreSQLDriver.escape(
299
- `${this.prefix}data_${etype}_id_guid_name__user`
401
+ `${this.prefix}data_${etype}_id_name`,
300
402
  )} ON ${PostgreSQLDriver.escape(
301
- `${this.prefix}data_${etype}`
302
- )} USING btree ("guid") WHERE "name" = 'user'::text;`
403
+ `${this.prefix}data_${etype}`,
404
+ )} USING btree ("name");`,
405
+ { connection },
303
406
  );
304
- this.queryRunSync(
407
+ await this.queryRun(
305
408
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
306
- `${this.prefix}data_${etype}_id_guid_name__group`
307
- )};`
409
+ `${this.prefix}data_${etype}_id_name_string`,
410
+ )};`,
411
+ { connection },
308
412
  );
309
- this.queryRunSync(
413
+ await this.queryRun(
310
414
  `CREATE INDEX ${PostgreSQLDriver.escape(
311
- `${this.prefix}data_${etype}_id_guid_name__group`
415
+ `${this.prefix}data_${etype}_id_name_string`,
312
416
  )} ON ${PostgreSQLDriver.escape(
313
- `${this.prefix}data_${etype}`
314
- )} USING btree ("guid") WHERE "name" = 'group'::text;`
417
+ `${this.prefix}data_${etype}`,
418
+ )} USING btree ("name", LEFT("string", 512));`,
419
+ { connection },
315
420
  );
316
- // Create the data comparisons table.
317
- this.queryRunSync(
318
- `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
319
- `${this.prefix}comparisons_${etype}`
320
- )} (
321
- "guid" BYTEA NOT NULL,
322
- "name" TEXT NOT NULL,
323
- "truthy" BOOLEAN,
324
- "string" TEXT,
325
- "number" DOUBLE PRECISION,
326
- PRIMARY KEY ("guid", "name"),
327
- FOREIGN KEY ("guid")
328
- REFERENCES ${PostgreSQLDriver.escape(
329
- `${this.prefix}entities_${etype}`
330
- )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
331
- ) WITH ( OIDS=FALSE );`
421
+ await this.queryRun(
422
+ `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
423
+ `${this.prefix}data_${etype}_id_name_number`,
424
+ )};`,
425
+ { connection },
332
426
  );
333
- this.queryRunSync(
334
- `ALTER TABLE ${PostgreSQLDriver.escape(
335
- `${this.prefix}comparisons_${etype}`
336
- )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
427
+ await this.queryRun(
428
+ `CREATE INDEX ${PostgreSQLDriver.escape(
429
+ `${this.prefix}data_${etype}_id_name_number`,
430
+ )} ON ${PostgreSQLDriver.escape(
431
+ `${this.prefix}data_${etype}`,
432
+ )} USING btree ("name", "number");`,
433
+ { connection },
337
434
  );
338
- this.queryRunSync(
435
+ await this.queryRun(
339
436
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
340
- `${this.prefix}comparisons_${etype}_id_guid`
341
- )};`
437
+ `${this.prefix}data_${etype}_id_name_truthy`,
438
+ )};`,
439
+ { connection },
342
440
  );
343
- this.queryRunSync(
441
+ await this.queryRun(
344
442
  `CREATE INDEX ${PostgreSQLDriver.escape(
345
- `${this.prefix}comparisons_${etype}_id_guid`
443
+ `${this.prefix}data_${etype}_id_name_truthy`,
346
444
  )} ON ${PostgreSQLDriver.escape(
347
- `${this.prefix}comparisons_${etype}`
348
- )} USING btree ("guid");`
445
+ `${this.prefix}data_${etype}`,
446
+ )} USING btree ("name") WHERE "truthy" = TRUE;`,
447
+ { connection },
349
448
  );
350
- this.queryRunSync(
449
+ await this.queryRun(
351
450
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
352
- `${this.prefix}comparisons_${etype}_id_name`
353
- )};`
451
+ `${this.prefix}data_${etype}_id_name_falsy`,
452
+ )};`,
453
+ { connection },
354
454
  );
355
- this.queryRunSync(
455
+ await this.queryRun(
356
456
  `CREATE INDEX ${PostgreSQLDriver.escape(
357
- `${this.prefix}comparisons_${etype}_id_name`
457
+ `${this.prefix}data_${etype}_id_name_falsy`,
358
458
  )} ON ${PostgreSQLDriver.escape(
359
- `${this.prefix}comparisons_${etype}`
360
- )} USING btree ("name");`
459
+ `${this.prefix}data_${etype}`,
460
+ )} USING btree ("name") WHERE "truthy" <> TRUE;`,
461
+ { connection },
361
462
  );
362
- this.queryRunSync(
463
+ await this.queryRun(
363
464
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
364
- `${this.prefix}comparisons_${etype}_id_guid_name_truthy`
365
- )};`
465
+ `${this.prefix}data_${etype}_id_string`,
466
+ )};`,
467
+ { connection },
366
468
  );
367
- this.queryRunSync(
469
+ await this.queryRun(
368
470
  `CREATE INDEX ${PostgreSQLDriver.escape(
369
- `${this.prefix}comparisons_${etype}_id_guid_name_truthy`
471
+ `${this.prefix}data_${etype}_id_string`,
370
472
  )} ON ${PostgreSQLDriver.escape(
371
- `${this.prefix}comparisons_${etype}`
372
- )} USING btree ("guid", "name") WHERE "truthy" = TRUE;`
473
+ `${this.prefix}data_${etype}`,
474
+ )} USING gin ("string" gin_trgm_ops);`,
475
+ { connection },
373
476
  );
374
- this.queryRunSync(
477
+ await this.queryRun(
375
478
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
376
- `${this.prefix}comparisons_${etype}_id_guid_name_falsy`
377
- )};`
479
+ `${this.prefix}data_${etype}_id_json`,
480
+ )};`,
481
+ { connection },
378
482
  );
379
- this.queryRunSync(
483
+ await this.queryRun(
380
484
  `CREATE INDEX ${PostgreSQLDriver.escape(
381
- `${this.prefix}comparisons_${etype}_id_guid_name_falsy`
485
+ `${this.prefix}data_${etype}_id_json`,
382
486
  )} ON ${PostgreSQLDriver.escape(
383
- `${this.prefix}comparisons_${etype}`
384
- )} USING btree ("guid", "name") WHERE "truthy" <> TRUE;`
487
+ `${this.prefix}data_${etype}`,
488
+ )} USING gin ("json");`,
489
+ { connection },
385
490
  );
386
491
  // Create the references table.
387
- this.queryRunSync(
492
+ await this.queryRun(
388
493
  `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
389
- `${this.prefix}references_${etype}`
494
+ `${this.prefix}references_${etype}`,
390
495
  )} (
391
496
  "guid" BYTEA NOT NULL,
392
497
  "name" TEXT NOT NULL,
@@ -394,74 +499,106 @@ export default class PostgreSQLDriver extends NymphDriver {
394
499
  PRIMARY KEY ("guid", "name", "reference"),
395
500
  FOREIGN KEY ("guid")
396
501
  REFERENCES ${PostgreSQLDriver.escape(
397
- `${this.prefix}entities_${etype}`
502
+ `${this.prefix}entities_${etype}`,
398
503
  )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
399
- ) WITH ( OIDS=FALSE );`
504
+ ) WITH ( OIDS=FALSE );`,
505
+ { connection },
400
506
  );
401
- this.queryRunSync(
507
+ await this.queryRun(
402
508
  `ALTER TABLE ${PostgreSQLDriver.escape(
403
- `${this.prefix}references_${etype}`
404
- )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
509
+ `${this.prefix}references_${etype}`,
510
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`,
511
+ { connection },
405
512
  );
406
- this.queryRunSync(
513
+ await this.queryRun(
407
514
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
408
- `${this.prefix}references_${etype}_id_guid`
409
- )};`
515
+ `${this.prefix}references_${etype}_id_guid`,
516
+ )};`,
517
+ { connection },
410
518
  );
411
- this.queryRunSync(
519
+ await this.queryRun(
412
520
  `CREATE INDEX ${PostgreSQLDriver.escape(
413
- `${this.prefix}references_${etype}_id_guid`
521
+ `${this.prefix}references_${etype}_id_guid`,
414
522
  )} ON ${PostgreSQLDriver.escape(
415
- `${this.prefix}references_${etype}`
416
- )} USING btree ("guid");`
523
+ `${this.prefix}references_${etype}`,
524
+ )} USING btree ("guid");`,
525
+ { connection },
417
526
  );
418
- this.queryRunSync(
527
+ await this.queryRun(
419
528
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
420
- `${this.prefix}references_${etype}_id_name`
421
- )};`
529
+ `${this.prefix}references_${etype}_id_name`,
530
+ )};`,
531
+ { connection },
422
532
  );
423
- this.queryRunSync(
533
+ await this.queryRun(
424
534
  `CREATE INDEX ${PostgreSQLDriver.escape(
425
- `${this.prefix}references_${etype}_id_name`
535
+ `${this.prefix}references_${etype}_id_name`,
426
536
  )} ON ${PostgreSQLDriver.escape(
427
- `${this.prefix}references_${etype}`
428
- )} USING btree ("name");`
537
+ `${this.prefix}references_${etype}`,
538
+ )} USING btree ("name");`,
539
+ { connection },
429
540
  );
430
- this.queryRunSync(
541
+ await this.queryRun(
431
542
  `DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(
432
- `${this.prefix}references_${etype}_id_reference`
433
- )};`
543
+ `${this.prefix}references_${etype}_id_name_reference`,
544
+ )};`,
545
+ { connection },
434
546
  );
435
- this.queryRunSync(
547
+ await this.queryRun(
436
548
  `CREATE INDEX ${PostgreSQLDriver.escape(
437
- `${this.prefix}references_${etype}_id_reference`
549
+ `${this.prefix}references_${etype}_id_name_reference`,
438
550
  )} ON ${PostgreSQLDriver.escape(
439
- `${this.prefix}references_${etype}`
440
- )} USING btree ("reference");`
551
+ `${this.prefix}references_${etype}`,
552
+ )} USING btree ("name", "reference");`,
553
+ { connection },
554
+ );
555
+ // Create the unique strings table.
556
+ await this.queryRun(
557
+ `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
558
+ `${this.prefix}uniques_${etype}`,
559
+ )} (
560
+ "guid" BYTEA NOT NULL,
561
+ "unique" TEXT NOT NULL UNIQUE,
562
+ PRIMARY KEY ("guid", "unique"),
563
+ FOREIGN KEY ("guid")
564
+ REFERENCES ${PostgreSQLDriver.escape(
565
+ `${this.prefix}entities_${etype}`,
566
+ )} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
567
+ ) WITH ( OIDS=FALSE );`,
568
+ { connection },
441
569
  );
442
570
  } else {
571
+ // Add trigram extensions.
572
+ try {
573
+ await this.queryRun(`CREATE EXTENSION pg_trgm;`, { connection });
574
+ } catch (e: any) {
575
+ // Ignore errors.
576
+ }
443
577
  // Create the UID table.
444
- this.queryRunSync(
578
+ await this.queryRun(
445
579
  `CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(
446
- `${this.prefix}uids`
580
+ `${this.prefix}uids`,
447
581
  )} (
448
582
  "name" TEXT NOT NULL,
449
583
  "cur_uid" BIGINT NOT NULL,
450
584
  PRIMARY KEY ("name")
451
- ) WITH ( OIDS = FALSE );`
585
+ ) WITH ( OIDS = FALSE );`,
586
+ { connection },
452
587
  );
453
- this.queryRunSync(
588
+ await this.queryRun(
454
589
  `ALTER TABLE ${PostgreSQLDriver.escape(
455
- `${this.prefix}uids`
456
- )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`
590
+ `${this.prefix}uids`,
591
+ )} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`,
592
+ { connection },
457
593
  );
458
594
  }
595
+ connection.done();
459
596
  return true;
460
597
  }
461
598
 
462
599
  private translateQuery(
463
600
  origQuery: string,
464
- origParams: { [k: string]: any }
601
+ origParams: { [k: string]: any },
465
602
  ): { query: string; params: any[] } {
466
603
  const params: any[] = [];
467
604
  let query = origQuery;
@@ -481,61 +618,39 @@ export default class PostgreSQLDriver extends NymphDriver {
481
618
  private async query<T extends () => any>(
482
619
  runQuery: T,
483
620
  query: string,
484
- etypes: string[] = []
621
+ etypes: string[] = [],
485
622
  // @ts-ignore: The return type of T is a promise.
486
623
  ): ReturnType<T> {
487
624
  try {
625
+ this.nymph.config.debugInfo('postgresql:query', query);
488
626
  return await runQuery();
489
627
  } catch (e: any) {
490
628
  const errorCode = e?.code;
491
- if (errorCode === '42P01' && this.createTables()) {
629
+ if (errorCode === '42P01' && (await this.createTables())) {
492
630
  // If the tables don't exist yet, create them.
493
631
  for (let etype of etypes) {
494
- this.createTables(etype);
632
+ await this.createTables(etype);
495
633
  }
496
634
  try {
497
635
  return await runQuery();
498
636
  } catch (e2: any) {
499
637
  throw new QueryFailedError(
500
638
  'Query failed: ' + e2?.code + ' - ' + e2?.message,
501
- query
502
- );
503
- }
504
- } else {
505
- throw e;
506
- }
507
- }
508
- }
509
-
510
- private querySync<T extends () => any>(
511
- runQuery: T,
512
- query: string,
513
- etypes: string[] = []
514
- ): ReturnType<T> {
515
- try {
516
- return runQuery();
517
- } catch (e: any) {
518
- const errorCode = e?.code;
519
- if (errorCode === '42P01' && this.createTables()) {
520
- // If the tables don't exist yet, create them.
521
- for (let etype of etypes) {
522
- this.createTables(etype);
523
- }
524
- try {
525
- return runQuery();
526
- } catch (e2: any) {
527
- throw new QueryFailedError(
528
- 'Query failed: ' + e2?.code + ' - ' + e2?.message,
529
- query
639
+ query,
530
640
  );
531
641
  }
642
+ } else if (errorCode === '23505') {
643
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
532
644
  } else {
533
- throw e;
645
+ throw new QueryFailedError(
646
+ 'Query failed: ' + e?.code + ' - ' + e?.message,
647
+ query,
648
+ );
534
649
  }
535
650
  }
536
651
  }
537
652
 
538
- private queryIter(
653
+ private queryArray(
539
654
  query: string,
540
655
  {
541
656
  etypes = [],
@@ -543,11 +658,11 @@ export default class PostgreSQLDriver extends NymphDriver {
543
658
  }: {
544
659
  etypes?: string[];
545
660
  params?: { [k: string]: any };
546
- } = {}
661
+ } = {},
547
662
  ) {
548
663
  const { query: newQuery, params: newParams } = this.translateQuery(
549
664
  query,
550
- params
665
+ params,
551
666
  );
552
667
  return this.query(
553
668
  async () => {
@@ -558,64 +673,65 @@ export default class PostgreSQLDriver extends NymphDriver {
558
673
  .query(newQuery, newParams)
559
674
  .then(
560
675
  (results) => resolve(results),
561
- (error) => reject(error)
676
+ (error) => reject(error),
562
677
  );
563
678
  } catch (e) {
564
679
  reject(e);
565
680
  }
566
- }
681
+ },
567
682
  );
568
683
  return results.rows;
569
684
  },
570
685
  `${query} -- ${JSON.stringify(params)}`,
571
- etypes
686
+ etypes,
572
687
  );
573
688
  }
574
689
 
575
- private queryIterSync(
690
+ private async queryIter(
576
691
  query: string,
577
692
  {
578
693
  etypes = [],
579
694
  params = {},
580
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
695
+ }: {
696
+ etypes?: string[];
697
+ params?: { [k: string]: any };
698
+ } = {},
581
699
  ) {
582
700
  const { query: newQuery, params: newParams } = this.translateQuery(
583
701
  query,
584
- params
702
+ params,
585
703
  );
586
- return this.querySync(
587
- () => {
588
- const output = cp.spawnSync(
589
- process.argv0,
590
- [__dirname + '/runPostgresqlSync.js'],
591
- {
592
- input: JSON.stringify({
593
- postgresqlConfig: this.postgresqlConfig,
594
- query: newQuery,
595
- params: newParams,
596
- }),
597
- timeout: 30000,
598
- maxBuffer: 100 * 1024 * 1024,
599
- encoding: 'utf8',
600
- windowsHide: true,
704
+ const that = this;
705
+
706
+ return this.query(
707
+ async function* (): AsyncGenerator<any, void, false | undefined> {
708
+ const transaction = !!that.transaction?.connection;
709
+ const connection = await that.getConnection();
710
+ const cursor = new Cursor(newQuery, newParams);
711
+ const iter = connection.client.query(cursor);
712
+
713
+ while (true) {
714
+ const rows = await iter.read(100);
715
+
716
+ if (!rows.length) {
717
+ await new Promise<void>((resolve) => {
718
+ iter.close(() => {
719
+ if (!transaction) {
720
+ connection.done();
721
+ }
722
+ resolve();
723
+ });
724
+ });
725
+ return;
726
+ }
727
+
728
+ for (let row of rows) {
729
+ yield row;
601
730
  }
602
- );
603
- try {
604
- return JSON.parse(output.stdout).rows;
605
- } catch (e) {
606
- // Do nothing.
607
- }
608
- if (output.status === 0) {
609
- throw new Error('Unknown parse error.');
610
- }
611
- const err = JSON.parse(output.stderr);
612
- const e = new Error(err.name);
613
- for (const name in err) {
614
- (e as any)[name] = err[name];
615
731
  }
616
732
  },
617
733
  `${query} -- ${JSON.stringify(params)}`,
618
- etypes
734
+ etypes,
619
735
  );
620
736
  }
621
737
 
@@ -627,11 +743,11 @@ export default class PostgreSQLDriver extends NymphDriver {
627
743
  }: {
628
744
  etypes?: string[];
629
745
  params?: { [k: string]: any };
630
- } = {}
746
+ } = {},
631
747
  ) {
632
748
  const { query: newQuery, params: newParams } = this.translateQuery(
633
749
  query,
634
- params
750
+ params,
635
751
  );
636
752
  return this.query(
637
753
  async () => {
@@ -642,17 +758,17 @@ export default class PostgreSQLDriver extends NymphDriver {
642
758
  .query(newQuery, newParams)
643
759
  .then(
644
760
  (results) => resolve(results),
645
- (error) => reject(error)
761
+ (error) => reject(error),
646
762
  );
647
763
  } catch (e) {
648
764
  reject(e);
649
765
  }
650
- }
766
+ },
651
767
  );
652
768
  return results.rows[0];
653
769
  },
654
770
  `${query} -- ${JSON.stringify(params)}`,
655
- etypes
771
+ etypes,
656
772
  );
657
773
  }
658
774
 
@@ -661,90 +777,47 @@ export default class PostgreSQLDriver extends NymphDriver {
661
777
  {
662
778
  etypes = [],
663
779
  params = {},
780
+ connection,
664
781
  }: {
665
782
  etypes?: string[];
666
783
  params?: { [k: string]: any };
667
- } = {}
784
+ connection?: PostgreSQLDriverConnection;
785
+ } = {},
668
786
  ) {
669
787
  const { query: newQuery, params: newParams } = this.translateQuery(
670
788
  query,
671
- params
789
+ params,
672
790
  );
673
791
  return this.query(
674
792
  async () => {
675
793
  const results: QueryResult<any> = await new Promise(
676
794
  (resolve, reject) => {
677
795
  try {
678
- (this.transaction?.connection?.client ?? this.link)
796
+ (
797
+ (connection ?? this.transaction?.connection)?.client ??
798
+ this.link
799
+ )
679
800
  .query(newQuery, newParams)
680
801
  .then(
681
802
  (results) => resolve(results),
682
- (error) => reject(error)
803
+ (error) => reject(error),
683
804
  );
684
805
  } catch (e) {
685
806
  reject(e);
686
807
  }
687
- }
808
+ },
688
809
  );
689
810
  return { rowCount: results.rowCount ?? 0 };
690
811
  },
691
812
  `${query} -- ${JSON.stringify(params)}`,
692
- etypes
693
- );
694
- }
695
-
696
- private queryRunSync(
697
- query: string,
698
- {
699
- etypes = [],
700
- params = {},
701
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
702
- ) {
703
- const { query: newQuery, params: newParams } = this.translateQuery(
704
- query,
705
- params
706
- );
707
- return this.querySync(
708
- () => {
709
- const output = cp.spawnSync(
710
- process.argv0,
711
- [__dirname + '/runPostgresqlSync.js'],
712
- {
713
- input: JSON.stringify({
714
- postgresqlConfig: this.postgresqlConfig,
715
- query: newQuery,
716
- params: newParams,
717
- }),
718
- timeout: 30000,
719
- maxBuffer: 100 * 1024 * 1024,
720
- encoding: 'utf8',
721
- windowsHide: true,
722
- }
723
- );
724
- try {
725
- const results = JSON.parse(output.stdout);
726
- return { rowCount: results.rowCount ?? 0 };
727
- } catch (e) {
728
- // Do nothing.
729
- }
730
- if (output.status === 0) {
731
- throw new Error('Unknown parse error.');
732
- }
733
- const err = JSON.parse(output.stderr);
734
- const e = new Error(err.name);
735
- for (const name in err) {
736
- (e as any)[name] = err[name];
737
- }
738
- },
739
- `${query} -- ${JSON.stringify(params)}`,
740
- etypes
813
+ etypes,
741
814
  );
742
815
  }
743
816
 
744
817
  public async commit(name: string) {
745
818
  if (name == null || typeof name !== 'string' || name.length === 0) {
746
819
  throw new InvalidParametersError(
747
- 'Transaction commit attempted without a name.'
820
+ 'Transaction commit attempted without a name.',
748
821
  );
749
822
  }
750
823
  if (!this.transaction || this.transaction.count === 0) {
@@ -764,7 +837,7 @@ export default class PostgreSQLDriver extends NymphDriver {
764
837
 
765
838
  public async deleteEntityByID(
766
839
  guid: string,
767
- className?: EntityConstructor | string | null
840
+ className?: EntityConstructor | string | null,
768
841
  ) {
769
842
  let EntityClass: EntityConstructor;
770
843
  if (typeof className === 'string' || className == null) {
@@ -778,58 +851,60 @@ export default class PostgreSQLDriver extends NymphDriver {
778
851
  try {
779
852
  await this.queryRun(
780
853
  `DELETE FROM ${PostgreSQLDriver.escape(
781
- `${this.prefix}entities_${etype}`
854
+ `${this.prefix}entities_${etype}`,
782
855
  )} WHERE "guid"=decode(@guid, 'hex');`,
783
856
  {
784
857
  etypes: [etype],
785
858
  params: {
786
859
  guid,
787
860
  },
788
- }
861
+ },
789
862
  );
790
863
  await this.queryRun(
791
864
  `DELETE FROM ${PostgreSQLDriver.escape(
792
- `${this.prefix}data_${etype}`
865
+ `${this.prefix}data_${etype}`,
793
866
  )} WHERE "guid"=decode(@guid, 'hex');`,
794
867
  {
795
868
  etypes: [etype],
796
869
  params: {
797
870
  guid,
798
871
  },
799
- }
872
+ },
800
873
  );
801
874
  await this.queryRun(
802
875
  `DELETE FROM ${PostgreSQLDriver.escape(
803
- `${this.prefix}comparisons_${etype}`
876
+ `${this.prefix}references_${etype}`,
804
877
  )} WHERE "guid"=decode(@guid, 'hex');`,
805
878
  {
806
879
  etypes: [etype],
807
880
  params: {
808
881
  guid,
809
882
  },
810
- }
883
+ },
811
884
  );
812
885
  await this.queryRun(
813
886
  `DELETE FROM ${PostgreSQLDriver.escape(
814
- `${this.prefix}references_${etype}`
887
+ `${this.prefix}uniques_${etype}`,
815
888
  )} WHERE "guid"=decode(@guid, 'hex');`,
816
889
  {
817
890
  etypes: [etype],
818
891
  params: {
819
892
  guid,
820
893
  },
821
- }
894
+ },
822
895
  );
823
- await this.commit('nymph-delete');
824
- // Remove any cached versions of this entity.
825
- if (this.nymph.config.cache) {
826
- this.cleanCache(guid);
827
- }
828
- return true;
829
896
  } catch (e: any) {
897
+ this.nymph.config.debugError('postgresql', `Delete entity error: "${e}"`);
830
898
  await this.rollback('nymph-delete');
831
899
  throw e;
832
900
  }
901
+
902
+ await this.commit('nymph-delete');
903
+ // Remove any cached versions of this entity.
904
+ if (this.nymph.config.cache) {
905
+ this.cleanCache(guid);
906
+ }
907
+ return true;
833
908
  }
834
909
 
835
910
  public async deleteUID(name: string) {
@@ -838,103 +913,143 @@ export default class PostgreSQLDriver extends NymphDriver {
838
913
  }
839
914
  await this.queryRun(
840
915
  `DELETE FROM ${PostgreSQLDriver.escape(
841
- `${this.prefix}uids`
916
+ `${this.prefix}uids`,
842
917
  )} WHERE "name"=@name;`,
843
918
  {
844
919
  params: {
845
920
  name,
846
921
  },
847
- }
922
+ },
848
923
  );
849
924
  return true;
850
925
  }
851
926
 
852
- protected async exportEntities(writeLine: (line: string) => void) {
853
- writeLine('#nex2');
854
- writeLine('# Nymph Entity Exchange v2');
855
- writeLine('# http://nymph.io');
856
- writeLine('#');
857
- writeLine('# Generation Time: ' + new Date().toLocaleString());
858
- writeLine('');
927
+ public async *exportDataIterator(): AsyncGenerator<
928
+ { type: 'comment' | 'uid' | 'entity'; content: string },
929
+ void,
930
+ false | undefined
931
+ > {
932
+ if (
933
+ yield {
934
+ type: 'comment',
935
+ content: `#nex2
936
+ # Nymph Entity Exchange v2
937
+ # http://nymph.io
938
+ #
939
+ # Generation Time: ${new Date().toLocaleString()}
940
+ `,
941
+ }
942
+ ) {
943
+ return;
944
+ }
945
+
946
+ if (
947
+ yield {
948
+ type: 'comment',
949
+ content: `
859
950
 
860
- writeLine('#');
861
- writeLine('# UIDs');
862
- writeLine('#');
863
- writeLine('');
951
+ #
952
+ # UIDs
953
+ #
954
+
955
+ `,
956
+ }
957
+ ) {
958
+ return;
959
+ }
864
960
 
865
961
  // Export UIDs.
866
962
  let uids = await this.queryIter(
867
963
  `SELECT * FROM ${PostgreSQLDriver.escape(
868
- `${this.prefix}uids`
869
- )} ORDER BY "name";`
964
+ `${this.prefix}uids`,
965
+ )} ORDER BY "name";`,
870
966
  );
871
- for (const uid of uids) {
872
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
967
+ for await (const uid of uids) {
968
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
969
+ return;
970
+ }
873
971
  }
874
972
 
875
- writeLine('');
876
- writeLine('#');
877
- writeLine('# Entities');
878
- writeLine('#');
879
- writeLine('');
973
+ if (
974
+ yield {
975
+ type: 'comment',
976
+ content: `
977
+
978
+ #
979
+ # Entities
980
+ #
981
+
982
+ `,
983
+ }
984
+ ) {
985
+ return;
986
+ }
880
987
 
881
988
  // Get the etypes.
882
- const tables = await this.queryIter(
883
- 'SELECT relname FROM pg_stat_user_tables ORDER BY relname;'
989
+ const tables = await this.queryArray(
990
+ 'SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix;',
991
+ {
992
+ params: {
993
+ db: this.config.database,
994
+ prefix: this.prefix + 'entities_' + '%',
995
+ },
996
+ },
884
997
  );
885
998
  const etypes = [];
886
- for (const tableRow of tables) {
887
- const table = tableRow.relname;
888
- if (table.startsWith(this.prefix + 'entities_')) {
889
- etypes.push(table.substr((this.prefix + 'entities_').length));
890
- }
999
+ for (const table of tables) {
1000
+ etypes.push(table.table_name.substr((this.prefix + 'entities_').length));
891
1001
  }
892
1002
 
893
1003
  for (const etype of etypes) {
894
1004
  // Export entities.
895
- const dataIterator = (
896
- await this.queryIter(
897
- `SELECT encode(e."guid", 'hex') AS "guid", e."tags", e."cdate", e."mdate", d."name" AS "dname", d."value" AS "dvalue", c."string", c."number"
1005
+ const dataIterator = await this.queryIter(
1006
+ `SELECT encode(e."guid", 'hex') AS "guid", e."tags", e."cdate", e."mdate", d."name", d."value", d."json", d."string", d."number"
898
1007
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} e
899
1008
  LEFT JOIN ${PostgreSQLDriver.escape(
900
- `${this.prefix}data_${etype}`
1009
+ `${this.prefix}data_${etype}`,
901
1010
  )} d ON e."guid"=d."guid"
902
- INNER JOIN ${PostgreSQLDriver.escape(
903
- `${this.prefix}comparisons_${etype}`
904
- )} c ON d."guid"=c."guid" AND d."name"=c."name"
905
- ORDER BY e."guid";`
906
- )
907
- )[Symbol.iterator]();
908
- let datum = dataIterator.next();
1011
+ ORDER BY e."guid";`,
1012
+ );
1013
+ let datum = await dataIterator.next();
909
1014
  while (!datum.done) {
910
1015
  const guid = datum.value.guid;
911
- const tags = datum.value.tags.join(',');
1016
+ const tags = datum.value.tags.filter((tag: string) => tag).join(',');
912
1017
  const cdate = datum.value.cdate;
913
1018
  const mdate = datum.value.mdate;
914
- writeLine(`{${guid}}<${etype}>[${tags}]`);
915
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
916
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
917
- if (datum.value.dname != null) {
1019
+ let currentEntityExport: string[] = [];
1020
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
1021
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
1022
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
1023
+ if (datum.value.name != null) {
918
1024
  // This do will keep going and adding the data until the
919
1025
  // next entity is reached. $row will end on the next entity.
920
1026
  do {
921
1027
  const value =
922
- datum.value.dvalue === 'N'
1028
+ datum.value.value === 'N'
923
1029
  ? JSON.stringify(Number(datum.value.number))
924
- : datum.value.dvalue === 'S'
925
- ? JSON.stringify(datum.value.string)
926
- : datum.value.dvalue;
927
- writeLine(`\t${datum.value.dname}=${value}`);
928
- datum = dataIterator.next();
1030
+ : datum.value.value === 'S'
1031
+ ? JSON.stringify(
1032
+ PostgreSQLDriver.unescapeNulls(datum.value.string),
1033
+ )
1034
+ : datum.value.value === 'J'
1035
+ ? PostgreSQLDriver.unescapeNullSequences(
1036
+ JSON.stringify(datum.value.json),
1037
+ )
1038
+ : datum.value.value;
1039
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
1040
+ datum = await dataIterator.next();
929
1041
  } while (!datum.done && datum.value.guid === guid);
930
1042
  } else {
931
1043
  // Make sure that datum is incremented :)
932
- datum = dataIterator.next();
1044
+ datum = await dataIterator.next();
1045
+ }
1046
+ currentEntityExport.push('');
1047
+
1048
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
1049
+ return;
933
1050
  }
934
1051
  }
935
1052
  }
936
-
937
- return;
938
1053
  }
939
1054
 
940
1055
  /**
@@ -955,21 +1070,22 @@ export default class PostgreSQLDriver extends NymphDriver {
955
1070
  params: { [k: string]: any } = {},
956
1071
  subquery = false,
957
1072
  tableSuffix = '',
958
- etypes: string[] = []
1073
+ etypes: string[] = [],
1074
+ guidSelector: string | undefined = undefined,
959
1075
  ) {
960
1076
  if (typeof options.class?.alterOptions === 'function') {
961
1077
  options = options.class.alterOptions(options);
962
1078
  }
963
1079
  const eTable = `e${tableSuffix}`;
964
1080
  const dTable = `d${tableSuffix}`;
965
- const cTable = `c${tableSuffix}`;
966
1081
  const fTable = `f${tableSuffix}`;
967
1082
  const ieTable = `ie${tableSuffix}`;
968
1083
  const countTable = `count${tableSuffix}`;
1084
+ const sTable = `s${tableSuffix}`;
969
1085
  const sort = options.sort ?? 'cdate';
970
1086
  const queryParts = this.iterateSelectorsForQuery(
971
1087
  formattedSelectors,
972
- (key, value, typeIsOr, typeIsNot) => {
1088
+ ({ key, value, typeIsOr, typeIsNot }) => {
973
1089
  const clauseNot = key.startsWith('!');
974
1090
  let curQuery = '';
975
1091
  for (const curValue of value) {
@@ -1013,12 +1129,12 @@ export default class PostgreSQLDriver extends NymphDriver {
1013
1129
  }
1014
1130
  const name = `param${++count.i}`;
1015
1131
  curQuery +=
1016
- ieTable +
1017
- '."guid" ' +
1018
1132
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1019
- 'IN (SELECT "guid" FROM ' +
1133
+ 'EXISTS (SELECT "guid" FROM ' +
1020
1134
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1021
- ' WHERE "name"=@' +
1135
+ ' WHERE "guid"=' +
1136
+ ieTable +
1137
+ '."guid" AND "name"=@' +
1022
1138
  name +
1023
1139
  ')';
1024
1140
  params[name] = curVar;
@@ -1048,12 +1164,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1048
1164
  const name = `param${++count.i}`;
1049
1165
  curQuery +=
1050
1166
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1167
+ 'EXISTS (SELECT "guid" FROM ' +
1168
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1169
+ ' WHERE "guid"=' +
1051
1170
  ieTable +
1052
- '."guid" IN (SELECT "guid" FROM ' +
1053
- PostgreSQLDriver.escape(
1054
- this.prefix + 'comparisons_' + etype
1055
- ) +
1056
- ' WHERE "name"=@' +
1171
+ '."guid" AND "name"=@' +
1057
1172
  name +
1058
1173
  ' AND "truthy"=TRUE)';
1059
1174
  params[name] = curVar;
@@ -1098,12 +1213,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1098
1213
  const value = `param${++count.i}`;
1099
1214
  curQuery +=
1100
1215
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1216
+ 'EXISTS (SELECT "guid" FROM ' +
1217
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1218
+ ' WHERE "guid"=' +
1101
1219
  ieTable +
1102
- '."guid" IN (SELECT "guid" FROM ' +
1103
- PostgreSQLDriver.escape(
1104
- this.prefix + 'comparisons_' + etype
1105
- ) +
1106
- ' WHERE "name"=@' +
1220
+ '."guid" AND "name"=@' +
1107
1221
  name +
1108
1222
  ' AND "number"=@' +
1109
1223
  value +
@@ -1118,18 +1232,19 @@ export default class PostgreSQLDriver extends NymphDriver {
1118
1232
  const value = `param${++count.i}`;
1119
1233
  curQuery +=
1120
1234
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1235
+ 'EXISTS (SELECT "guid" FROM ' +
1236
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1237
+ ' WHERE "guid"=' +
1121
1238
  ieTable +
1122
- '."guid" IN (SELECT "guid" FROM ' +
1123
- PostgreSQLDriver.escape(
1124
- this.prefix + 'comparisons_' + etype
1125
- ) +
1126
- ' WHERE "name"=@' +
1239
+ '."guid" AND "name"=@' +
1127
1240
  name +
1128
- ' AND "string"=@' +
1129
- value +
1241
+ ' AND "string"=' +
1242
+ (curValue[1].length < 512
1243
+ ? 'LEFT(@' + value + ', 512)'
1244
+ : '@' + value) +
1130
1245
  ')';
1131
1246
  params[name] = curValue[0];
1132
- params[value] = curValue[1];
1247
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
1133
1248
  } else {
1134
1249
  if (curQuery) {
1135
1250
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1147,16 +1262,17 @@ export default class PostgreSQLDriver extends NymphDriver {
1147
1262
  const value = `param${++count.i}`;
1148
1263
  curQuery +=
1149
1264
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1150
- ieTable +
1151
- '."guid" IN (SELECT "guid" FROM ' +
1265
+ 'EXISTS (SELECT "guid" FROM ' +
1152
1266
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1153
- ' WHERE "name"=@' +
1267
+ ' WHERE "guid"=' +
1268
+ ieTable +
1269
+ '."guid" AND "name"=@' +
1154
1270
  name +
1155
- ' AND "value"=@' +
1271
+ ' AND "json"=@' +
1156
1272
  value +
1157
1273
  ')';
1158
1274
  params[name] = curValue[0];
1159
- params[value] = svalue;
1275
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
1160
1276
  }
1161
1277
  break;
1162
1278
  case 'contain':
@@ -1169,7 +1285,7 @@ export default class PostgreSQLDriver extends NymphDriver {
1169
1285
  curQuery +=
1170
1286
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1171
1287
  ieTable +
1172
- '."cdate"=' +
1288
+ '."cdate"=@' +
1173
1289
  cdate;
1174
1290
  params[cdate] = isNaN(Number(curValue[1]))
1175
1291
  ? null
@@ -1183,7 +1299,7 @@ export default class PostgreSQLDriver extends NymphDriver {
1183
1299
  curQuery +=
1184
1300
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1185
1301
  ieTable +
1186
- '."mdate"=' +
1302
+ '."mdate"=@' +
1187
1303
  mdate;
1188
1304
  params[mdate] = isNaN(Number(curValue[1]))
1189
1305
  ? null
@@ -1194,57 +1310,34 @@ export default class PostgreSQLDriver extends NymphDriver {
1194
1310
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1195
1311
  }
1196
1312
  let svalue: string;
1197
- let stringValue: string;
1198
1313
  if (
1199
1314
  curValue[1] instanceof Object &&
1200
1315
  typeof curValue[1].toReference === 'function'
1201
1316
  ) {
1202
1317
  svalue = JSON.stringify(curValue[1].toReference());
1203
- stringValue = `${curValue[1].toReference()}`;
1204
- } else {
1318
+ } else if (
1319
+ typeof curValue[1] === 'string' ||
1320
+ typeof curValue[1] === 'number'
1321
+ ) {
1205
1322
  svalue = JSON.stringify(curValue[1]);
1206
- stringValue = `${curValue[1]}`;
1323
+ } else {
1324
+ svalue = JSON.stringify([curValue[1]]);
1207
1325
  }
1208
1326
  const name = `param${++count.i}`;
1209
1327
  const value = `param${++count.i}`;
1210
- if (typeof curValue[1] === 'string') {
1211
- const stringParam = `param${++count.i}`;
1212
- curQuery +=
1213
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1214
- '(' +
1215
- ieTable +
1216
- '."guid" IN (SELECT "guid" FROM ' +
1217
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1218
- ' WHERE "name"=@' +
1219
- name +
1220
- ' AND position(@' +
1221
- value +
1222
- ' IN "value")>0) OR ' +
1223
- ieTable +
1224
- '."guid" IN (SELECT "guid" FROM ' +
1225
- PostgreSQLDriver.escape(
1226
- this.prefix + 'comparisons_' + etype
1227
- ) +
1228
- ' WHERE "name"=@' +
1229
- name +
1230
- ' AND "string"=@' +
1231
- stringParam +
1232
- '))';
1233
- params[stringParam] = stringValue;
1234
- } else {
1235
- curQuery +=
1236
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1237
- ieTable +
1238
- '."guid" IN (SELECT "guid" FROM ' +
1239
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1240
- ' WHERE "name"=@' +
1241
- name +
1242
- ' AND position(@' +
1243
- value +
1244
- ' IN "value")>0)';
1245
- }
1328
+ curQuery +=
1329
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1330
+ 'EXISTS (SELECT "guid" FROM ' +
1331
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1332
+ ' WHERE "guid"=' +
1333
+ ieTable +
1334
+ '."guid" AND "name"=@' +
1335
+ name +
1336
+ ' AND "json" @> @' +
1337
+ value +
1338
+ ')';
1246
1339
  params[name] = curValue[0];
1247
- params[value] = svalue;
1340
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
1248
1341
  }
1249
1342
  break;
1250
1343
  case 'match':
@@ -1285,18 +1378,17 @@ export default class PostgreSQLDriver extends NymphDriver {
1285
1378
  const value = `param${++count.i}`;
1286
1379
  curQuery +=
1287
1380
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1381
+ 'EXISTS (SELECT "guid" FROM ' +
1382
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1383
+ ' WHERE "guid"=' +
1288
1384
  ieTable +
1289
- '."guid" IN (SELECT "guid" FROM ' +
1290
- PostgreSQLDriver.escape(
1291
- this.prefix + 'comparisons_' + etype
1292
- ) +
1293
- ' WHERE "name"=@' +
1385
+ '."guid" AND "name"=@' +
1294
1386
  name +
1295
1387
  ' AND "string" ~ @' +
1296
1388
  value +
1297
1389
  ')';
1298
1390
  params[name] = curValue[0];
1299
- params[value] = curValue[1];
1391
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
1300
1392
  }
1301
1393
  break;
1302
1394
  case 'imatch':
@@ -1337,18 +1429,17 @@ export default class PostgreSQLDriver extends NymphDriver {
1337
1429
  const value = `param${++count.i}`;
1338
1430
  curQuery +=
1339
1431
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1432
+ 'EXISTS (SELECT "guid" FROM ' +
1433
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1434
+ ' WHERE "guid"=' +
1340
1435
  ieTable +
1341
- '."guid" IN (SELECT "guid" FROM ' +
1342
- PostgreSQLDriver.escape(
1343
- this.prefix + 'comparisons_' + etype
1344
- ) +
1345
- ' WHERE "name"=@' +
1436
+ '."guid" AND "name"=@' +
1346
1437
  name +
1347
1438
  ' AND "string" ~* @' +
1348
1439
  value +
1349
1440
  ')';
1350
1441
  params[name] = curValue[0];
1351
- params[value] = curValue[1];
1442
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
1352
1443
  }
1353
1444
  break;
1354
1445
  case 'like':
@@ -1389,18 +1480,17 @@ export default class PostgreSQLDriver extends NymphDriver {
1389
1480
  const value = `param${++count.i}`;
1390
1481
  curQuery +=
1391
1482
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1483
+ 'EXISTS (SELECT "guid" FROM ' +
1484
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1485
+ ' WHERE "guid"=' +
1392
1486
  ieTable +
1393
- '."guid" IN (SELECT "guid" FROM ' +
1394
- PostgreSQLDriver.escape(
1395
- this.prefix + 'comparisons_' + etype
1396
- ) +
1397
- ' WHERE "name"=@' +
1487
+ '."guid" AND "name"=@' +
1398
1488
  name +
1399
1489
  ' AND "string" LIKE @' +
1400
1490
  value +
1401
1491
  ')';
1402
1492
  params[name] = curValue[0];
1403
- params[value] = curValue[1];
1493
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
1404
1494
  }
1405
1495
  break;
1406
1496
  case 'ilike':
@@ -1441,18 +1531,17 @@ export default class PostgreSQLDriver extends NymphDriver {
1441
1531
  const value = `param${++count.i}`;
1442
1532
  curQuery +=
1443
1533
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1534
+ 'EXISTS (SELECT "guid" FROM ' +
1535
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1536
+ ' WHERE "guid"=' +
1444
1537
  ieTable +
1445
- '."guid" IN (SELECT "guid" FROM ' +
1446
- PostgreSQLDriver.escape(
1447
- this.prefix + 'comparisons_' + etype
1448
- ) +
1449
- ' WHERE "name"=@' +
1538
+ '."guid" AND "name"=@' +
1450
1539
  name +
1451
1540
  ' AND "string" ILIKE @' +
1452
1541
  value +
1453
1542
  ')';
1454
1543
  params[name] = curValue[0];
1455
- params[value] = curValue[1];
1544
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
1456
1545
  }
1457
1546
  break;
1458
1547
  case 'gt':
@@ -1493,12 +1582,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1493
1582
  const value = `param${++count.i}`;
1494
1583
  curQuery +=
1495
1584
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1585
+ 'EXISTS (SELECT "guid" FROM ' +
1586
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1587
+ ' WHERE "guid"=' +
1496
1588
  ieTable +
1497
- '."guid" IN (SELECT "guid" FROM ' +
1498
- PostgreSQLDriver.escape(
1499
- this.prefix + 'comparisons_' + etype
1500
- ) +
1501
- ' WHERE "name"=@' +
1589
+ '."guid" AND "name"=@' +
1502
1590
  name +
1503
1591
  ' AND "number">@' +
1504
1592
  value +
@@ -1547,12 +1635,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1547
1635
  const value = `param${++count.i}`;
1548
1636
  curQuery +=
1549
1637
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1638
+ 'EXISTS (SELECT "guid" FROM ' +
1639
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1640
+ ' WHERE "guid"=' +
1550
1641
  ieTable +
1551
- '."guid" IN (SELECT "guid" FROM ' +
1552
- PostgreSQLDriver.escape(
1553
- this.prefix + 'comparisons_' + etype
1554
- ) +
1555
- ' WHERE "name"=@' +
1642
+ '."guid" AND "name"=@' +
1556
1643
  name +
1557
1644
  ' AND "number">=@' +
1558
1645
  value +
@@ -1601,12 +1688,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1601
1688
  const value = `param${++count.i}`;
1602
1689
  curQuery +=
1603
1690
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1691
+ 'EXISTS (SELECT "guid" FROM ' +
1692
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1693
+ ' WHERE "guid"=' +
1604
1694
  ieTable +
1605
- '."guid" IN (SELECT "guid" FROM ' +
1606
- PostgreSQLDriver.escape(
1607
- this.prefix + 'comparisons_' + etype
1608
- ) +
1609
- ' WHERE "name"=@' +
1695
+ '."guid" AND "name"=@' +
1610
1696
  name +
1611
1697
  ' AND "number"<@' +
1612
1698
  value +
@@ -1655,12 +1741,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1655
1741
  const value = `param${++count.i}`;
1656
1742
  curQuery +=
1657
1743
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1744
+ 'EXISTS (SELECT "guid" FROM ' +
1745
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1746
+ ' WHERE "guid"=' +
1658
1747
  ieTable +
1659
- '."guid" IN (SELECT "guid" FROM ' +
1660
- PostgreSQLDriver.escape(
1661
- this.prefix + 'comparisons_' + etype
1662
- ) +
1663
- ' WHERE "name"=@' +
1748
+ '."guid" AND "name"=@' +
1664
1749
  name +
1665
1750
  ' AND "number"<=@' +
1666
1751
  value +
@@ -1688,10 +1773,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1688
1773
  const guid = `param${++count.i}`;
1689
1774
  curQuery +=
1690
1775
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1691
- ieTable +
1692
- '."guid" IN (SELECT "guid" FROM ' +
1776
+ 'EXISTS (SELECT "guid" FROM ' +
1693
1777
  PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1694
- ' WHERE "name"=@' +
1778
+ ' WHERE "guid"=' +
1779
+ ieTable +
1780
+ '."guid" AND "name"=@' +
1695
1781
  name +
1696
1782
  ' AND "reference"=decode(@' +
1697
1783
  guid +
@@ -1709,7 +1795,7 @@ export default class PostgreSQLDriver extends NymphDriver {
1709
1795
  params,
1710
1796
  true,
1711
1797
  tableSuffix,
1712
- etypes
1798
+ etypes,
1713
1799
  );
1714
1800
  if (curQuery) {
1715
1801
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1722,9 +1808,10 @@ export default class PostgreSQLDriver extends NymphDriver {
1722
1808
  break;
1723
1809
  case 'qref':
1724
1810
  case '!qref':
1811
+ const referenceTableSuffix = makeTableSuffix();
1725
1812
  const [qrefOptions, ...qrefSelectors] = curValue[1] as [
1726
1813
  Options,
1727
- ...FormattedSelector[]
1814
+ ...FormattedSelector[],
1728
1815
  ];
1729
1816
  const QrefEntityClass = qrefOptions.class as EntityConstructor;
1730
1817
  etypes.push(QrefEntityClass.ETYPE);
@@ -1736,7 +1823,8 @@ export default class PostgreSQLDriver extends NymphDriver {
1736
1823
  params,
1737
1824
  false,
1738
1825
  makeTableSuffix(),
1739
- etypes
1826
+ etypes,
1827
+ 'r' + referenceTableSuffix + '."reference"',
1740
1828
  );
1741
1829
  if (curQuery) {
1742
1830
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1744,12 +1832,19 @@ export default class PostgreSQLDriver extends NymphDriver {
1744
1832
  const qrefName = `param${++count.i}`;
1745
1833
  curQuery +=
1746
1834
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1747
- ieTable +
1748
- '."guid" IN (SELECT "guid" FROM ' +
1835
+ 'EXISTS (SELECT "guid" FROM ' +
1749
1836
  PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1750
- ' WHERE "name"=@' +
1837
+ ' r' +
1838
+ referenceTableSuffix +
1839
+ ' WHERE r' +
1840
+ referenceTableSuffix +
1841
+ '."guid"=' +
1842
+ ieTable +
1843
+ '."guid" AND r' +
1844
+ referenceTableSuffix +
1845
+ '."name"=@' +
1751
1846
  qrefName +
1752
- ' AND "reference" IN (' +
1847
+ ' AND EXISTS (' +
1753
1848
  qrefQuery.query +
1754
1849
  '))';
1755
1850
  params[qrefName] = curValue[0];
@@ -1757,22 +1852,42 @@ export default class PostgreSQLDriver extends NymphDriver {
1757
1852
  }
1758
1853
  }
1759
1854
  return curQuery;
1760
- }
1855
+ },
1761
1856
  );
1762
1857
 
1763
1858
  let sortBy: string;
1859
+ let sortByInner: string;
1860
+ let sortJoin = '';
1861
+ let sortJoinInner = '';
1862
+ const order = options.reverse ? ' DESC' : '';
1764
1863
  switch (sort) {
1765
1864
  case 'mdate':
1766
- sortBy = '"mdate"';
1865
+ sortBy = `${eTable}."mdate"${order}`;
1866
+ sortByInner = `${ieTable}."mdate"${order}`;
1767
1867
  break;
1768
1868
  case 'cdate':
1869
+ sortBy = `${eTable}."cdate"${order}`;
1870
+ sortByInner = `${ieTable}."cdate"${order}`;
1871
+ break;
1769
1872
  default:
1770
- sortBy = '"cdate"';
1873
+ const name = `param${++count.i}`;
1874
+ sortJoin = `LEFT JOIN (
1875
+ SELECT "guid", "string", "number"
1876
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1877
+ WHERE "name"=@${name}
1878
+ ORDER BY "number"${order}, "string"${order}
1879
+ ) ${sTable} ON ${eTable}."guid"=${sTable}."guid"`;
1880
+ sortJoinInner = `LEFT JOIN (
1881
+ SELECT "guid", "string", "number"
1882
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1883
+ WHERE "name"=@${name}
1884
+ ORDER BY "number"${order}, "string"${order}
1885
+ ) ${sTable} ON ${ieTable}."guid"=${sTable}."guid"`;
1886
+ sortBy = `${sTable}."number"${order}, ${sTable}."string"${order}`;
1887
+ sortByInner = sortBy;
1888
+ params[name] = sort;
1771
1889
  break;
1772
1890
  }
1773
- if (options.reverse) {
1774
- sortBy += ' DESC';
1775
- }
1776
1891
 
1777
1892
  let query: string;
1778
1893
  if (queryParts.length) {
@@ -1782,31 +1897,34 @@ export default class PostgreSQLDriver extends NymphDriver {
1782
1897
  let limit = '';
1783
1898
  if ('limit' in options) {
1784
1899
  limit = ` LIMIT ${Math.floor(
1785
- isNaN(Number(options.limit)) ? 0 : Number(options.limit)
1900
+ isNaN(Number(options.limit)) ? 0 : Number(options.limit),
1786
1901
  )}`;
1787
1902
  }
1788
1903
  let offset = '';
1789
1904
  if ('offset' in options) {
1790
1905
  offset = ` OFFSET ${Math.floor(
1791
- isNaN(Number(options.offset)) ? 0 : Number(options.offset)
1906
+ isNaN(Number(options.offset)) ? 0 : Number(options.offset),
1792
1907
  )}`;
1793
1908
  }
1794
1909
  const whereClause = queryParts.join(') AND (');
1910
+ const guidClause = guidSelector
1911
+ ? `${ieTable}."guid"=${guidSelector} AND `
1912
+ : '';
1795
1913
  if (options.return === 'count') {
1796
1914
  if (limit || offset) {
1797
1915
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1798
- SELECT COUNT(${ieTable}."guid") AS "guid"
1916
+ SELECT ${ieTable}."guid" AS "guid"
1799
1917
  FROM ${PostgreSQLDriver.escape(
1800
- `${this.prefix}entities_${etype}`
1918
+ `${this.prefix}entities_${etype}`,
1801
1919
  )} ${ieTable}
1802
- WHERE (${whereClause})${limit}${offset}
1920
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1803
1921
  ) ${countTable}`;
1804
1922
  } else {
1805
1923
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1806
1924
  FROM ${PostgreSQLDriver.escape(
1807
- `${this.prefix}entities_${etype}`
1925
+ `${this.prefix}entities_${etype}`,
1808
1926
  )} ${ieTable}
1809
- WHERE (${whereClause})`;
1927
+ WHERE ${guidClause}(${whereClause})`;
1810
1928
  }
1811
1929
  } else if (options.return === 'guid') {
1812
1930
  const guidColumn =
@@ -1815,10 +1933,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1815
1933
  : `${ieTable}."guid"`;
1816
1934
  query = `SELECT ${guidColumn} AS "guid"
1817
1935
  FROM ${PostgreSQLDriver.escape(
1818
- `${this.prefix}entities_${etype}`
1936
+ `${this.prefix}entities_${etype}`,
1819
1937
  )} ${ieTable}
1820
- WHERE (${whereClause})
1821
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1938
+ ${sortJoinInner}
1939
+ WHERE ${guidClause}(${whereClause})
1940
+ ORDER BY ${sortByInner}, ${ieTable}."guid"${limit}${offset}`;
1822
1941
  } else {
1823
1942
  query = `SELECT
1824
1943
  encode(${eTable}."guid", 'hex') AS "guid",
@@ -1827,26 +1946,26 @@ export default class PostgreSQLDriver extends NymphDriver {
1827
1946
  ${eTable}."mdate",
1828
1947
  ${dTable}."name",
1829
1948
  ${dTable}."value",
1830
- ${cTable}."string",
1831
- ${cTable}."number"
1949
+ ${dTable}."json",
1950
+ ${dTable}."string",
1951
+ ${dTable}."number"
1832
1952
  FROM ${PostgreSQLDriver.escape(
1833
- `${this.prefix}entities_${etype}`
1953
+ `${this.prefix}entities_${etype}`,
1834
1954
  )} ${eTable}
1835
1955
  LEFT JOIN ${PostgreSQLDriver.escape(
1836
- `${this.prefix}data_${etype}`
1956
+ `${this.prefix}data_${etype}`,
1837
1957
  )} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1838
- INNER JOIN ${PostgreSQLDriver.escape(
1839
- `${this.prefix}comparisons_${etype}`
1840
- )} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1958
+ ${sortJoin}
1841
1959
  INNER JOIN (
1842
1960
  SELECT ${ieTable}."guid"
1843
1961
  FROM ${PostgreSQLDriver.escape(
1844
- `${this.prefix}entities_${etype}`
1962
+ `${this.prefix}entities_${etype}`,
1845
1963
  )} ${ieTable}
1846
- WHERE (${whereClause})
1847
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1964
+ ${sortJoinInner}
1965
+ WHERE ${guidClause}(${whereClause})
1966
+ ORDER BY ${sortByInner}${limit}${offset}
1848
1967
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1849
- ORDER BY ${eTable}.${sortBy}`;
1968
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1850
1969
  }
1851
1970
  }
1852
1971
  } else {
@@ -1856,28 +1975,31 @@ export default class PostgreSQLDriver extends NymphDriver {
1856
1975
  let limit = '';
1857
1976
  if ('limit' in options) {
1858
1977
  limit = ` LIMIT ${Math.floor(
1859
- isNaN(Number(options.limit)) ? 0 : Number(options.limit)
1978
+ isNaN(Number(options.limit)) ? 0 : Number(options.limit),
1860
1979
  )}`;
1861
1980
  }
1862
1981
  let offset = '';
1863
1982
  if ('offset' in options) {
1864
1983
  offset = ` OFFSET ${Math.floor(
1865
- isNaN(Number(options.offset)) ? 0 : Number(options.offset)
1984
+ isNaN(Number(options.offset)) ? 0 : Number(options.offset),
1866
1985
  )}`;
1867
1986
  }
1987
+ const guidClause = guidSelector
1988
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
1989
+ : '';
1868
1990
  if (options.return === 'count') {
1869
1991
  if (limit || offset) {
1870
1992
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1871
- SELECT COUNT(${ieTable}."guid") AS "guid"
1993
+ SELECT ${ieTable}."guid" AS "guid"
1872
1994
  FROM ${PostgreSQLDriver.escape(
1873
- `${this.prefix}entities_${etype}`
1874
- )} ${ieTable}${limit}${offset}
1995
+ `${this.prefix}entities_${etype}`,
1996
+ )} ${ieTable}${guidClause}${limit}${offset}
1875
1997
  ) ${countTable}`;
1876
1998
  } else {
1877
1999
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1878
2000
  FROM ${PostgreSQLDriver.escape(
1879
- `${this.prefix}entities_${etype}`
1880
- )} ${ieTable}`;
2001
+ `${this.prefix}entities_${etype}`,
2002
+ )} ${ieTable}${guidClause}`;
1881
2003
  }
1882
2004
  } else if (options.return === 'guid') {
1883
2005
  const guidColumn =
@@ -1886,9 +2008,11 @@ export default class PostgreSQLDriver extends NymphDriver {
1886
2008
  : `${ieTable}."guid"`;
1887
2009
  query = `SELECT ${guidColumn} AS "guid"
1888
2010
  FROM ${PostgreSQLDriver.escape(
1889
- `${this.prefix}entities_${etype}`
2011
+ `${this.prefix}entities_${etype}`,
1890
2012
  )} ${ieTable}
1891
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
2013
+ ${sortJoinInner}
2014
+ ${guidClause}
2015
+ ORDER BY ${sortByInner}, ${ieTable}."guid"${limit}${offset}`;
1892
2016
  } else {
1893
2017
  if (limit || offset) {
1894
2018
  query = `SELECT
@@ -1898,25 +2022,26 @@ export default class PostgreSQLDriver extends NymphDriver {
1898
2022
  ${eTable}."mdate",
1899
2023
  ${dTable}."name",
1900
2024
  ${dTable}."value",
1901
- ${cTable}."string",
1902
- ${cTable}."number"
2025
+ ${dTable}."json",
2026
+ ${dTable}."string",
2027
+ ${dTable}."number"
1903
2028
  FROM ${PostgreSQLDriver.escape(
1904
- `${this.prefix}entities_${etype}`
2029
+ `${this.prefix}entities_${etype}`,
1905
2030
  )} ${eTable}
1906
2031
  LEFT JOIN ${PostgreSQLDriver.escape(
1907
- `${this.prefix}data_${etype}`
2032
+ `${this.prefix}data_${etype}`,
1908
2033
  )} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1909
- INNER JOIN ${PostgreSQLDriver.escape(
1910
- `${this.prefix}comparisons_${etype}`
1911
- )} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
2034
+ ${sortJoin}
1912
2035
  INNER JOIN (
1913
2036
  SELECT ${ieTable}."guid"
1914
2037
  FROM ${PostgreSQLDriver.escape(
1915
- `${this.prefix}entities_${etype}`
2038
+ `${this.prefix}entities_${etype}`,
1916
2039
  )} ${ieTable}
1917
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
2040
+ ${sortJoinInner}
2041
+ ${guidClause}
2042
+ ORDER BY ${sortByInner}${limit}${offset}
1918
2043
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1919
- ORDER BY ${eTable}.${sortBy}`;
2044
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1920
2045
  } else {
1921
2046
  query = `SELECT
1922
2047
  encode(${eTable}."guid", 'hex') AS "guid",
@@ -1925,18 +2050,18 @@ export default class PostgreSQLDriver extends NymphDriver {
1925
2050
  ${eTable}."mdate",
1926
2051
  ${dTable}."name",
1927
2052
  ${dTable}."value",
1928
- ${cTable}."string",
1929
- ${cTable}."number"
2053
+ ${dTable}."json",
2054
+ ${dTable}."string",
2055
+ ${dTable}."number"
1930
2056
  FROM ${PostgreSQLDriver.escape(
1931
- `${this.prefix}entities_${etype}`
2057
+ `${this.prefix}entities_${etype}`,
1932
2058
  )} ${eTable}
1933
2059
  LEFT JOIN ${PostgreSQLDriver.escape(
1934
- `${this.prefix}data_${etype}`
2060
+ `${this.prefix}data_${etype}`,
1935
2061
  )} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1936
- INNER JOIN ${PostgreSQLDriver.escape(
1937
- `${this.prefix}comparisons_${etype}`
1938
- )} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1939
- ORDER BY ${eTable}.${sortBy}`;
2062
+ ${sortJoin}
2063
+ ${guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
2064
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1940
2065
  }
1941
2066
  }
1942
2067
  }
@@ -1956,43 +2081,23 @@ export default class PostgreSQLDriver extends NymphDriver {
1956
2081
  protected performQuery(
1957
2082
  options: Options,
1958
2083
  formattedSelectors: FormattedSelector[],
1959
- etype: string
2084
+ etype: string,
1960
2085
  ): {
1961
2086
  result: any;
1962
2087
  } {
1963
2088
  const { query, params, etypes } = this.makeEntityQuery(
1964
2089
  options,
1965
2090
  formattedSelectors,
1966
- etype
2091
+ etype,
1967
2092
  );
1968
- const result = this.queryIter(query, { etypes, params }).then((val) =>
1969
- val[Symbol.iterator]()
2093
+ const result = this.queryArray(query, { etypes, params }).then((val) =>
2094
+ val[Symbol.iterator](),
1970
2095
  );
1971
2096
  return {
1972
2097
  result,
1973
2098
  };
1974
2099
  }
1975
2100
 
1976
- protected performQuerySync(
1977
- options: Options,
1978
- formattedSelectors: FormattedSelector[],
1979
- etype: string
1980
- ): {
1981
- result: any;
1982
- } {
1983
- const { query, params, etypes } = this.makeEntityQuery(
1984
- options,
1985
- formattedSelectors,
1986
- etype
1987
- );
1988
- const result = (this.queryIterSync(query, { etypes, params }) || [])[
1989
- Symbol.iterator
1990
- ]();
1991
- return {
1992
- result,
1993
- };
1994
- }
1995
-
1996
2101
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1997
2102
  options: Options<T> & { return: 'count' },
1998
2103
  ...selectors: Selector[]
@@ -2004,17 +2109,17 @@ export default class PostgreSQLDriver extends NymphDriver {
2004
2109
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
2005
2110
  options?: Options<T>,
2006
2111
  ...selectors: Selector[]
2007
- ): Promise<ReturnType<T['factorySync']>[]>;
2112
+ ): Promise<EntityInstanceType<T>[]>;
2008
2113
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
2009
2114
  options: Options<T> = {},
2010
2115
  ...selectors: Selector[]
2011
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number> {
2012
- const { result: resultPromise, process } = this.getEntitesRowLike<T>(
2116
+ ): Promise<EntityInstanceType<T>[] | string[] | number> {
2117
+ const { result: resultPromise, process } = this.getEntitiesRowLike<T>(
2013
2118
  // @ts-ignore: options is correct here.
2014
2119
  options,
2015
2120
  selectors,
2016
- (options, formattedSelectors, etype) =>
2017
- this.performQuery(options, formattedSelectors, etype),
2121
+ ({ options, selectors, etype }) =>
2122
+ this.performQuery(options, selectors, etype),
2018
2123
  () => {
2019
2124
  const next: any = result.next();
2020
2125
  return next.done ? null : next.value;
@@ -2023,7 +2128,7 @@ export default class PostgreSQLDriver extends NymphDriver {
2023
2128
  (row) => Number(row.count),
2024
2129
  (row) => row.guid,
2025
2130
  (row) => ({
2026
- tags: row.tags,
2131
+ tags: row.tags.filter((tag: string) => tag),
2027
2132
  cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
2028
2133
  mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
2029
2134
  }),
@@ -2033,9 +2138,13 @@ export default class PostgreSQLDriver extends NymphDriver {
2033
2138
  row.value === 'N'
2034
2139
  ? JSON.stringify(Number(row.number))
2035
2140
  : row.value === 'S'
2036
- ? JSON.stringify(row.string)
2037
- : row.value,
2038
- })
2141
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(row.string))
2142
+ : row.value === 'J'
2143
+ ? PostgreSQLDriver.unescapeNullSequences(
2144
+ JSON.stringify(row.json),
2145
+ )
2146
+ : row.value,
2147
+ }),
2039
2148
  );
2040
2149
 
2041
2150
  const result = await resultPromise;
@@ -2046,252 +2155,228 @@ export default class PostgreSQLDriver extends NymphDriver {
2046
2155
  return value;
2047
2156
  }
2048
2157
 
2049
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2050
- options: Options<T> & { return: 'count' },
2051
- ...selectors: Selector[]
2052
- ): number;
2053
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2054
- options: Options<T> & { return: 'guid' },
2055
- ...selectors: Selector[]
2056
- ): string[];
2057
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2058
- options?: Options<T>,
2059
- ...selectors: Selector[]
2060
- ): ReturnType<T['factorySync']>[];
2061
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2062
- options: Options<T> = {},
2063
- ...selectors: Selector[]
2064
- ): ReturnType<T['factorySync']>[] | string[] | number {
2065
- const { result, process } = this.getEntitesRowLike<T>(
2066
- // @ts-ignore: options is correct here.
2067
- options,
2068
- selectors,
2069
- (options, formattedSelectors, etype) =>
2070
- this.performQuerySync(options, formattedSelectors, etype),
2071
- () => {
2072
- const next: any = result.next();
2073
- return next.done ? null : next.value;
2074
- },
2075
- () => undefined,
2076
- (row) => Number(row.count),
2077
- (row) => row.guid,
2078
- (row) => ({
2079
- tags: row.tags,
2080
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
2081
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
2082
- }),
2083
- (row) => ({
2084
- name: row.name,
2085
- svalue:
2086
- row.value === 'N'
2087
- ? JSON.stringify(Number(row.number))
2088
- : row.value === 'S'
2089
- ? JSON.stringify(row.string)
2090
- : row.value,
2091
- })
2092
- );
2093
- const value = process();
2094
- if (value instanceof Error) {
2095
- throw value;
2096
- }
2097
- return value;
2098
- }
2099
-
2100
2158
  public async getUID(name: string) {
2101
2159
  if (name == null) {
2102
2160
  throw new InvalidParametersError('Name not given for UID.');
2103
2161
  }
2104
2162
  const result = await this.queryGet(
2105
2163
  `SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(
2106
- `${this.prefix}uids`
2164
+ `${this.prefix}uids`,
2107
2165
  )} WHERE "name"=@name;`,
2108
2166
  {
2109
2167
  params: {
2110
2168
  name: name,
2111
2169
  },
2112
- }
2170
+ },
2113
2171
  );
2114
2172
  return result?.cur_uid == null ? null : Number(result.cur_uid);
2115
2173
  }
2116
2174
 
2117
- public async import(filename: string) {
2175
+ public async importEntity({
2176
+ guid,
2177
+ cdate,
2178
+ mdate,
2179
+ tags,
2180
+ sdata,
2181
+ etype,
2182
+ }: {
2183
+ guid: string;
2184
+ cdate: number;
2185
+ mdate: number;
2186
+ tags: string[];
2187
+ sdata: SerializedEntityData;
2188
+ etype: string;
2189
+ }) {
2118
2190
  try {
2119
- const result = await this.importFromFile(
2120
- filename,
2121
- async (guid, tags, sdata, etype) => {
2122
- await this.queryRun(
2123
- `DELETE FROM ${PostgreSQLDriver.escape(
2124
- `${this.prefix}entities_${etype}`
2125
- )} WHERE "guid"=decode(@guid, 'hex');`,
2126
- {
2127
- etypes: [etype],
2128
- params: {
2129
- guid,
2130
- },
2131
- }
2132
- );
2133
- await this.queryRun(
2191
+ let promises = [];
2192
+ promises.push(
2193
+ this.queryRun(
2194
+ `DELETE FROM ${PostgreSQLDriver.escape(
2195
+ `${this.prefix}entities_${etype}`,
2196
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2197
+ {
2198
+ etypes: [etype],
2199
+ params: {
2200
+ guid,
2201
+ },
2202
+ },
2203
+ ),
2204
+ );
2205
+ promises.push(
2206
+ this.queryRun(
2207
+ `DELETE FROM ${PostgreSQLDriver.escape(
2208
+ `${this.prefix}data_${etype}`,
2209
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2210
+ {
2211
+ etypes: [etype],
2212
+ params: {
2213
+ guid,
2214
+ },
2215
+ },
2216
+ ),
2217
+ );
2218
+ promises.push(
2219
+ this.queryRun(
2220
+ `DELETE FROM ${PostgreSQLDriver.escape(
2221
+ `${this.prefix}references_${etype}`,
2222
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2223
+ {
2224
+ etypes: [etype],
2225
+ params: {
2226
+ guid,
2227
+ },
2228
+ },
2229
+ ),
2230
+ );
2231
+ promises.push(
2232
+ this.queryRun(
2233
+ `DELETE FROM ${PostgreSQLDriver.escape(
2234
+ `${this.prefix}uniques_${etype}`,
2235
+ )} WHERE "guid"=decode(@guid, 'hex');`,
2236
+ {
2237
+ etypes: [etype],
2238
+ params: {
2239
+ guid,
2240
+ },
2241
+ },
2242
+ ),
2243
+ );
2244
+ await Promise.all(promises);
2245
+ promises = [];
2246
+ await this.queryRun(
2247
+ `INSERT INTO ${PostgreSQLDriver.escape(
2248
+ `${this.prefix}entities_${etype}`,
2249
+ )} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`,
2250
+ {
2251
+ etypes: [etype],
2252
+ params: {
2253
+ guid,
2254
+ tags,
2255
+ cdate: isNaN(cdate) ? null : cdate,
2256
+ mdate: isNaN(mdate) ? null : mdate,
2257
+ },
2258
+ },
2259
+ );
2260
+ for (const name in sdata) {
2261
+ const value = sdata[name];
2262
+ const uvalue = JSON.parse(value);
2263
+ if (value === undefined) {
2264
+ continue;
2265
+ }
2266
+ const storageValue =
2267
+ typeof uvalue === 'number'
2268
+ ? 'N'
2269
+ : typeof uvalue === 'string'
2270
+ ? 'S'
2271
+ : 'J';
2272
+ const jsonValue =
2273
+ storageValue === 'J'
2274
+ ? PostgreSQLDriver.escapeNullSequences(value)
2275
+ : null;
2276
+ promises.push(
2277
+ this.queryRun(
2134
2278
  `INSERT INTO ${PostgreSQLDriver.escape(
2135
- `${this.prefix}entities_${etype}`
2136
- )} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`,
2279
+ `${this.prefix}data_${etype}`,
2280
+ )} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (decode(@guid, 'hex'), @name, @storageValue, @jsonValue, @string, @number, @truthy);`,
2137
2281
  {
2138
2282
  etypes: [etype],
2139
2283
  params: {
2140
2284
  guid,
2141
- tags,
2142
- cdate: isNaN(Number(JSON.parse(sdata.cdate)))
2143
- ? null
2144
- : Number(JSON.parse(sdata.cdate)),
2145
- mdate: isNaN(Number(JSON.parse(sdata.mdate)))
2146
- ? null
2147
- : Number(JSON.parse(sdata.mdate)),
2285
+ name,
2286
+ storageValue,
2287
+ jsonValue,
2288
+ string:
2289
+ storageValue === 'J'
2290
+ ? null
2291
+ : PostgreSQLDriver.escapeNulls(`${uvalue}`),
2292
+ number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2293
+ truthy: !!uvalue,
2148
2294
  },
2149
- }
2150
- );
2151
- const promises = [];
2152
- promises.push(
2153
- this.queryRun(
2154
- `DELETE FROM ${PostgreSQLDriver.escape(
2155
- `${this.prefix}data_${etype}`
2156
- )} WHERE "guid"=decode(@guid, 'hex');`,
2157
- {
2158
- etypes: [etype],
2159
- params: {
2160
- guid,
2161
- },
2162
- }
2163
- )
2164
- );
2165
- promises.push(
2166
- this.queryRun(
2167
- `DELETE FROM ${PostgreSQLDriver.escape(
2168
- `${this.prefix}comparisons_${etype}`
2169
- )} WHERE "guid"=decode(@guid, 'hex');`,
2170
- {
2171
- etypes: [etype],
2172
- params: {
2173
- guid,
2174
- },
2175
- }
2176
- )
2177
- );
2295
+ },
2296
+ ),
2297
+ );
2298
+ const references = this.findReferences(value);
2299
+ for (const reference of references) {
2178
2300
  promises.push(
2179
2301
  this.queryRun(
2180
- `DELETE FROM ${PostgreSQLDriver.escape(
2181
- `${this.prefix}references_${etype}`
2182
- )} WHERE "guid"=decode(@guid, 'hex');`,
2302
+ `INSERT INTO ${PostgreSQLDriver.escape(
2303
+ `${this.prefix}references_${etype}`,
2304
+ )} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`,
2183
2305
  {
2184
2306
  etypes: [etype],
2185
2307
  params: {
2186
2308
  guid,
2309
+ name,
2310
+ reference,
2187
2311
  },
2188
- }
2189
- )
2190
- );
2191
- await Promise.all(promises);
2192
- delete sdata.cdate;
2193
- delete sdata.mdate;
2194
- for (const name in sdata) {
2195
- const value = sdata[name];
2196
- const uvalue = JSON.parse(value);
2197
- if (value === undefined) {
2198
- continue;
2199
- }
2200
- const storageValue =
2201
- typeof uvalue === 'number'
2202
- ? 'N'
2203
- : typeof uvalue === 'string'
2204
- ? 'S'
2205
- : value;
2206
- const promises = [];
2207
- promises.push(
2208
- this.queryRun(
2209
- `INSERT INTO ${PostgreSQLDriver.escape(
2210
- `${this.prefix}data_${etype}`
2211
- )} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`,
2212
- {
2213
- etypes: [etype],
2214
- params: {
2215
- guid,
2216
- name,
2217
- storageValue,
2218
- },
2219
- }
2220
- )
2221
- );
2222
- promises.push(
2223
- this.queryRun(
2224
- `INSERT INTO ${PostgreSQLDriver.escape(
2225
- `${this.prefix}comparisons_${etype}`
2226
- )} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`,
2227
- {
2228
- etypes: [etype],
2229
- params: {
2230
- guid,
2231
- name,
2232
- truthy: !!uvalue,
2233
- string: `${uvalue}`,
2234
- number: isNaN(Number(uvalue)) ? null : Number(uvalue),
2235
- },
2236
- }
2237
- )
2238
- );
2239
- const references = this.findReferences(value);
2240
- for (const reference of references) {
2241
- promises.push(
2242
- this.queryRun(
2243
- `INSERT INTO ${PostgreSQLDriver.escape(
2244
- `${this.prefix}references_${etype}`
2245
- )} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`,
2246
- {
2247
- etypes: [etype],
2248
- params: {
2249
- guid,
2250
- name,
2251
- reference,
2252
- },
2253
- }
2254
- )
2255
- );
2256
- }
2257
- }
2258
- await Promise.all(promises);
2259
- },
2260
- async (name, curUid) => {
2261
- await this.queryRun(
2262
- `DELETE FROM ${PostgreSQLDriver.escape(
2263
- `${this.prefix}uids`
2264
- )} WHERE "name"=@name;`,
2265
- {
2266
- params: {
2267
- name,
2268
2312
  },
2269
- }
2313
+ ),
2270
2314
  );
2271
- await this.queryRun(
2315
+ }
2316
+ }
2317
+ const uniques = await this.nymph
2318
+ .getEntityClassByEtype(etype)
2319
+ .getUniques({ guid, cdate, mdate, tags, data: {}, sdata });
2320
+ for (const unique of uniques) {
2321
+ promises.push(
2322
+ this.queryRun(
2272
2323
  `INSERT INTO ${PostgreSQLDriver.escape(
2273
- `${this.prefix}uids`
2274
- )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2324
+ `${this.prefix}uniques_${etype}`,
2325
+ )} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`,
2275
2326
  {
2327
+ etypes: [etype],
2276
2328
  params: {
2277
- name,
2278
- curUid,
2329
+ guid,
2330
+ unique,
2279
2331
  },
2332
+ },
2333
+ ).catch((e: any) => {
2334
+ if (e instanceof EntityUniqueConstraintError) {
2335
+ this.nymph.config.debugError(
2336
+ 'postgresql',
2337
+ `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
2338
+ );
2280
2339
  }
2281
- );
2340
+ return e;
2341
+ }),
2342
+ );
2343
+ }
2344
+ await Promise.all(promises);
2345
+ } catch (e: any) {
2346
+ this.nymph.config.debugError('postgresql', `Import entity error: "${e}"`);
2347
+ throw e;
2348
+ }
2349
+ }
2350
+
2351
+ public async importUID({ name, value }: { name: string; value: number }) {
2352
+ try {
2353
+ await this.internalTransaction(`nymph-import-uid-${name}`);
2354
+ await this.queryRun(
2355
+ `DELETE FROM ${PostgreSQLDriver.escape(
2356
+ `${this.prefix}uids`,
2357
+ )} WHERE "name"=@name;`,
2358
+ {
2359
+ params: {
2360
+ name,
2361
+ },
2282
2362
  },
2283
- async () => {
2284
- await this.internalTransaction('nymph-import');
2363
+ );
2364
+ await this.queryRun(
2365
+ `INSERT INTO ${PostgreSQLDriver.escape(
2366
+ `${this.prefix}uids`,
2367
+ )} ("name", "cur_uid") VALUES (@name, @value);`,
2368
+ {
2369
+ params: {
2370
+ name,
2371
+ value,
2372
+ },
2285
2373
  },
2286
- async () => {
2287
- await this.commit('nymph-import');
2288
- }
2289
2374
  );
2290
-
2291
- return result;
2375
+ await this.commit(`nymph-import-uid-${name}`);
2292
2376
  } catch (e: any) {
2293
- await this.rollback('nymph-import');
2294
- return false;
2377
+ this.nymph.config.debugError('postgresql', `Import UID error: "${e}"`);
2378
+ await this.rollback(`nymph-import-uid-${name}`);
2379
+ throw e;
2295
2380
  }
2296
2381
  }
2297
2382
 
@@ -2300,52 +2385,54 @@ export default class PostgreSQLDriver extends NymphDriver {
2300
2385
  throw new InvalidParametersError('Name not given for UID.');
2301
2386
  }
2302
2387
  await this.internalTransaction('nymph-newuid');
2388
+ let curUid: number | undefined = undefined;
2303
2389
  try {
2304
2390
  const lock = await this.queryGet(
2305
2391
  `SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(
2306
- `${this.prefix}uids`
2392
+ `${this.prefix}uids`,
2307
2393
  )} WHERE "name"=@name FOR UPDATE;`,
2308
2394
  {
2309
2395
  params: {
2310
2396
  name,
2311
2397
  },
2312
- }
2398
+ },
2313
2399
  );
2314
- let curUid: number | undefined =
2315
- lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
2400
+ curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
2316
2401
  if (curUid == null) {
2317
2402
  curUid = 1;
2318
2403
  await this.queryRun(
2319
2404
  `INSERT INTO ${PostgreSQLDriver.escape(
2320
- `${this.prefix}uids`
2405
+ `${this.prefix}uids`,
2321
2406
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2322
2407
  {
2323
2408
  params: {
2324
2409
  name,
2325
2410
  curUid,
2326
2411
  },
2327
- }
2412
+ },
2328
2413
  );
2329
2414
  } else {
2330
2415
  curUid++;
2331
2416
  await this.queryRun(
2332
2417
  `UPDATE ${PostgreSQLDriver.escape(
2333
- `${this.prefix}uids`
2418
+ `${this.prefix}uids`,
2334
2419
  )} SET "cur_uid"=@curUid WHERE "name"=@name;`,
2335
2420
  {
2336
2421
  params: {
2337
2422
  name,
2338
2423
  curUid,
2339
2424
  },
2340
- }
2425
+ },
2341
2426
  );
2342
2427
  }
2343
- await this.commit('nymph-newuid');
2344
- return curUid;
2345
2428
  } catch (e: any) {
2429
+ this.nymph.config.debugError('postgresql', `New UID error: "${e}"`);
2346
2430
  await this.rollback('nymph-newuid');
2347
2431
  throw e;
2348
2432
  }
2433
+
2434
+ await this.commit('nymph-newuid');
2435
+ return curUid;
2349
2436
  }
2350
2437
 
2351
2438
  public async renameUID(oldName: string, newName: string) {
@@ -2354,14 +2441,14 @@ export default class PostgreSQLDriver extends NymphDriver {
2354
2441
  }
2355
2442
  await this.queryRun(
2356
2443
  `UPDATE ${PostgreSQLDriver.escape(
2357
- `${this.prefix}uids`
2444
+ `${this.prefix}uids`,
2358
2445
  )} SET "name"=@newName WHERE "name"=@oldName;`,
2359
2446
  {
2360
2447
  params: {
2361
2448
  newName,
2362
2449
  oldName,
2363
2450
  },
2364
- }
2451
+ },
2365
2452
  );
2366
2453
  return true;
2367
2454
  }
@@ -2369,7 +2456,7 @@ export default class PostgreSQLDriver extends NymphDriver {
2369
2456
  public async rollback(name: string) {
2370
2457
  if (name == null || typeof name !== 'string' || name.length === 0) {
2371
2458
  throw new InvalidParametersError(
2372
- 'Transaction rollback attempted without a name.'
2459
+ 'Transaction rollback attempted without a name.',
2373
2460
  );
2374
2461
  }
2375
2462
  if (!this.transaction || this.transaction.count === 0) {
@@ -2377,7 +2464,7 @@ export default class PostgreSQLDriver extends NymphDriver {
2377
2464
  return true;
2378
2465
  }
2379
2466
  await this.queryRun(
2380
- `ROLLBACK TO SAVEPOINT ${PostgreSQLDriver.escape(name)};`
2467
+ `ROLLBACK TO SAVEPOINT ${PostgreSQLDriver.escape(name)};`,
2381
2468
  );
2382
2469
  this.transaction.count--;
2383
2470
  if (this.transaction.count === 0) {
@@ -2394,12 +2481,13 @@ export default class PostgreSQLDriver extends NymphDriver {
2394
2481
  guid: string,
2395
2482
  data: EntityData,
2396
2483
  sdata: SerializedEntityData,
2397
- etype: string
2484
+ uniques: string[],
2485
+ etype: string,
2398
2486
  ) => {
2399
2487
  const runInsertQuery = async (
2400
2488
  name: string,
2401
2489
  value: any,
2402
- svalue: string
2490
+ svalue: string,
2403
2491
  ) => {
2404
2492
  if (value === undefined) {
2405
2493
  return;
@@ -2408,47 +2496,41 @@ export default class PostgreSQLDriver extends NymphDriver {
2408
2496
  typeof value === 'number'
2409
2497
  ? 'N'
2410
2498
  : typeof value === 'string'
2411
- ? 'S'
2412
- : svalue;
2499
+ ? 'S'
2500
+ : 'J';
2501
+ const jsonValue =
2502
+ storageValue === 'J'
2503
+ ? PostgreSQLDriver.escapeNullSequences(svalue)
2504
+ : null;
2413
2505
  const promises = [];
2414
2506
  promises.push(
2415
2507
  this.queryRun(
2416
2508
  `INSERT INTO ${PostgreSQLDriver.escape(
2417
- `${this.prefix}data_${etype}`
2418
- )} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`,
2509
+ `${this.prefix}data_${etype}`,
2510
+ )} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (decode(@guid, 'hex'), @name, @storageValue, @jsonValue, @string, @number, @truthy);`,
2419
2511
  {
2420
2512
  etypes: [etype],
2421
2513
  params: {
2422
2514
  guid,
2423
2515
  name,
2424
2516
  storageValue,
2425
- },
2426
- }
2427
- )
2428
- );
2429
- promises.push(
2430
- this.queryRun(
2431
- `INSERT INTO ${PostgreSQLDriver.escape(
2432
- `${this.prefix}comparisons_${etype}`
2433
- )} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`,
2434
- {
2435
- etypes: [etype],
2436
- params: {
2437
- guid,
2438
- name,
2439
- truthy: !!value,
2440
- string: `${value}`,
2517
+ jsonValue,
2518
+ string:
2519
+ storageValue === 'J'
2520
+ ? null
2521
+ : PostgreSQLDriver.escapeNulls(`${value}`),
2441
2522
  number: isNaN(Number(value)) ? null : Number(value),
2523
+ truthy: !!value,
2442
2524
  },
2443
- }
2444
- )
2525
+ },
2526
+ ),
2445
2527
  );
2446
2528
  const references = this.findReferences(svalue);
2447
2529
  for (const reference of references) {
2448
2530
  promises.push(
2449
2531
  this.queryRun(
2450
2532
  `INSERT INTO ${PostgreSQLDriver.escape(
2451
- `${this.prefix}references_${etype}`
2533
+ `${this.prefix}references_${etype}`,
2452
2534
  )} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`,
2453
2535
  {
2454
2536
  etypes: [etype],
@@ -2457,12 +2539,36 @@ export default class PostgreSQLDriver extends NymphDriver {
2457
2539
  name,
2458
2540
  reference,
2459
2541
  },
2460
- }
2461
- )
2542
+ },
2543
+ ),
2462
2544
  );
2463
2545
  }
2464
2546
  await Promise.all(promises);
2465
2547
  };
2548
+ for (const unique of uniques) {
2549
+ try {
2550
+ await this.queryRun(
2551
+ `INSERT INTO ${PostgreSQLDriver.escape(
2552
+ `${this.prefix}uniques_${etype}`,
2553
+ )} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`,
2554
+ {
2555
+ etypes: [etype],
2556
+ params: {
2557
+ guid,
2558
+ unique,
2559
+ },
2560
+ },
2561
+ );
2562
+ } catch (e: any) {
2563
+ if (e instanceof EntityUniqueConstraintError) {
2564
+ this.nymph.config.debugError(
2565
+ 'postgresql',
2566
+ `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
2567
+ );
2568
+ }
2569
+ throw e;
2570
+ }
2571
+ }
2466
2572
  for (const name in data) {
2467
2573
  await runInsertQuery(name, data[name], JSON.stringify(data[name]));
2468
2574
  }
@@ -2470,13 +2576,20 @@ export default class PostgreSQLDriver extends NymphDriver {
2470
2576
  await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
2471
2577
  }
2472
2578
  };
2579
+ let inTransaction = false;
2473
2580
  try {
2474
2581
  const result = await this.saveEntityRowLike(
2475
2582
  entity,
2476
- async (_entity, guid, tags, data, sdata, cdate, etype) => {
2583
+ async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
2584
+ if (
2585
+ Object.keys(data).length === 0 &&
2586
+ Object.keys(sdata).length === 0
2587
+ ) {
2588
+ return false;
2589
+ }
2477
2590
  await this.queryRun(
2478
2591
  `INSERT INTO ${PostgreSQLDriver.escape(
2479
- `${this.prefix}entities_${etype}`
2592
+ `${this.prefix}entities_${etype}`,
2480
2593
  )} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @cdate);`,
2481
2594
  {
2482
2595
  etypes: [etype],
@@ -2485,69 +2598,75 @@ export default class PostgreSQLDriver extends NymphDriver {
2485
2598
  tags,
2486
2599
  cdate,
2487
2600
  },
2488
- }
2601
+ },
2489
2602
  );
2490
- await insertData(guid, data, sdata, etype);
2603
+ await insertData(guid, data, sdata, uniques, etype);
2491
2604
  return true;
2492
2605
  },
2493
- async (entity, guid, tags, data, sdata, mdate, etype) => {
2606
+ async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
2607
+ if (
2608
+ Object.keys(data).length === 0 &&
2609
+ Object.keys(sdata).length === 0
2610
+ ) {
2611
+ return false;
2612
+ }
2494
2613
  const promises = [];
2495
2614
  promises.push(
2496
2615
  this.queryRun(
2497
2616
  `SELECT 1 FROM ${PostgreSQLDriver.escape(
2498
- `${this.prefix}entities_${etype}`
2617
+ `${this.prefix}entities_${etype}`,
2499
2618
  )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2500
2619
  {
2501
2620
  etypes: [etype],
2502
2621
  params: {
2503
2622
  guid,
2504
2623
  },
2505
- }
2506
- )
2624
+ },
2625
+ ),
2507
2626
  );
2508
2627
  promises.push(
2509
2628
  this.queryRun(
2510
2629
  `SELECT 1 FROM ${PostgreSQLDriver.escape(
2511
- `${this.prefix}data_${etype}`
2630
+ `${this.prefix}data_${etype}`,
2512
2631
  )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2513
2632
  {
2514
2633
  etypes: [etype],
2515
2634
  params: {
2516
2635
  guid,
2517
2636
  },
2518
- }
2519
- )
2637
+ },
2638
+ ),
2520
2639
  );
2521
2640
  promises.push(
2522
2641
  this.queryRun(
2523
2642
  `SELECT 1 FROM ${PostgreSQLDriver.escape(
2524
- `${this.prefix}comparisons_${etype}`
2643
+ `${this.prefix}references_${etype}`,
2525
2644
  )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2526
2645
  {
2527
2646
  etypes: [etype],
2528
2647
  params: {
2529
2648
  guid,
2530
2649
  },
2531
- }
2532
- )
2650
+ },
2651
+ ),
2533
2652
  );
2534
2653
  promises.push(
2535
2654
  this.queryRun(
2536
2655
  `SELECT 1 FROM ${PostgreSQLDriver.escape(
2537
- `${this.prefix}references_${etype}`
2656
+ `${this.prefix}uniques_${etype}`,
2538
2657
  )} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`,
2539
2658
  {
2540
2659
  etypes: [etype],
2541
2660
  params: {
2542
2661
  guid,
2543
2662
  },
2544
- }
2545
- )
2663
+ },
2664
+ ),
2546
2665
  );
2547
2666
  await Promise.all(promises);
2548
2667
  const info = await this.queryRun(
2549
2668
  `UPDATE ${PostgreSQLDriver.escape(
2550
- `${this.prefix}entities_${etype}`
2669
+ `${this.prefix}entities_${etype}`,
2551
2670
  )} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=decode(@guid, 'hex') AND "mdate" <= @emdate;`,
2552
2671
  {
2553
2672
  etypes: [etype],
@@ -2557,7 +2676,7 @@ export default class PostgreSQLDriver extends NymphDriver {
2557
2676
  guid,
2558
2677
  emdate: isNaN(Number(entity.mdate)) ? 0 : Number(entity.mdate),
2559
2678
  },
2560
- }
2679
+ },
2561
2680
  );
2562
2681
  let success = false;
2563
2682
  if (info.rowCount === 1) {
@@ -2565,64 +2684,71 @@ export default class PostgreSQLDriver extends NymphDriver {
2565
2684
  promises.push(
2566
2685
  this.queryRun(
2567
2686
  `DELETE FROM ${PostgreSQLDriver.escape(
2568
- `${this.prefix}data_${etype}`
2687
+ `${this.prefix}data_${etype}`,
2569
2688
  )} WHERE "guid"=decode(@guid, 'hex');`,
2570
2689
  {
2571
2690
  etypes: [etype],
2572
2691
  params: {
2573
2692
  guid,
2574
2693
  },
2575
- }
2576
- )
2694
+ },
2695
+ ),
2577
2696
  );
2578
2697
  promises.push(
2579
2698
  this.queryRun(
2580
2699
  `DELETE FROM ${PostgreSQLDriver.escape(
2581
- `${this.prefix}comparisons_${etype}`
2700
+ `${this.prefix}references_${etype}`,
2582
2701
  )} WHERE "guid"=decode(@guid, 'hex');`,
2583
2702
  {
2584
2703
  etypes: [etype],
2585
2704
  params: {
2586
2705
  guid,
2587
2706
  },
2588
- }
2589
- )
2707
+ },
2708
+ ),
2590
2709
  );
2591
2710
  promises.push(
2592
2711
  this.queryRun(
2593
2712
  `DELETE FROM ${PostgreSQLDriver.escape(
2594
- `${this.prefix}references_${etype}`
2713
+ `${this.prefix}uniques_${etype}`,
2595
2714
  )} WHERE "guid"=decode(@guid, 'hex');`,
2596
2715
  {
2597
2716
  etypes: [etype],
2598
2717
  params: {
2599
2718
  guid,
2600
2719
  },
2601
- }
2602
- )
2720
+ },
2721
+ ),
2603
2722
  );
2604
2723
  await Promise.all(promises);
2605
- await insertData(guid, data, sdata, etype);
2724
+ await insertData(guid, data, sdata, uniques, etype);
2606
2725
  success = true;
2607
2726
  }
2608
2727
  return success;
2609
2728
  },
2610
2729
  async () => {
2611
2730
  await this.internalTransaction('nymph-save');
2731
+ inTransaction = true;
2612
2732
  },
2613
2733
  async (success) => {
2614
- if (success) {
2615
- await this.commit('nymph-save');
2616
- } else {
2617
- await this.rollback('nymph-save');
2734
+ if (inTransaction) {
2735
+ inTransaction = false;
2736
+ if (success) {
2737
+ await this.commit('nymph-save');
2738
+ } else {
2739
+ await this.rollback('nymph-save');
2740
+ }
2618
2741
  }
2619
2742
  return success;
2620
- }
2743
+ },
2621
2744
  );
2622
2745
 
2623
2746
  return result;
2624
2747
  } catch (e: any) {
2625
- await this.rollback('nymph-save');
2748
+ this.nymph.config.debugError('postgresql', `Save entity error: "${e}"`);
2749
+ if (inTransaction) {
2750
+ await this.rollback('nymph-save');
2751
+ }
2626
2752
  throw e;
2627
2753
  }
2628
2754
  }
@@ -2635,38 +2761,39 @@ export default class PostgreSQLDriver extends NymphDriver {
2635
2761
  try {
2636
2762
  await this.queryRun(
2637
2763
  `DELETE FROM ${PostgreSQLDriver.escape(
2638
- `${this.prefix}uids`
2764
+ `${this.prefix}uids`,
2639
2765
  )} WHERE "name"=@name;`,
2640
2766
  {
2641
2767
  params: {
2642
2768
  name,
2643
2769
  curUid,
2644
2770
  },
2645
- }
2771
+ },
2646
2772
  );
2647
2773
  await this.queryRun(
2648
2774
  `INSERT INTO ${PostgreSQLDriver.escape(
2649
- `${this.prefix}uids`
2775
+ `${this.prefix}uids`,
2650
2776
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2651
2777
  {
2652
2778
  params: {
2653
2779
  name,
2654
2780
  curUid,
2655
2781
  },
2656
- }
2782
+ },
2657
2783
  );
2658
- await this.commit('nymph-setuid');
2659
- return true;
2660
2784
  } catch (e: any) {
2661
2785
  await this.rollback('nymph-setuid');
2662
2786
  throw e;
2663
2787
  }
2788
+
2789
+ await this.commit('nymph-setuid');
2790
+ return true;
2664
2791
  }
2665
2792
 
2666
- private async internalTransaction(name: string) {
2793
+ protected async internalTransaction(name: string) {
2667
2794
  if (name == null || typeof name !== 'string' || name.length === 0) {
2668
2795
  throw new InvalidParametersError(
2669
- 'Transaction start attempted without a name.'
2796
+ 'Transaction start attempted without a name.',
2670
2797
  );
2671
2798
  }
2672
2799
 
@@ -2688,7 +2815,7 @@ export default class PostgreSQLDriver extends NymphDriver {
2688
2815
  }
2689
2816
 
2690
2817
  public async startTransaction(name: string) {
2691
- const inTransaction = this.inTransaction();
2818
+ const inTransaction = await this.inTransaction();
2692
2819
  const transaction = await this.internalTransaction(name);
2693
2820
  if (!inTransaction) {
2694
2821
  this.transaction = null;
@@ -2699,4 +2826,29 @@ export default class PostgreSQLDriver extends NymphDriver {
2699
2826
 
2700
2827
  return nymph;
2701
2828
  }
2829
+
2830
+ public async needsMigration(): Promise<boolean> {
2831
+ const table = await this.queryGet(
2832
+ 'SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix LIMIT 1;',
2833
+ {
2834
+ params: {
2835
+ db: this.config.database,
2836
+ prefix: this.prefix + 'data_' + '%',
2837
+ },
2838
+ },
2839
+ );
2840
+ if (table?.table_name) {
2841
+ const result = await this.queryGet(
2842
+ 'SELECT 1 AS "exists" FROM "information_schema"."columns" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name"=@table AND "column_name"=\'json\';',
2843
+ {
2844
+ params: {
2845
+ db: this.config.database,
2846
+ table: table.table_name,
2847
+ },
2848
+ },
2849
+ );
2850
+ return !result?.exists;
2851
+ }
2852
+ return false;
2853
+ }
2702
2854
  }