@nymphjs/driver-sqlite3 1.0.0-beta.7 → 1.0.0-beta.71

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,17 +1,19 @@
1
1
  import SQLite3 from 'better-sqlite3';
2
2
  import {
3
3
  NymphDriver,
4
- EntityConstructor,
5
- EntityData,
6
- EntityInterface,
7
- SerializedEntityData,
4
+ type EntityConstructor,
5
+ type EntityData,
6
+ type EntityInterface,
7
+ type EntityInstanceType,
8
+ type SerializedEntityData,
9
+ type FormattedSelector,
10
+ type Options,
11
+ type Selector,
12
+ EntityUniqueConstraintError,
8
13
  InvalidParametersError,
9
14
  NotConfiguredError,
10
15
  QueryFailedError,
11
16
  UnableToConnectError,
12
- FormattedSelector,
13
- Options,
14
- Selector,
15
17
  xor,
16
18
  } from '@nymphjs/nymph';
17
19
  import { makeTableSuffix } from '@nymphjs/guid';
@@ -23,6 +25,7 @@ import {
23
25
 
24
26
  class InternalStore {
25
27
  public link: SQLite3.Database;
28
+ public linkWrite?: SQLite3.Database;
26
29
  public connected: boolean = false;
27
30
  public transactionsStarted = 0;
28
31
 
@@ -43,7 +46,7 @@ export default class SQLite3Driver extends NymphDriver {
43
46
  static escape(input: string) {
44
47
  if (input.indexOf('\x00') !== -1) {
45
48
  throw new InvalidParametersError(
46
- 'SQLite3 identifiers (like entity ETYPE) cannot contain null characters.'
49
+ 'SQLite3 identifiers (like entity ETYPE) cannot contain null characters.',
47
50
  );
48
51
  }
49
52
 
@@ -53,6 +56,9 @@ export default class SQLite3Driver extends NymphDriver {
53
56
  constructor(config: Partial<SQLite3DriverConfig>, store?: InternalStore) {
54
57
  super();
55
58
  this.config = { ...defaults, ...config };
59
+ if (this.config.filename === ':memory:') {
60
+ this.config.explicitWrite = true;
61
+ }
56
62
  this.prefix = this.config.prefix;
57
63
  if (store) {
58
64
  this.store = store;
@@ -75,53 +81,107 @@ export default class SQLite3Driver extends NymphDriver {
75
81
  *
76
82
  * @returns Whether this instance is connected to a SQLite3 database.
77
83
  */
78
- public async connect() {
79
- const { filename, fileMustExist, timeout, readonly, verbose } = this.config;
80
-
84
+ public connect() {
81
85
  if (this.store && this.store.connected) {
82
- return true;
86
+ return Promise.resolve(true);
83
87
  }
84
88
 
85
89
  // Connecting
90
+ this._connect(false);
91
+
92
+ return Promise.resolve(this.store.connected);
93
+ }
94
+
95
+ private _connect(write: boolean) {
96
+ const { filename, fileMustExist, timeout, explicitWrite, wal, verbose } =
97
+ this.config;
98
+
86
99
  try {
87
- const link = new SQLite3(filename, {
88
- readonly,
89
- fileMustExist,
90
- timeout,
91
- verbose,
92
- });
100
+ const setOptions = (link: SQLite3.Database) => {
101
+ // Set database and connection options.
102
+ if (wal) {
103
+ link.pragma('journal_mode = WAL;');
104
+ }
105
+ link.pragma('encoding = "UTF-8";');
106
+ link.pragma('foreign_keys = 1;');
107
+ link.pragma('case_sensitive_like = 1;');
108
+ for (let pragma of this.config.pragmas) {
109
+ link.pragma(pragma);
110
+ }
111
+ // Create the preg_match and regexp functions.
112
+ link.function('regexp', { deterministic: true }, ((
113
+ pattern: string,
114
+ subject: string,
115
+ ) => (this.posixRegexMatch(pattern, subject) ? 1 : 0)) as (
116
+ ...params: any[]
117
+ ) => any);
118
+ };
119
+
120
+ let link: SQLite3.Database;
121
+ try {
122
+ link = new SQLite3(filename, {
123
+ readonly: !explicitWrite && !write,
124
+ fileMustExist,
125
+ timeout,
126
+ verbose,
127
+ });
128
+ } catch (e: any) {
129
+ if (
130
+ e.code === 'SQLITE_CANTOPEN' &&
131
+ !explicitWrite &&
132
+ !write &&
133
+ !this.config.fileMustExist
134
+ ) {
135
+ // This happens when the file doesn't exist and we attempt to open it
136
+ // readonly.
137
+ // First open it in write mode.
138
+ const writeLink = new SQLite3(filename, {
139
+ readonly: false,
140
+ fileMustExist,
141
+ timeout,
142
+ verbose,
143
+ });
144
+ setOptions(writeLink);
145
+ writeLink.close();
146
+ // Now open in readonly.
147
+ link = new SQLite3(filename, {
148
+ readonly: true,
149
+ fileMustExist,
150
+ timeout,
151
+ verbose,
152
+ });
153
+ } else {
154
+ throw e;
155
+ }
156
+ }
93
157
 
94
158
  if (!this.store) {
159
+ if (write) {
160
+ throw new Error(
161
+ 'Tried to open in write without opening in read first.',
162
+ );
163
+ }
95
164
  this.store = new InternalStore(link);
165
+ } else if (write) {
166
+ this.store.linkWrite = link;
96
167
  } else {
97
168
  this.store.link = link;
98
169
  }
99
170
  this.store.connected = true;
100
- // Set database and connection options.
101
- this.store.link.pragma('encoding = "UTF-8";');
102
- this.store.link.pragma('foreign_keys = 1;');
103
- this.store.link.pragma('case_sensitive_like = 1;');
104
- // Create the preg_match and regexp functions.
105
- this.store.link.function(
106
- 'regexp',
107
- { deterministic: true },
108
- (pattern: string, subject: string) =>
109
- this.posixRegexMatch(pattern, subject) ? 1 : 0
110
- );
171
+ setOptions(link);
111
172
  } catch (e: any) {
112
173
  if (this.store) {
113
174
  this.store.connected = false;
114
175
  }
115
176
  if (filename === ':memory:') {
116
177
  throw new NotConfiguredError(
117
- "It seems the config hasn't been set up correctly."
178
+ "It seems the config hasn't been set up correctly. Could not connect: " +
179
+ e?.message,
118
180
  );
119
181
  } else {
120
182
  throw new UnableToConnectError('Could not connect: ' + e?.message);
121
183
  }
122
184
  }
123
-
124
- return this.store.connected;
125
185
  }
126
186
 
127
187
  /**
@@ -131,8 +191,16 @@ export default class SQLite3Driver extends NymphDriver {
131
191
  */
132
192
  public async disconnect() {
133
193
  if (this.store.connected) {
134
- this.store.link.exec('PRAGMA optimize;');
194
+ if (this.store.linkWrite && !this.config.explicitWrite) {
195
+ this.store.linkWrite.exec('PRAGMA optimize;');
196
+ this.store.linkWrite.close();
197
+ this.store.linkWrite = undefined;
198
+ }
199
+ if (this.config.explicitWrite) {
200
+ this.store.link.exec('PRAGMA optimize;');
201
+ }
135
202
  this.store.link.close();
203
+ this.store.transactionsStarted = 0;
136
204
  this.store.connected = false;
137
205
  }
138
206
  return this.store.connected;
@@ -151,175 +219,200 @@ export default class SQLite3Driver extends NymphDriver {
151
219
  return this.store.connected;
152
220
  }
153
221
 
154
- /**
155
- * Check if SQLite3 DB is read only and throw error if so.
156
- */
157
- private checkReadOnlyMode() {
158
- if (this.config.readonly) {
159
- throw new InvalidParametersError(
160
- 'Attempt to write to SQLite3 DB in read only mode.'
161
- );
162
- }
163
- }
164
-
165
222
  /**
166
223
  * Create entity tables in the database.
167
224
  *
168
225
  * @param etype The entity type to create a table for. If this is blank, the default tables are created.
169
226
  */
170
227
  private createTables(etype: string | null = null) {
171
- this.checkReadOnlyMode();
172
228
  this.startTransaction('nymph-tablecreation');
173
229
  try {
174
230
  if (etype != null) {
175
231
  // Create the entity table.
176
232
  this.queryRun(
177
233
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
178
- `${this.prefix}entities_${etype}`
179
- )} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL);`
234
+ `${this.prefix}entities_${etype}`,
235
+ )} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL);`,
180
236
  );
181
237
  this.queryRun(
182
238
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
183
- `${this.prefix}entities_${etype}_id_cdate`
239
+ `${this.prefix}entities_${etype}_id_cdate`,
184
240
  )} ON ${SQLite3Driver.escape(
185
- `${this.prefix}entities_${etype}`
186
- )} ("cdate");`
241
+ `${this.prefix}entities_${etype}`,
242
+ )} ("cdate");`,
187
243
  );
188
244
  this.queryRun(
189
245
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
190
- `${this.prefix}entities_${etype}_id_mdate`
246
+ `${this.prefix}entities_${etype}_id_mdate`,
191
247
  )} ON ${SQLite3Driver.escape(
192
- `${this.prefix}entities_${etype}`
193
- )} ("mdate");`
248
+ `${this.prefix}entities_${etype}`,
249
+ )} ("mdate");`,
194
250
  );
195
251
  this.queryRun(
196
252
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
197
- `${this.prefix}entities_${etype}_id_tags`
253
+ `${this.prefix}entities_${etype}_id_tags`,
198
254
  )} ON ${SQLite3Driver.escape(
199
- `${this.prefix}entities_${etype}`
200
- )} ("tags");`
255
+ `${this.prefix}entities_${etype}`,
256
+ )} ("tags");`,
201
257
  );
202
258
  // Create the data table.
203
259
  this.queryRun(
204
260
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
205
- `${this.prefix}data_${etype}`
261
+ `${this.prefix}data_${etype}`,
206
262
  )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
207
- `${this.prefix}entities_${etype}`
208
- )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY("guid", "name"));`
263
+ `${this.prefix}entities_${etype}`,
264
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY("guid", "name"));`,
209
265
  );
210
266
  this.queryRun(
211
267
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
212
- `${this.prefix}data_${etype}_id_guid`
268
+ `${this.prefix}data_${etype}_id_guid`,
213
269
  )} ON ${SQLite3Driver.escape(
214
- `${this.prefix}data_${etype}`
215
- )} ("guid");`
270
+ `${this.prefix}data_${etype}`,
271
+ )} ("guid");`,
216
272
  );
217
273
  this.queryRun(
218
274
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
219
- `${this.prefix}data_${etype}_id_name`
275
+ `${this.prefix}data_${etype}_id_name`,
220
276
  )} ON ${SQLite3Driver.escape(
221
- `${this.prefix}data_${etype}`
222
- )} ("name");`
277
+ `${this.prefix}data_${etype}`,
278
+ )} ("name");`,
223
279
  );
224
280
  this.queryRun(
225
281
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
226
- `${this.prefix}data_${etype}_id_value`
282
+ `${this.prefix}data_${etype}_id_name_value`,
227
283
  )} ON ${SQLite3Driver.escape(
228
- `${this.prefix}data_${etype}`
229
- )} ("value");`
284
+ `${this.prefix}data_${etype}`,
285
+ )} ("name", "value");`,
230
286
  );
231
287
  this.queryRun(
232
288
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
233
- `${this.prefix}data_${etype}_id_guid__name_user`
289
+ `${this.prefix}data_${etype}_id_value`,
234
290
  )} ON ${SQLite3Driver.escape(
235
- `${this.prefix}data_${etype}`
236
- )} ("guid") WHERE "name" = \'user\';`
291
+ `${this.prefix}data_${etype}`,
292
+ )} ("value");`,
237
293
  );
238
294
  this.queryRun(
239
295
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
240
- `${this.prefix}data_${etype}_id_guid__name_group`
296
+ `${this.prefix}data_${etype}_id_guid__name_user`,
241
297
  )} ON ${SQLite3Driver.escape(
242
- `${this.prefix}data_${etype}`
243
- )} ("guid") WHERE "name" = \'group\';`
298
+ `${this.prefix}data_${etype}`,
299
+ )} ("guid") WHERE "name" = \'user\';`,
300
+ );
301
+ this.queryRun(
302
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
303
+ `${this.prefix}data_${etype}_id_guid__name_group`,
304
+ )} ON ${SQLite3Driver.escape(
305
+ `${this.prefix}data_${etype}`,
306
+ )} ("guid") WHERE "name" = \'group\';`,
244
307
  );
245
308
  // Create the comparisons table.
246
309
  this.queryRun(
247
310
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
248
- `${this.prefix}comparisons_${etype}`
311
+ `${this.prefix}comparisons_${etype}`,
249
312
  )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
250
- `${this.prefix}entities_${etype}`
251
- )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "truthy" INTEGER, "string" TEXT, "number" REAL, PRIMARY KEY("guid", "name"));`
313
+ `${this.prefix}entities_${etype}`,
314
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "truthy" INTEGER, "string" TEXT, "number" REAL, PRIMARY KEY("guid", "name"));`,
315
+ );
316
+ this.queryRun(
317
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
318
+ `${this.prefix}comparisons_${etype}_id_guid`,
319
+ )} ON ${SQLite3Driver.escape(
320
+ `${this.prefix}comparisons_${etype}`,
321
+ )} ("guid");`,
322
+ );
323
+ this.queryRun(
324
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
325
+ `${this.prefix}comparisons_${etype}_id_name`,
326
+ )} ON ${SQLite3Driver.escape(
327
+ `${this.prefix}comparisons_${etype}`,
328
+ )} ("name");`,
252
329
  );
253
330
  this.queryRun(
254
331
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
255
- `${this.prefix}comparisons_${etype}_id_guid`
332
+ `${this.prefix}comparisons_${etype}_id_name__truthy`,
256
333
  )} ON ${SQLite3Driver.escape(
257
- `${this.prefix}comparisons_${etype}`
258
- )} ("guid");`
334
+ `${this.prefix}comparisons_${etype}`,
335
+ )} ("name") WHERE "truthy" = 1;`,
259
336
  );
260
337
  this.queryRun(
261
338
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
262
- `${this.prefix}comparisons_${etype}_id_name`
339
+ `${this.prefix}comparisons_${etype}_id_name__falsy`,
263
340
  )} ON ${SQLite3Driver.escape(
264
- `${this.prefix}comparisons_${etype}`
265
- )} ("name");`
341
+ `${this.prefix}comparisons_${etype}`,
342
+ )} ("name") WHERE "truthy" <> 1;`,
266
343
  );
267
344
  this.queryRun(
268
345
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
269
- `${this.prefix}comparisons_${etype}_id_name__truthy`
346
+ `${this.prefix}comparisons_${etype}_id_name_string`,
270
347
  )} ON ${SQLite3Driver.escape(
271
- `${this.prefix}comparisons_${etype}`
272
- )} ("name") WHERE "truthy" = 1;`
348
+ `${this.prefix}comparisons_${etype}`,
349
+ )} ("name", "string");`,
350
+ );
351
+ this.queryRun(
352
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
353
+ `${this.prefix}comparisons_${etype}_id_name_number`,
354
+ )} ON ${SQLite3Driver.escape(
355
+ `${this.prefix}comparisons_${etype}`,
356
+ )} ("name", "number");`,
273
357
  );
274
358
  // Create the references table.
275
359
  this.queryRun(
276
360
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
277
- `${this.prefix}references_${etype}`
361
+ `${this.prefix}references_${etype}`,
278
362
  )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
279
- `${this.prefix}entities_${etype}`
280
- )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`
363
+ `${this.prefix}entities_${etype}`,
364
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`,
281
365
  );
282
366
  this.queryRun(
283
367
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
284
- `${this.prefix}references_${etype}_id_guid`
368
+ `${this.prefix}references_${etype}_id_guid`,
285
369
  )} ON ${SQLite3Driver.escape(
286
- `${this.prefix}references_${etype}`
287
- )} ("guid");`
370
+ `${this.prefix}references_${etype}`,
371
+ )} ("guid");`,
288
372
  );
289
373
  this.queryRun(
290
374
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
291
- `${this.prefix}references_${etype}_id_name`
375
+ `${this.prefix}references_${etype}_id_name`,
292
376
  )} ON ${SQLite3Driver.escape(
293
- `${this.prefix}references_${etype}`
294
- )} ("name");`
377
+ `${this.prefix}references_${etype}`,
378
+ )} ("name");`,
295
379
  );
296
380
  this.queryRun(
297
381
  `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
298
- `${this.prefix}references_${etype}_id_reference`
382
+ `${this.prefix}references_${etype}_id_name_reference`,
299
383
  )} ON ${SQLite3Driver.escape(
300
- `${this.prefix}references_${etype}`
301
- )} ("reference");`
384
+ `${this.prefix}references_${etype}`,
385
+ )} ("name", "reference");`,
386
+ );
387
+ // Create the unique strings table.
388
+ this.queryRun(
389
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
390
+ `${this.prefix}uniques_${etype}`,
391
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
392
+ `${this.prefix}entities_${etype}`,
393
+ )} ("guid") ON DELETE CASCADE, "unique" TEXT NOT NULL UNIQUE, PRIMARY KEY("guid", "unique"));`,
302
394
  );
303
395
  } else {
304
396
  // Create the UID table.
305
397
  this.queryRun(
306
398
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
307
- `${this.prefix}uids`
308
- )} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`
399
+ `${this.prefix}uids`,
400
+ )} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`,
309
401
  );
310
402
  }
311
- this.commit('nymph-tablecreation');
312
- return true;
313
403
  } catch (e: any) {
314
404
  this.rollback('nymph-tablecreation');
315
405
  throw e;
316
406
  }
407
+
408
+ this.commit('nymph-tablecreation');
409
+ return true;
317
410
  }
318
411
 
319
412
  private query<T extends () => any>(
320
413
  runQuery: T,
321
414
  query: string,
322
- etypes: string[] = []
415
+ etypes: string[] = [],
323
416
  ): ReturnType<T> {
324
417
  try {
325
418
  return runQuery();
@@ -339,13 +432,18 @@ export default class SQLite3Driver extends NymphDriver {
339
432
  } catch (e2: any) {
340
433
  throw new QueryFailedError(
341
434
  'Query failed: ' + e2?.code + ' - ' + e2?.message,
342
- query
435
+ query,
343
436
  );
344
437
  }
438
+ } else if (
439
+ errorCode === 'SQLITE_CONSTRAINT_UNIQUE' &&
440
+ errorMsg.match(/^UNIQUE constraint failed: /)
441
+ ) {
442
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
345
443
  } else {
346
444
  throw new QueryFailedError(
347
445
  'Query failed: ' + e?.code + ' - ' + e?.message,
348
- query
446
+ query,
349
447
  );
350
448
  }
351
449
  }
@@ -356,12 +454,15 @@ export default class SQLite3Driver extends NymphDriver {
356
454
  {
357
455
  etypes = [],
358
456
  params = {},
359
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
457
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
360
458
  ) {
361
459
  return this.query(
362
- () => this.store.link.prepare(query).iterate(params),
460
+ () =>
461
+ (this.store.linkWrite || this.store.link)
462
+ .prepare(query)
463
+ .iterate(params),
363
464
  `${query} -- ${JSON.stringify(params)}`,
364
- etypes
465
+ etypes,
365
466
  );
366
467
  }
367
468
 
@@ -370,12 +471,13 @@ export default class SQLite3Driver extends NymphDriver {
370
471
  {
371
472
  etypes = [],
372
473
  params = {},
373
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
474
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
374
475
  ) {
375
476
  return this.query(
376
- () => this.store.link.prepare(query).get(params),
477
+ () =>
478
+ (this.store.linkWrite || this.store.link).prepare(query).get(params),
377
479
  `${query} -- ${JSON.stringify(params)}`,
378
- etypes
480
+ etypes,
379
481
  );
380
482
  }
381
483
 
@@ -384,19 +486,20 @@ export default class SQLite3Driver extends NymphDriver {
384
486
  {
385
487
  etypes = [],
386
488
  params = {},
387
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
489
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
388
490
  ) {
389
491
  return this.query(
390
- () => this.store.link.prepare(query).run(params),
492
+ () =>
493
+ (this.store.linkWrite || this.store.link).prepare(query).run(params),
391
494
  `${query} -- ${JSON.stringify(params)}`,
392
- etypes
495
+ etypes,
393
496
  );
394
497
  }
395
498
 
396
499
  public async commit(name: string) {
397
500
  if (name == null || typeof name !== 'string' || name.length === 0) {
398
501
  throw new InvalidParametersError(
399
- 'Transaction commit attempted without a name.'
502
+ 'Transaction commit attempted without a name.',
400
503
  );
401
504
  }
402
505
  if (this.store.transactionsStarted === 0) {
@@ -404,12 +507,23 @@ export default class SQLite3Driver extends NymphDriver {
404
507
  }
405
508
  this.queryRun(`RELEASE SAVEPOINT ${SQLite3Driver.escape(name)};`);
406
509
  this.store.transactionsStarted--;
510
+
511
+ if (
512
+ this.store.transactionsStarted === 0 &&
513
+ this.store.linkWrite &&
514
+ !this.config.explicitWrite
515
+ ) {
516
+ this.store.linkWrite.exec('PRAGMA optimize;');
517
+ this.store.linkWrite.close();
518
+ this.store.linkWrite = undefined;
519
+ }
520
+
407
521
  return true;
408
522
  }
409
523
 
410
524
  public async deleteEntityByID(
411
525
  guid: string,
412
- className?: EntityConstructor | string | null
526
+ className?: EntityConstructor | string | null,
413
527
  ) {
414
528
  let EntityClass: EntityConstructor;
415
529
  if (typeof className === 'string' || className == null) {
@@ -419,115 +533,160 @@ export default class SQLite3Driver extends NymphDriver {
419
533
  EntityClass = className;
420
534
  }
421
535
  const etype = EntityClass.ETYPE;
422
- this.checkReadOnlyMode();
423
536
  await this.startTransaction('nymph-delete');
424
537
  try {
425
538
  this.queryRun(
426
539
  `DELETE FROM ${SQLite3Driver.escape(
427
- `${this.prefix}entities_${etype}`
540
+ `${this.prefix}entities_${etype}`,
428
541
  )} WHERE "guid"=@guid;`,
429
542
  {
430
543
  etypes: [etype],
431
544
  params: {
432
545
  guid,
433
546
  },
434
- }
547
+ },
435
548
  );
436
549
  this.queryRun(
437
550
  `DELETE FROM ${SQLite3Driver.escape(
438
- `${this.prefix}data_${etype}`
551
+ `${this.prefix}data_${etype}`,
439
552
  )} WHERE "guid"=@guid;`,
440
553
  {
441
554
  etypes: [etype],
442
555
  params: {
443
556
  guid,
444
557
  },
445
- }
558
+ },
446
559
  );
447
560
  this.queryRun(
448
561
  `DELETE FROM ${SQLite3Driver.escape(
449
- `${this.prefix}comparisons_${etype}`
562
+ `${this.prefix}comparisons_${etype}`,
450
563
  )} WHERE "guid"=@guid;`,
451
564
  {
452
565
  etypes: [etype],
453
566
  params: {
454
567
  guid,
455
568
  },
456
- }
569
+ },
457
570
  );
458
571
  this.queryRun(
459
572
  `DELETE FROM ${SQLite3Driver.escape(
460
- `${this.prefix}references_${etype}`
573
+ `${this.prefix}references_${etype}`,
461
574
  )} WHERE "guid"=@guid;`,
462
575
  {
463
576
  etypes: [etype],
464
577
  params: {
465
578
  guid,
466
579
  },
467
- }
580
+ },
581
+ );
582
+ this.queryRun(
583
+ `DELETE FROM ${SQLite3Driver.escape(
584
+ `${this.prefix}uniques_${etype}`,
585
+ )} WHERE "guid"=@guid;`,
586
+ {
587
+ etypes: [etype],
588
+ params: {
589
+ guid,
590
+ },
591
+ },
468
592
  );
469
- await this.commit('nymph-delete');
470
- // Remove any cached versions of this entity.
471
- if (this.nymph.config.cache) {
472
- this.cleanCache(guid);
473
- }
474
- return true;
475
593
  } catch (e: any) {
594
+ this.nymph.config.debugError('sqlite3', `Delete entity error: "${e}"`);
476
595
  await this.rollback('nymph-delete');
477
596
  throw e;
478
597
  }
598
+
599
+ await this.commit('nymph-delete');
600
+ // Remove any cached versions of this entity.
601
+ if (this.nymph.config.cache) {
602
+ this.cleanCache(guid);
603
+ }
604
+ return true;
479
605
  }
480
606
 
481
607
  public async deleteUID(name: string) {
482
608
  if (!name) {
483
609
  throw new InvalidParametersError('Name not given for UID');
484
610
  }
485
- this.checkReadOnlyMode();
611
+ await this.startTransaction('nymph-delete-uid');
486
612
  this.queryRun(
487
613
  `DELETE FROM ${SQLite3Driver.escape(
488
- `${this.prefix}uids`
614
+ `${this.prefix}uids`,
489
615
  )} WHERE "name"=@name;`,
490
616
  {
491
617
  params: {
492
618
  name,
493
619
  },
494
- }
620
+ },
495
621
  );
622
+ await this.commit('nymph-delete-uid');
496
623
  return true;
497
624
  }
498
625
 
499
- protected async exportEntities(writeLine: (line: string) => void) {
500
- writeLine('#nex2');
501
- writeLine('# Nymph Entity Exchange v2');
502
- writeLine('# http://nymph.io');
503
- writeLine('#');
504
- writeLine('# Generation Time: ' + new Date().toLocaleString());
505
- writeLine('');
626
+ public async *exportDataIterator(): AsyncGenerator<
627
+ { type: 'comment' | 'uid' | 'entity'; content: string },
628
+ void,
629
+ false | undefined
630
+ > {
631
+ if (
632
+ yield {
633
+ type: 'comment',
634
+ content: `#nex2
635
+ # Nymph Entity Exchange v2
636
+ # http://nymph.io
637
+ #
638
+ # Generation Time: ${new Date().toLocaleString()}
639
+ `,
640
+ }
641
+ ) {
642
+ return;
643
+ }
644
+
645
+ if (
646
+ yield {
647
+ type: 'comment',
648
+ content: `
506
649
 
507
- writeLine('#');
508
- writeLine('# UIDs');
509
- writeLine('#');
510
- writeLine('');
650
+ #
651
+ # UIDs
652
+ #
653
+
654
+ `,
655
+ }
656
+ ) {
657
+ return;
658
+ }
511
659
 
512
660
  // Export UIDs.
513
- let uids = this.queryIter(
661
+ let uids: IterableIterator<any> = this.queryIter(
514
662
  `SELECT * FROM ${SQLite3Driver.escape(
515
- `${this.prefix}uids`
516
- )} ORDER BY "name";`
663
+ `${this.prefix}uids`,
664
+ )} ORDER BY "name";`,
517
665
  );
518
666
  for (const uid of uids) {
519
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
667
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
668
+ return;
669
+ }
520
670
  }
521
671
 
522
- writeLine('');
523
- writeLine('#');
524
- writeLine('# Entities');
525
- writeLine('#');
526
- writeLine('');
672
+ if (
673
+ yield {
674
+ type: 'comment',
675
+ content: `
676
+
677
+ #
678
+ # Entities
679
+ #
680
+
681
+ `,
682
+ }
683
+ ) {
684
+ return;
685
+ }
527
686
 
528
687
  // Get the etypes.
529
- const tables = this.queryIter(
530
- "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;"
688
+ const tables: IterableIterator<any> = this.queryIter(
689
+ "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;",
531
690
  );
532
691
  const etypes = [];
533
692
  for (const table of tables) {
@@ -538,14 +697,14 @@ export default class SQLite3Driver extends NymphDriver {
538
697
 
539
698
  for (const etype of etypes) {
540
699
  // Export entities.
541
- const dataIterator = this.queryIter(
700
+ const dataIterator: IterableIterator<any> = this.queryIter(
542
701
  `SELECT e.*, d."name" AS "dname", d."value" AS "dvalue", c."string", c."number" FROM ${SQLite3Driver.escape(
543
- `${this.prefix}entities_${etype}`
702
+ `${this.prefix}entities_${etype}`,
544
703
  )} e LEFT JOIN ${SQLite3Driver.escape(
545
- `${this.prefix}data_${etype}`
704
+ `${this.prefix}data_${etype}`,
546
705
  )} d USING ("guid") INNER JOIN ${SQLite3Driver.escape(
547
- `${this.prefix}comparisons_${etype}`
548
- )} c USING ("guid", "name") ORDER BY e."guid";`
706
+ `${this.prefix}comparisons_${etype}`,
707
+ )} c USING ("guid", "name") ORDER BY e."guid";`,
549
708
  )[Symbol.iterator]();
550
709
  let datum = dataIterator.next();
551
710
  while (!datum.done) {
@@ -553,9 +712,10 @@ export default class SQLite3Driver extends NymphDriver {
553
712
  const tags = datum.value.tags.slice(1, -1);
554
713
  const cdate = datum.value.cdate;
555
714
  const mdate = datum.value.mdate;
556
- writeLine(`{${guid}}<${etype}>[${tags}]`);
557
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
558
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
715
+ let currentEntityExport: string[] = [];
716
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
717
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
718
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
559
719
  if (datum.value.dname != null) {
560
720
  // This do will keep going and adding the data until the
561
721
  // next entity is reached. datum will end on the next entity.
@@ -566,17 +726,20 @@ export default class SQLite3Driver extends NymphDriver {
566
726
  : datum.value.dvalue === 'S'
567
727
  ? JSON.stringify(datum.value.string)
568
728
  : datum.value.dvalue;
569
- writeLine(`\t${datum.value.dname}=${value}`);
729
+ currentEntityExport.push(`\t${datum.value.dname}=${value}`);
570
730
  datum = dataIterator.next();
571
731
  } while (!datum.done && datum.value.guid === guid);
572
732
  } else {
573
733
  // Make sure that datum is incremented :)
574
734
  datum = dataIterator.next();
575
735
  }
736
+ currentEntityExport.push('');
737
+
738
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
739
+ return;
740
+ }
576
741
  }
577
742
  }
578
-
579
- return;
580
743
  }
581
744
 
582
745
  /**
@@ -597,7 +760,7 @@ export default class SQLite3Driver extends NymphDriver {
597
760
  params: { [k: string]: any } = {},
598
761
  subquery = false,
599
762
  tableSuffix = '',
600
- etypes: string[] = []
763
+ etypes: string[] = [],
601
764
  ) {
602
765
  if (typeof options.class?.alterOptions === 'function') {
603
766
  options = options.class.alterOptions(options);
@@ -607,10 +770,11 @@ export default class SQLite3Driver extends NymphDriver {
607
770
  const cTable = `c${tableSuffix}`;
608
771
  const fTable = `f${tableSuffix}`;
609
772
  const ieTable = `ie${tableSuffix}`;
773
+ const sTable = `s${tableSuffix}`;
610
774
  const sort = options.sort ?? 'cdate';
611
775
  const queryParts = this.iterateSelectorsForQuery(
612
776
  formattedSelectors,
613
- (key, value, typeIsOr, typeIsNot) => {
777
+ ({ key, value, typeIsOr, typeIsNot }) => {
614
778
  const clauseNot = key.startsWith('!');
615
779
  let curQuery = '';
616
780
  for (const curValue of value) {
@@ -695,10 +859,11 @@ export default class SQLite3Driver extends NymphDriver {
695
859
  const name = `param${++count.i}`;
696
860
  curQuery +=
697
861
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
698
- ieTable +
699
- '."guid" IN (SELECT "guid" FROM ' +
862
+ 'EXISTS (SELECT "guid" FROM ' +
700
863
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
701
- ' WHERE "name"=@' +
864
+ ' WHERE "guid"=' +
865
+ ieTable +
866
+ '."guid" AND "name"=@' +
702
867
  name +
703
868
  ' AND "truthy"=1)';
704
869
  params[name] = curVar;
@@ -739,10 +904,11 @@ export default class SQLite3Driver extends NymphDriver {
739
904
  const value = `param${++count.i}`;
740
905
  curQuery +=
741
906
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
742
- ieTable +
743
- '."guid" IN (SELECT "guid" FROM ' +
907
+ 'EXISTS (SELECT "guid" FROM ' +
744
908
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
745
- ' WHERE "name"=@' +
909
+ ' WHERE "guid"=' +
910
+ ieTable +
911
+ '."guid" AND "name"=@' +
746
912
  name +
747
913
  ' AND "number"=@' +
748
914
  value +
@@ -757,10 +923,11 @@ export default class SQLite3Driver extends NymphDriver {
757
923
  const value = `param${++count.i}`;
758
924
  curQuery +=
759
925
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
760
- ieTable +
761
- '."guid" IN (SELECT "guid" FROM ' +
926
+ 'EXISTS (SELECT "guid" FROM ' +
762
927
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
763
- ' WHERE "name"=@' +
928
+ ' WHERE "guid"=' +
929
+ ieTable +
930
+ '."guid" AND "name"=@' +
764
931
  name +
765
932
  ' AND "string"=@' +
766
933
  value +
@@ -784,10 +951,11 @@ export default class SQLite3Driver extends NymphDriver {
784
951
  const value = `param${++count.i}`;
785
952
  curQuery +=
786
953
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
787
- ieTable +
788
- '."guid" IN (SELECT "guid" FROM ' +
954
+ 'EXISTS (SELECT "guid" FROM ' +
789
955
  SQLite3Driver.escape(this.prefix + 'data_' + etype) +
790
- ' WHERE "name"=@' +
956
+ ' WHERE "guid"=' +
957
+ ieTable +
958
+ '."guid" AND "name"=@' +
791
959
  name +
792
960
  ' AND "value"=@' +
793
961
  value +
@@ -844,19 +1012,19 @@ export default class SQLite3Driver extends NymphDriver {
844
1012
  const stringParam = `param${++count.i}`;
845
1013
  curQuery +=
846
1014
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
847
- '(' +
848
- ieTable +
849
- '."guid" IN (SELECT "guid" FROM ' +
1015
+ '(EXISTS (SELECT "guid" FROM ' +
850
1016
  SQLite3Driver.escape(this.prefix + 'data_' + etype) +
851
- ' WHERE "name"=@' +
1017
+ ' WHERE "guid"=' +
1018
+ ieTable +
1019
+ '."guid" AND "name"=@' +
852
1020
  name +
853
1021
  ' AND instr("value", @' +
854
1022
  value +
855
- ')) OR ' +
856
- ieTable +
857
- '."guid" IN (SELECT "guid" FROM ' +
1023
+ ')) OR EXISTS (SELECT "guid" FROM ' +
858
1024
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
859
- ' WHERE "name"=@' +
1025
+ ' WHERE "guid"=' +
1026
+ ieTable +
1027
+ '."guid" AND "name"=@' +
860
1028
  name +
861
1029
  ' AND "string"=@' +
862
1030
  stringParam +
@@ -865,10 +1033,11 @@ export default class SQLite3Driver extends NymphDriver {
865
1033
  } else {
866
1034
  curQuery +=
867
1035
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
868
- ieTable +
869
- '."guid" IN (SELECT "guid" FROM ' +
1036
+ 'EXISTS (SELECT "guid" FROM ' +
870
1037
  SQLite3Driver.escape(this.prefix + 'data_' + etype) +
871
- ' WHERE "name"=@' +
1038
+ ' WHERE "guid"=' +
1039
+ ieTable +
1040
+ '."guid" AND "name"=@' +
872
1041
  name +
873
1042
  ' AND instr("value", @' +
874
1043
  value +
@@ -916,10 +1085,11 @@ export default class SQLite3Driver extends NymphDriver {
916
1085
  const value = `param${++count.i}`;
917
1086
  curQuery +=
918
1087
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
919
- ieTable +
920
- '."guid" IN (SELECT "guid" FROM ' +
1088
+ 'EXISTS (SELECT "guid" FROM ' +
921
1089
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
922
- ' WHERE "name"=@' +
1090
+ ' WHERE "guid"=' +
1091
+ ieTable +
1092
+ '."guid" AND "name"=@' +
923
1093
  name +
924
1094
  ' AND "string" REGEXP @' +
925
1095
  value +
@@ -966,10 +1136,11 @@ export default class SQLite3Driver extends NymphDriver {
966
1136
  const value = `param${++count.i}`;
967
1137
  curQuery +=
968
1138
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
969
- ieTable +
970
- '."guid" IN (SELECT "guid" FROM ' +
1139
+ 'EXISTS (SELECT "guid" FROM ' +
971
1140
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
972
- ' WHERE "name"=@' +
1141
+ ' WHERE "guid"=' +
1142
+ ieTable +
1143
+ '."guid" AND "name"=@' +
973
1144
  name +
974
1145
  ' AND lower("string") REGEXP lower(@' +
975
1146
  value +
@@ -1016,10 +1187,11 @@ export default class SQLite3Driver extends NymphDriver {
1016
1187
  const value = `param${++count.i}`;
1017
1188
  curQuery +=
1018
1189
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1019
- ieTable +
1020
- '."guid" IN (SELECT "guid" FROM ' +
1190
+ 'EXISTS (SELECT "guid" FROM ' +
1021
1191
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1022
- ' WHERE "name"=@' +
1192
+ ' WHERE "guid"=' +
1193
+ ieTable +
1194
+ '."guid" AND "name"=@' +
1023
1195
  name +
1024
1196
  ' AND "string" LIKE @' +
1025
1197
  value +
@@ -1066,10 +1238,11 @@ export default class SQLite3Driver extends NymphDriver {
1066
1238
  const value = `param${++count.i}`;
1067
1239
  curQuery +=
1068
1240
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1069
- ieTable +
1070
- '."guid" IN (SELECT "guid" FROM ' +
1241
+ 'EXISTS (SELECT "guid" FROM ' +
1071
1242
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1072
- ' WHERE "name"=@' +
1243
+ ' WHERE "guid"=' +
1244
+ ieTable +
1245
+ '."guid" AND "name"=@' +
1073
1246
  name +
1074
1247
  ' AND lower("string") LIKE lower(@' +
1075
1248
  value +
@@ -1112,10 +1285,11 @@ export default class SQLite3Driver extends NymphDriver {
1112
1285
  const value = `param${++count.i}`;
1113
1286
  curQuery +=
1114
1287
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1115
- ieTable +
1116
- '."guid" IN (SELECT "guid" FROM ' +
1288
+ 'EXISTS (SELECT "guid" FROM ' +
1117
1289
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1118
- ' WHERE "name"=@' +
1290
+ ' WHERE "guid"=' +
1291
+ ieTable +
1292
+ '."guid" AND "name"=@' +
1119
1293
  name +
1120
1294
  ' AND "number">@' +
1121
1295
  value +
@@ -1158,10 +1332,11 @@ export default class SQLite3Driver extends NymphDriver {
1158
1332
  const value = `param${++count.i}`;
1159
1333
  curQuery +=
1160
1334
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1161
- ieTable +
1162
- '."guid" IN (SELECT "guid" FROM ' +
1335
+ 'EXISTS (SELECT "guid" FROM ' +
1163
1336
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1164
- ' WHERE "name"=@' +
1337
+ ' WHERE "guid"=' +
1338
+ ieTable +
1339
+ '."guid" AND "name"=@' +
1165
1340
  name +
1166
1341
  ' AND "number">=@' +
1167
1342
  value +
@@ -1204,10 +1379,11 @@ export default class SQLite3Driver extends NymphDriver {
1204
1379
  const value = `param${++count.i}`;
1205
1380
  curQuery +=
1206
1381
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1207
- ieTable +
1208
- '."guid" IN (SELECT "guid" FROM ' +
1382
+ 'EXISTS (SELECT "guid" FROM ' +
1209
1383
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1210
- ' WHERE "name"=@' +
1384
+ ' WHERE "guid"=' +
1385
+ ieTable +
1386
+ '."guid" AND "name"=@' +
1211
1387
  name +
1212
1388
  ' AND "number"<@' +
1213
1389
  value +
@@ -1250,10 +1426,11 @@ export default class SQLite3Driver extends NymphDriver {
1250
1426
  const value = `param${++count.i}`;
1251
1427
  curQuery +=
1252
1428
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1253
- ieTable +
1254
- '."guid" IN (SELECT "guid" FROM ' +
1429
+ 'EXISTS (SELECT "guid" FROM ' +
1255
1430
  SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1256
- ' WHERE "name"=@' +
1431
+ ' WHERE "guid"=' +
1432
+ ieTable +
1433
+ '."guid" AND "name"=@' +
1257
1434
  name +
1258
1435
  ' AND "number"<=@' +
1259
1436
  value +
@@ -1279,10 +1456,11 @@ export default class SQLite3Driver extends NymphDriver {
1279
1456
  const guid = `param${++count.i}`;
1280
1457
  curQuery +=
1281
1458
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1282
- ieTable +
1283
- '."guid" IN (SELECT "guid" FROM ' +
1459
+ 'EXISTS (SELECT "guid" FROM ' +
1284
1460
  SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1285
- ' WHERE "name"=@' +
1461
+ ' WHERE "guid"=' +
1462
+ ieTable +
1463
+ '."guid" AND "name"=@' +
1286
1464
  name +
1287
1465
  ' AND "reference"=@' +
1288
1466
  guid +
@@ -1300,7 +1478,7 @@ export default class SQLite3Driver extends NymphDriver {
1300
1478
  params,
1301
1479
  true,
1302
1480
  tableSuffix,
1303
- etypes
1481
+ etypes,
1304
1482
  );
1305
1483
  if (curQuery) {
1306
1484
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1315,7 +1493,7 @@ export default class SQLite3Driver extends NymphDriver {
1315
1493
  case '!qref':
1316
1494
  const [qrefOptions, ...qrefSelectors] = curValue[1] as [
1317
1495
  Options,
1318
- ...FormattedSelector[]
1496
+ ...FormattedSelector[],
1319
1497
  ];
1320
1498
  const QrefEntityClass = qrefOptions.class as EntityConstructor;
1321
1499
  etypes.push(QrefEntityClass.ETYPE);
@@ -1327,7 +1505,7 @@ export default class SQLite3Driver extends NymphDriver {
1327
1505
  params,
1328
1506
  false,
1329
1507
  makeTableSuffix(),
1330
- etypes
1508
+ etypes,
1331
1509
  );
1332
1510
  if (curQuery) {
1333
1511
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1335,10 +1513,11 @@ export default class SQLite3Driver extends NymphDriver {
1335
1513
  const qrefName = `param${++count.i}`;
1336
1514
  curQuery +=
1337
1515
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1338
- ieTable +
1339
- '."guid" IN (SELECT "guid" FROM ' +
1516
+ 'EXISTS (SELECT "guid" FROM ' +
1340
1517
  SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1341
- ' WHERE "name"=@' +
1518
+ ' WHERE "guid"=' +
1519
+ ieTable +
1520
+ '."guid" AND "name"=@' +
1342
1521
  qrefName +
1343
1522
  ' AND "reference" IN (' +
1344
1523
  qrefQuery.query +
@@ -1348,22 +1527,35 @@ export default class SQLite3Driver extends NymphDriver {
1348
1527
  }
1349
1528
  }
1350
1529
  return curQuery;
1351
- }
1530
+ },
1352
1531
  );
1353
1532
 
1354
1533
  let sortBy: string;
1534
+ let sortByInner: string;
1535
+ let sortJoin = '';
1536
+ const order = options.reverse ? ' DESC' : '';
1355
1537
  switch (sort) {
1356
1538
  case 'mdate':
1357
- sortBy = '"mdate"';
1539
+ sortBy = `${eTable}."mdate"${order}`;
1540
+ sortByInner = `${ieTable}."mdate"${order}`;
1358
1541
  break;
1359
1542
  case 'cdate':
1543
+ sortBy = `${eTable}."cdate"${order}`;
1544
+ sortByInner = `${ieTable}."cdate"${order}`;
1545
+ break;
1360
1546
  default:
1361
- sortBy = '"cdate"';
1547
+ const name = `param${++count.i}`;
1548
+ sortJoin = `LEFT JOIN (
1549
+ SELECT "guid", "string", "number"
1550
+ FROM ${SQLite3Driver.escape(this.prefix + 'comparisons_' + etype)}
1551
+ WHERE "name"=@${name}
1552
+ ORDER BY "number"${order}, "string"${order}
1553
+ ) ${sTable} USING ("guid")`;
1554
+ sortBy = `${sTable}."number"${order}, ${sTable}."string"${order}`;
1555
+ sortByInner = sortBy;
1556
+ params[name] = sort;
1362
1557
  break;
1363
1558
  }
1364
- if (options.reverse) {
1365
- sortBy += ' DESC';
1366
- }
1367
1559
 
1368
1560
  let query: string;
1369
1561
  if (queryParts.length) {
@@ -1384,24 +1576,25 @@ export default class SQLite3Driver extends NymphDriver {
1384
1576
  query = `SELECT COUNT("guid") AS "count" FROM (
1385
1577
  SELECT "guid"
1386
1578
  FROM ${SQLite3Driver.escape(
1387
- this.prefix + 'entities_' + etype
1579
+ this.prefix + 'entities_' + etype,
1388
1580
  )} ${ieTable}
1389
1581
  WHERE (${whereClause})${limit}${offset}
1390
1582
  )`;
1391
1583
  } else {
1392
1584
  query = `SELECT COUNT("guid") AS "count"
1393
1585
  FROM ${SQLite3Driver.escape(
1394
- this.prefix + 'entities_' + etype
1586
+ this.prefix + 'entities_' + etype,
1395
1587
  )} ${ieTable}
1396
1588
  WHERE (${whereClause})`;
1397
1589
  }
1398
1590
  } else if (options.return === 'guid') {
1399
1591
  query = `SELECT "guid"
1400
1592
  FROM ${SQLite3Driver.escape(
1401
- this.prefix + 'entities_' + etype
1593
+ this.prefix + 'entities_' + etype,
1402
1594
  )} ${ieTable}
1595
+ ${sortJoin}
1403
1596
  WHERE (${whereClause})
1404
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1597
+ ORDER BY ${sortByInner}, "guid"${limit}${offset}`;
1405
1598
  } else {
1406
1599
  query = `SELECT
1407
1600
  ${eTable}."guid",
@@ -1413,23 +1606,25 @@ export default class SQLite3Driver extends NymphDriver {
1413
1606
  ${cTable}."string",
1414
1607
  ${cTable}."number"
1415
1608
  FROM ${SQLite3Driver.escape(
1416
- this.prefix + 'entities_' + etype
1609
+ this.prefix + 'entities_' + etype,
1417
1610
  )} ${eTable}
1418
1611
  LEFT JOIN ${SQLite3Driver.escape(
1419
- this.prefix + 'data_' + etype
1612
+ this.prefix + 'data_' + etype,
1420
1613
  )} ${dTable} USING ("guid")
1421
1614
  INNER JOIN ${SQLite3Driver.escape(
1422
- this.prefix + 'comparisons_' + etype
1615
+ this.prefix + 'comparisons_' + etype,
1423
1616
  )} ${cTable} USING ("guid", "name")
1617
+ ${sortJoin}
1424
1618
  INNER JOIN (
1425
1619
  SELECT "guid"
1426
1620
  FROM ${SQLite3Driver.escape(
1427
- this.prefix + 'entities_' + etype
1621
+ this.prefix + 'entities_' + etype,
1428
1622
  )} ${ieTable}
1623
+ ${sortJoin}
1429
1624
  WHERE (${whereClause})
1430
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1625
+ ORDER BY ${sortByInner}${limit}${offset}
1431
1626
  ) ${fTable} USING ("guid")
1432
- ORDER BY ${eTable}.${sortBy}`;
1627
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1433
1628
  }
1434
1629
  }
1435
1630
  } else {
@@ -1449,21 +1644,22 @@ export default class SQLite3Driver extends NymphDriver {
1449
1644
  query = `SELECT COUNT("guid") AS "count" FROM (
1450
1645
  SELECT "guid"
1451
1646
  FROM ${SQLite3Driver.escape(
1452
- this.prefix + 'entities_' + etype
1647
+ this.prefix + 'entities_' + etype,
1453
1648
  )} ${ieTable}${limit}${offset}
1454
1649
  )`;
1455
1650
  } else {
1456
1651
  query = `SELECT COUNT("guid") AS "count"
1457
1652
  FROM ${SQLite3Driver.escape(
1458
- this.prefix + 'entities_' + etype
1653
+ this.prefix + 'entities_' + etype,
1459
1654
  )} ${ieTable}`;
1460
1655
  }
1461
1656
  } else if (options.return === 'guid') {
1462
1657
  query = `SELECT "guid"
1463
1658
  FROM ${SQLite3Driver.escape(
1464
- this.prefix + 'entities_' + etype
1659
+ this.prefix + 'entities_' + etype,
1465
1660
  )} ${ieTable}
1466
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1661
+ ${sortJoin}
1662
+ ORDER BY ${sortByInner}, "guid"${limit}${offset}`;
1467
1663
  } else {
1468
1664
  if (limit || offset) {
1469
1665
  query = `SELECT
@@ -1476,22 +1672,24 @@ export default class SQLite3Driver extends NymphDriver {
1476
1672
  ${cTable}."string",
1477
1673
  ${cTable}."number"
1478
1674
  FROM ${SQLite3Driver.escape(
1479
- this.prefix + 'entities_' + etype
1675
+ this.prefix + 'entities_' + etype,
1480
1676
  )} ${eTable}
1481
1677
  LEFT JOIN ${SQLite3Driver.escape(
1482
- this.prefix + 'data_' + etype
1678
+ this.prefix + 'data_' + etype,
1483
1679
  )} ${dTable} USING ("guid")
1484
1680
  INNER JOIN ${SQLite3Driver.escape(
1485
- this.prefix + 'comparisons_' + etype
1681
+ this.prefix + 'comparisons_' + etype,
1486
1682
  )} c USING ("guid", "name")
1683
+ ${sortJoin}
1487
1684
  INNER JOIN (
1488
1685
  SELECT "guid"
1489
1686
  FROM ${SQLite3Driver.escape(
1490
- this.prefix + 'entities_' + etype
1687
+ this.prefix + 'entities_' + etype,
1491
1688
  )} ${ieTable}
1492
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1689
+ ${sortJoin}
1690
+ ORDER BY ${sortByInner}${limit}${offset}
1493
1691
  ) ${fTable} USING ("guid")
1494
- ORDER BY ${eTable}.${sortBy}`;
1692
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1495
1693
  } else {
1496
1694
  query = `SELECT
1497
1695
  ${eTable}."guid",
@@ -1503,15 +1701,16 @@ export default class SQLite3Driver extends NymphDriver {
1503
1701
  ${cTable}."string",
1504
1702
  ${cTable}."number"
1505
1703
  FROM ${SQLite3Driver.escape(
1506
- this.prefix + 'entities_' + etype
1704
+ this.prefix + 'entities_' + etype,
1507
1705
  )} ${eTable}
1508
1706
  LEFT JOIN ${SQLite3Driver.escape(
1509
- this.prefix + 'data_' + etype
1707
+ this.prefix + 'data_' + etype,
1510
1708
  )} ${dTable} USING ("guid")
1511
1709
  INNER JOIN ${SQLite3Driver.escape(
1512
- this.prefix + 'comparisons_' + etype
1710
+ this.prefix + 'comparisons_' + etype,
1513
1711
  )} ${cTable} USING ("guid", "name")
1514
- ORDER BY ${eTable}.${sortBy}`;
1712
+ ${sortJoin}
1713
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1515
1714
  }
1516
1715
  }
1517
1716
  }
@@ -1531,14 +1730,14 @@ export default class SQLite3Driver extends NymphDriver {
1531
1730
  protected performQuery(
1532
1731
  options: Options,
1533
1732
  formattedSelectors: FormattedSelector[],
1534
- etype: string
1733
+ etype: string,
1535
1734
  ): {
1536
1735
  result: any;
1537
1736
  } {
1538
1737
  const { query, params, etypes } = this.makeEntityQuery(
1539
1738
  options,
1540
1739
  formattedSelectors,
1541
- etype
1740
+ etype,
1542
1741
  );
1543
1742
  const result = this.queryIter(query, { etypes, params })[Symbol.iterator]();
1544
1743
  return {
@@ -1557,35 +1756,16 @@ export default class SQLite3Driver extends NymphDriver {
1557
1756
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1558
1757
  options?: Options<T>,
1559
1758
  ...selectors: Selector[]
1560
- ): Promise<ReturnType<T['factorySync']>[]>;
1759
+ ): Promise<EntityInstanceType<T>[]>;
1561
1760
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1562
1761
  options: Options<T> = {},
1563
1762
  ...selectors: Selector[]
1564
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number> {
1565
- return this.getEntitiesSync(options, ...selectors);
1566
- }
1567
-
1568
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1569
- options: Options<T> & { return: 'count' },
1570
- ...selectors: Selector[]
1571
- ): number;
1572
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1573
- options: Options<T> & { return: 'guid' },
1574
- ...selectors: Selector[]
1575
- ): string[];
1576
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1577
- options?: Options<T>,
1578
- ...selectors: Selector[]
1579
- ): ReturnType<T['factorySync']>[];
1580
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1581
- options: Options<T> = {},
1582
- ...selectors: Selector[]
1583
- ): ReturnType<T['factorySync']>[] | string[] | number {
1584
- const { result, process } = this.getEntitesRowLike<T>(
1763
+ ): Promise<EntityInstanceType<T>[] | string[] | number> {
1764
+ const { result, process } = this.getEntitiesRowLike<T>(
1585
1765
  options,
1586
1766
  selectors,
1587
- (options, formattedSelectors, etype) =>
1588
- this.performQuery(options, formattedSelectors, etype),
1767
+ ({ options, selectors, etype }) =>
1768
+ this.performQuery(options, selectors, etype),
1589
1769
  () => {
1590
1770
  const next: any = result.next();
1591
1771
  return next.done ? null : next.value;
@@ -1606,7 +1786,7 @@ export default class SQLite3Driver extends NymphDriver {
1606
1786
  : row.value === 'S'
1607
1787
  ? JSON.stringify(row.string)
1608
1788
  : row.value,
1609
- })
1789
+ }),
1610
1790
  );
1611
1791
  const value = process();
1612
1792
  if (value instanceof Error) {
@@ -1619,175 +1799,227 @@ export default class SQLite3Driver extends NymphDriver {
1619
1799
  if (name == null) {
1620
1800
  throw new InvalidParametersError('Name not given for UID.');
1621
1801
  }
1622
- const result = this.queryGet(
1802
+ const result: any = this.queryGet(
1623
1803
  `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
1624
- `${this.prefix}uids`
1804
+ `${this.prefix}uids`,
1625
1805
  )} WHERE "name"=@name;`,
1626
1806
  {
1627
1807
  params: {
1628
1808
  name: name,
1629
1809
  },
1630
- }
1810
+ },
1631
1811
  );
1632
1812
  return (result?.cur_uid as number | null) ?? null;
1633
1813
  }
1634
1814
 
1635
- public async import(filename: string) {
1636
- this.checkReadOnlyMode();
1815
+ public async importEntity({
1816
+ guid,
1817
+ cdate,
1818
+ mdate,
1819
+ tags,
1820
+ sdata,
1821
+ etype,
1822
+ }: {
1823
+ guid: string;
1824
+ cdate: number;
1825
+ mdate: number;
1826
+ tags: string[];
1827
+ sdata: SerializedEntityData;
1828
+ etype: string;
1829
+ }) {
1637
1830
  try {
1638
- return this.importFromFile(
1639
- filename,
1640
- async (guid, tags, sdata, etype) => {
1641
- this.queryRun(
1642
- `DELETE FROM ${SQLite3Driver.escape(
1643
- `${this.prefix}entities_${etype}`
1644
- )} WHERE "guid"=@guid;`,
1645
- {
1646
- etypes: [etype],
1647
- params: {
1648
- guid,
1649
- },
1650
- }
1651
- );
1652
- this.queryRun(
1653
- `DELETE FROM ${SQLite3Driver.escape(
1654
- `${this.prefix}data_${etype}`
1655
- )} WHERE "guid"=@guid;`,
1656
- {
1657
- etypes: [etype],
1658
- params: {
1659
- guid,
1660
- },
1661
- }
1662
- );
1663
- this.queryRun(
1664
- `DELETE FROM ${SQLite3Driver.escape(
1665
- `${this.prefix}comparisons_${etype}`
1666
- )} WHERE "guid"=@guid;`,
1667
- {
1668
- etypes: [etype],
1669
- params: {
1670
- guid,
1671
- },
1672
- }
1673
- );
1831
+ await this.startTransaction(`nymph-import-entity-${guid}`);
1832
+
1833
+ this.queryRun(
1834
+ `DELETE FROM ${SQLite3Driver.escape(
1835
+ `${this.prefix}entities_${etype}`,
1836
+ )} WHERE "guid"=@guid;`,
1837
+ {
1838
+ etypes: [etype],
1839
+ params: {
1840
+ guid,
1841
+ },
1842
+ },
1843
+ );
1844
+ this.queryRun(
1845
+ `DELETE FROM ${SQLite3Driver.escape(
1846
+ `${this.prefix}data_${etype}`,
1847
+ )} WHERE "guid"=@guid;`,
1848
+ {
1849
+ etypes: [etype],
1850
+ params: {
1851
+ guid,
1852
+ },
1853
+ },
1854
+ );
1855
+ this.queryRun(
1856
+ `DELETE FROM ${SQLite3Driver.escape(
1857
+ `${this.prefix}comparisons_${etype}`,
1858
+ )} WHERE "guid"=@guid;`,
1859
+ {
1860
+ etypes: [etype],
1861
+ params: {
1862
+ guid,
1863
+ },
1864
+ },
1865
+ );
1866
+ this.queryRun(
1867
+ `DELETE FROM ${SQLite3Driver.escape(
1868
+ `${this.prefix}references_${etype}`,
1869
+ )} WHERE "guid"=@guid;`,
1870
+ {
1871
+ etypes: [etype],
1872
+ params: {
1873
+ guid,
1874
+ },
1875
+ },
1876
+ );
1877
+ this.queryRun(
1878
+ `DELETE FROM ${SQLite3Driver.escape(
1879
+ `${this.prefix}uniques_${etype}`,
1880
+ )} WHERE "guid"=@guid;`,
1881
+ {
1882
+ etypes: [etype],
1883
+ params: {
1884
+ guid,
1885
+ },
1886
+ },
1887
+ );
1888
+
1889
+ this.queryRun(
1890
+ `INSERT INTO ${SQLite3Driver.escape(
1891
+ `${this.prefix}entities_${etype}`,
1892
+ )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`,
1893
+ {
1894
+ etypes: [etype],
1895
+ params: {
1896
+ guid,
1897
+ tags: ',' + tags.join(',') + ',',
1898
+ cdate,
1899
+ mdate,
1900
+ },
1901
+ },
1902
+ );
1903
+ for (const name in sdata) {
1904
+ const value = sdata[name];
1905
+ const uvalue = JSON.parse(value);
1906
+ if (value === undefined) {
1907
+ continue;
1908
+ }
1909
+ const storageValue =
1910
+ typeof uvalue === 'number'
1911
+ ? 'N'
1912
+ : typeof uvalue === 'string'
1913
+ ? 'S'
1914
+ : value;
1915
+ this.queryRun(
1916
+ `INSERT INTO ${SQLite3Driver.escape(
1917
+ `${this.prefix}data_${etype}`,
1918
+ )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
1919
+ {
1920
+ etypes: [etype],
1921
+ params: {
1922
+ guid,
1923
+ name,
1924
+ storageValue,
1925
+ },
1926
+ },
1927
+ );
1928
+ this.queryRun(
1929
+ `INSERT INTO ${SQLite3Driver.escape(
1930
+ `${this.prefix}comparisons_${etype}`,
1931
+ )} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`,
1932
+ {
1933
+ etypes: [etype],
1934
+ params: {
1935
+ guid,
1936
+ name,
1937
+ truthy: uvalue ? 1 : 0,
1938
+ string: `${uvalue}`,
1939
+ number: Number(uvalue),
1940
+ },
1941
+ },
1942
+ );
1943
+ const references = this.findReferences(value);
1944
+ for (const reference of references) {
1674
1945
  this.queryRun(
1675
- `DELETE FROM ${SQLite3Driver.escape(
1676
- `${this.prefix}references_${etype}`
1677
- )} WHERE "guid"=@guid;`,
1946
+ `INSERT INTO ${SQLite3Driver.escape(
1947
+ `${this.prefix}references_${etype}`,
1948
+ )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1678
1949
  {
1679
1950
  etypes: [etype],
1680
1951
  params: {
1681
1952
  guid,
1953
+ name,
1954
+ reference,
1682
1955
  },
1683
- }
1956
+ },
1684
1957
  );
1958
+ }
1959
+ }
1960
+ const uniques = await this.nymph
1961
+ .getEntityClassByEtype(etype)
1962
+ .getUniques({ guid, cdate, mdate, tags, data: {}, sdata });
1963
+ for (const unique of uniques) {
1964
+ try {
1685
1965
  this.queryRun(
1686
1966
  `INSERT INTO ${SQLite3Driver.escape(
1687
- `${this.prefix}entities_${etype}`
1688
- )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`,
1967
+ `${this.prefix}uniques_${etype}`,
1968
+ )} ("guid", "unique") VALUES (@guid, @unique);`,
1689
1969
  {
1690
1970
  etypes: [etype],
1691
1971
  params: {
1692
1972
  guid,
1693
- tags: ',' + tags.join(',') + ',',
1694
- cdate: Number(JSON.parse(sdata.cdate)),
1695
- mdate: Number(JSON.parse(sdata.mdate)),
1973
+ unique,
1696
1974
  },
1697
- }
1975
+ },
1698
1976
  );
1699
- delete sdata.cdate;
1700
- delete sdata.mdate;
1701
- for (const name in sdata) {
1702
- const value = sdata[name];
1703
- const uvalue = JSON.parse(value);
1704
- if (value === undefined) {
1705
- continue;
1706
- }
1707
- const storageValue =
1708
- typeof uvalue === 'number'
1709
- ? 'N'
1710
- : typeof uvalue === 'string'
1711
- ? 'S'
1712
- : value;
1713
- this.queryRun(
1714
- `INSERT INTO ${SQLite3Driver.escape(
1715
- `${this.prefix}data_${etype}`
1716
- )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
1717
- {
1718
- etypes: [etype],
1719
- params: {
1720
- guid,
1721
- name,
1722
- storageValue,
1723
- },
1724
- }
1725
- );
1726
- this.queryRun(
1727
- `INSERT INTO ${SQLite3Driver.escape(
1728
- `${this.prefix}comparisons_${etype}`
1729
- )} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`,
1730
- {
1731
- etypes: [etype],
1732
- params: {
1733
- guid,
1734
- name,
1735
- truthy: uvalue ? 1 : 0,
1736
- string: `${uvalue}`,
1737
- number: Number(uvalue),
1738
- },
1739
- }
1977
+ } catch (e: any) {
1978
+ if (e instanceof EntityUniqueConstraintError) {
1979
+ this.nymph.config.debugError(
1980
+ 'sqlite3',
1981
+ `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
1740
1982
  );
1741
- const references = this.findReferences(value);
1742
- for (const reference of references) {
1743
- this.queryRun(
1744
- `INSERT INTO ${SQLite3Driver.escape(
1745
- `${this.prefix}references_${etype}`
1746
- )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1747
- {
1748
- etypes: [etype],
1749
- params: {
1750
- guid,
1751
- name,
1752
- reference,
1753
- },
1754
- }
1755
- );
1756
- }
1757
1983
  }
1984
+ throw e;
1985
+ }
1986
+ }
1987
+ await this.commit(`nymph-import-entity-${guid}`);
1988
+ } catch (e: any) {
1989
+ this.nymph.config.debugError('sqlite3', `Import entity error: "${e}"`);
1990
+ await this.rollback(`nymph-import-entity-${guid}`);
1991
+ throw e;
1992
+ }
1993
+ }
1994
+
1995
+ public async importUID({ name, value }: { name: string; value: number }) {
1996
+ try {
1997
+ await this.startTransaction(`nymph-import-uid-${name}`);
1998
+ this.queryRun(
1999
+ `DELETE FROM ${SQLite3Driver.escape(
2000
+ `${this.prefix}uids`,
2001
+ )} WHERE "name"=@name;`,
2002
+ {
2003
+ params: {
2004
+ name,
2005
+ },
1758
2006
  },
1759
- async (name, curUid) => {
1760
- this.queryRun(
1761
- `DELETE FROM ${SQLite3Driver.escape(
1762
- `${this.prefix}uids`
1763
- )} WHERE "name"=@name;`,
1764
- {
1765
- params: {
1766
- name,
1767
- },
1768
- }
1769
- );
1770
- this.queryRun(
1771
- `INSERT INTO ${SQLite3Driver.escape(
1772
- `${this.prefix}uids`
1773
- )} ("name", "cur_uid") VALUES (@name, @curUid);`,
1774
- {
1775
- params: {
1776
- name,
1777
- curUid,
1778
- },
1779
- }
1780
- );
1781
- },
1782
- async () => {
1783
- await this.startTransaction('nymph-import');
2007
+ );
2008
+ this.queryRun(
2009
+ `INSERT INTO ${SQLite3Driver.escape(
2010
+ `${this.prefix}uids`,
2011
+ )} ("name", "cur_uid") VALUES (@name, @value);`,
2012
+ {
2013
+ params: {
2014
+ name,
2015
+ value,
2016
+ },
1784
2017
  },
1785
- async () => {
1786
- await this.commit('nymph-import');
1787
- }
1788
2018
  );
2019
+ await this.commit(`nymph-import-uid-${name}`);
1789
2020
  } catch (e: any) {
1790
- await this.rollback('nymph-import');
2021
+ this.nymph.config.debugError('sqlite3', `Import UID error: "${e}"`);
2022
+ await this.rollback(`nymph-import-uid-${name}`);
1791
2023
  throw e;
1792
2024
  }
1793
2025
  }
@@ -1796,78 +2028,83 @@ export default class SQLite3Driver extends NymphDriver {
1796
2028
  if (name == null) {
1797
2029
  throw new InvalidParametersError('Name not given for UID.');
1798
2030
  }
1799
- this.checkReadOnlyMode();
1800
2031
  await this.startTransaction('nymph-newuid');
2032
+ let curUid: number | undefined = undefined;
1801
2033
  try {
1802
- let curUid =
1803
- this.queryGet(
1804
- `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
1805
- `${this.prefix}uids`
1806
- )} WHERE "name"=@name;`,
1807
- {
1808
- params: {
1809
- name,
2034
+ curUid =
2035
+ (
2036
+ this.queryGet(
2037
+ `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
2038
+ `${this.prefix}uids`,
2039
+ )} WHERE "name"=@name;`,
2040
+ {
2041
+ params: {
2042
+ name,
2043
+ },
1810
2044
  },
1811
- }
2045
+ ) as any
1812
2046
  )?.cur_uid ?? null;
1813
2047
  if (curUid == null) {
1814
2048
  curUid = 1;
1815
2049
  this.queryRun(
1816
2050
  `INSERT INTO ${SQLite3Driver.escape(
1817
- `${this.prefix}uids`
2051
+ `${this.prefix}uids`,
1818
2052
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
1819
2053
  {
1820
2054
  params: {
1821
2055
  name,
1822
2056
  curUid,
1823
2057
  },
1824
- }
2058
+ },
1825
2059
  );
1826
2060
  } else {
1827
2061
  curUid++;
1828
2062
  this.queryRun(
1829
2063
  `UPDATE ${SQLite3Driver.escape(
1830
- `${this.prefix}uids`
2064
+ `${this.prefix}uids`,
1831
2065
  )} SET "cur_uid"=@curUid WHERE "name"=@name;`,
1832
2066
  {
1833
2067
  params: {
1834
2068
  curUid,
1835
2069
  name,
1836
2070
  },
1837
- }
2071
+ },
1838
2072
  );
1839
2073
  }
1840
- await this.commit('nymph-newuid');
1841
- return curUid as number;
1842
2074
  } catch (e: any) {
2075
+ this.nymph.config.debugError('sqlite3', `New UID error: "${e}"`);
1843
2076
  await this.rollback('nymph-newuid');
1844
2077
  throw e;
1845
2078
  }
2079
+
2080
+ await this.commit('nymph-newuid');
2081
+ return curUid as number;
1846
2082
  }
1847
2083
 
1848
2084
  public async renameUID(oldName: string, newName: string) {
1849
2085
  if (oldName == null || newName == null) {
1850
2086
  throw new InvalidParametersError('Name not given for UID.');
1851
2087
  }
1852
- this.checkReadOnlyMode();
2088
+ await this.startTransaction('nymph-rename-uid');
1853
2089
  this.queryRun(
1854
2090
  `UPDATE ${SQLite3Driver.escape(
1855
- `${this.prefix}uids`
2091
+ `${this.prefix}uids`,
1856
2092
  )} SET "name"=@newName WHERE "name"=@oldName;`,
1857
2093
  {
1858
2094
  params: {
1859
2095
  newName,
1860
2096
  oldName,
1861
2097
  },
1862
- }
2098
+ },
1863
2099
  );
2100
+ await this.commit('nymph-rename-uid');
1864
2101
  return true;
1865
2102
  }
1866
2103
 
1867
2104
  public async rollback(name: string) {
1868
2105
  if (name == null || typeof name !== 'string' || name.length === 0) {
1869
2106
  throw new InvalidParametersError(
1870
- 'Transaction rollback attempted without a name.'
2107
+ 'Transaction rollback attempted without a name.',
1871
2108
  );
1872
2109
  }
1873
2110
  if (this.store.transactionsStarted === 0) {
@@ -1875,16 +2112,27 @@ export default class SQLite3Driver extends NymphDriver {
1875
2112
  }
1876
2113
  this.queryRun(`ROLLBACK TO SAVEPOINT ${SQLite3Driver.escape(name)};`);
1877
2114
  this.store.transactionsStarted--;
2115
+
2116
+ if (
2117
+ this.store.transactionsStarted === 0 &&
2118
+ this.store.linkWrite &&
2119
+ !this.config.explicitWrite
2120
+ ) {
2121
+ this.store.linkWrite.exec('PRAGMA optimize;');
2122
+ this.store.linkWrite.close();
2123
+ this.store.linkWrite = undefined;
2124
+ }
2125
+
1878
2126
  return true;
1879
2127
  }
1880
2128
 
1881
2129
  public async saveEntity(entity: EntityInterface) {
1882
- this.checkReadOnlyMode();
1883
2130
  const insertData = (
1884
2131
  guid: string,
1885
2132
  data: EntityData,
1886
2133
  sdata: SerializedEntityData,
1887
- etype: string
2134
+ uniques: string[],
2135
+ etype: string,
1888
2136
  ) => {
1889
2137
  const runInsertQuery = (name: string, value: any, svalue: string) => {
1890
2138
  if (value === undefined) {
@@ -1898,7 +2146,7 @@ export default class SQLite3Driver extends NymphDriver {
1898
2146
  : svalue;
1899
2147
  this.queryRun(
1900
2148
  `INSERT INTO ${SQLite3Driver.escape(
1901
- `${this.prefix}data_${etype}`
2149
+ `${this.prefix}data_${etype}`,
1902
2150
  )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
1903
2151
  {
1904
2152
  etypes: [etype],
@@ -1907,11 +2155,11 @@ export default class SQLite3Driver extends NymphDriver {
1907
2155
  name,
1908
2156
  storageValue,
1909
2157
  },
1910
- }
2158
+ },
1911
2159
  );
1912
2160
  this.queryRun(
1913
2161
  `INSERT INTO ${SQLite3Driver.escape(
1914
- `${this.prefix}comparisons_${etype}`
2162
+ `${this.prefix}comparisons_${etype}`,
1915
2163
  )} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`,
1916
2164
  {
1917
2165
  etypes: [etype],
@@ -1922,13 +2170,13 @@ export default class SQLite3Driver extends NymphDriver {
1922
2170
  string: `${value}`,
1923
2171
  number: Number(value),
1924
2172
  },
1925
- }
2173
+ },
1926
2174
  );
1927
2175
  const references = this.findReferences(svalue);
1928
2176
  for (const reference of references) {
1929
2177
  this.queryRun(
1930
2178
  `INSERT INTO ${SQLite3Driver.escape(
1931
- `${this.prefix}references_${etype}`
2179
+ `${this.prefix}references_${etype}`,
1932
2180
  )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1933
2181
  {
1934
2182
  etypes: [etype],
@@ -1937,10 +2185,34 @@ export default class SQLite3Driver extends NymphDriver {
1937
2185
  name,
1938
2186
  reference,
1939
2187
  },
1940
- }
2188
+ },
1941
2189
  );
1942
2190
  }
1943
2191
  };
2192
+ for (const unique of uniques) {
2193
+ try {
2194
+ this.queryRun(
2195
+ `INSERT INTO ${SQLite3Driver.escape(
2196
+ `${this.prefix}uniques_${etype}`,
2197
+ )} ("guid", "unique") VALUES (@guid, @unique);`,
2198
+ {
2199
+ etypes: [etype],
2200
+ params: {
2201
+ guid,
2202
+ unique,
2203
+ },
2204
+ },
2205
+ );
2206
+ } catch (e: any) {
2207
+ if (e instanceof EntityUniqueConstraintError) {
2208
+ this.nymph.config.debugError(
2209
+ 'sqlite3',
2210
+ `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
2211
+ );
2212
+ }
2213
+ throw e;
2214
+ }
2215
+ }
1944
2216
  for (const name in data) {
1945
2217
  runInsertQuery(name, data[name], JSON.stringify(data[name]));
1946
2218
  }
@@ -1948,13 +2220,20 @@ export default class SQLite3Driver extends NymphDriver {
1948
2220
  runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1949
2221
  }
1950
2222
  };
2223
+ let inTransaction = false;
1951
2224
  try {
1952
2225
  return this.saveEntityRowLike(
1953
2226
  entity,
1954
- async (_entity, guid, tags, data, sdata, cdate, etype) => {
2227
+ async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
2228
+ if (
2229
+ Object.keys(data).length === 0 &&
2230
+ Object.keys(sdata).length === 0
2231
+ ) {
2232
+ return false;
2233
+ }
1955
2234
  this.queryRun(
1956
2235
  `INSERT INTO ${SQLite3Driver.escape(
1957
- `${this.prefix}entities_${etype}`
2236
+ `${this.prefix}entities_${etype}`,
1958
2237
  )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @cdate);`,
1959
2238
  {
1960
2239
  etypes: [etype],
@@ -1963,15 +2242,21 @@ export default class SQLite3Driver extends NymphDriver {
1963
2242
  tags: ',' + tags.join(',') + ',',
1964
2243
  cdate,
1965
2244
  },
1966
- }
2245
+ },
1967
2246
  );
1968
- insertData(guid, data, sdata, etype);
2247
+ insertData(guid, data, sdata, uniques, etype);
1969
2248
  return true;
1970
2249
  },
1971
- async (entity, guid, tags, data, sdata, mdate, etype) => {
2250
+ async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
2251
+ if (
2252
+ Object.keys(data).length === 0 &&
2253
+ Object.keys(sdata).length === 0
2254
+ ) {
2255
+ return false;
2256
+ }
1972
2257
  const info = this.queryRun(
1973
2258
  `UPDATE ${SQLite3Driver.escape(
1974
- `${this.prefix}entities_${etype}`
2259
+ `${this.prefix}entities_${etype}`,
1975
2260
  )} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=@guid AND "mdate" <= @emdate;`,
1976
2261
  {
1977
2262
  etypes: [etype],
@@ -1981,62 +2266,80 @@ export default class SQLite3Driver extends NymphDriver {
1981
2266
  guid,
1982
2267
  emdate: Number(entity.mdate),
1983
2268
  },
1984
- }
2269
+ },
1985
2270
  );
1986
2271
  let success = false;
1987
2272
  if (info.changes === 1) {
1988
2273
  this.queryRun(
1989
2274
  `DELETE FROM ${SQLite3Driver.escape(
1990
- `${this.prefix}data_${etype}`
2275
+ `${this.prefix}data_${etype}`,
1991
2276
  )} WHERE "guid"=@guid;`,
1992
2277
  {
1993
2278
  etypes: [etype],
1994
2279
  params: {
1995
2280
  guid,
1996
2281
  },
1997
- }
2282
+ },
1998
2283
  );
1999
2284
  this.queryRun(
2000
2285
  `DELETE FROM ${SQLite3Driver.escape(
2001
- `${this.prefix}comparisons_${etype}`
2286
+ `${this.prefix}comparisons_${etype}`,
2002
2287
  )} WHERE "guid"=@guid;`,
2003
2288
  {
2004
2289
  etypes: [etype],
2005
2290
  params: {
2006
2291
  guid,
2007
2292
  },
2008
- }
2293
+ },
2009
2294
  );
2010
2295
  this.queryRun(
2011
2296
  `DELETE FROM ${SQLite3Driver.escape(
2012
- `${this.prefix}references_${etype}`
2297
+ `${this.prefix}references_${etype}`,
2013
2298
  )} WHERE "guid"=@guid;`,
2014
2299
  {
2015
2300
  etypes: [etype],
2016
2301
  params: {
2017
2302
  guid,
2018
2303
  },
2019
- }
2304
+ },
2305
+ );
2306
+ this.queryRun(
2307
+ `DELETE FROM ${SQLite3Driver.escape(
2308
+ `${this.prefix}uniques_${etype}`,
2309
+ )} WHERE "guid"=@guid;`,
2310
+ {
2311
+ etypes: [etype],
2312
+ params: {
2313
+ guid,
2314
+ },
2315
+ },
2020
2316
  );
2021
- insertData(guid, data, sdata, etype);
2317
+ insertData(guid, data, sdata, uniques, etype);
2022
2318
  success = true;
2023
2319
  }
2024
2320
  return success;
2025
2321
  },
2026
2322
  async () => {
2027
2323
  await this.startTransaction('nymph-save');
2324
+ inTransaction = true;
2028
2325
  },
2029
2326
  async (success) => {
2030
- if (success) {
2031
- await this.commit('nymph-save');
2032
- } else {
2033
- await this.rollback('nymph-save');
2327
+ if (inTransaction) {
2328
+ inTransaction = false;
2329
+ if (success) {
2330
+ await this.commit('nymph-save');
2331
+ } else {
2332
+ await this.rollback('nymph-save');
2333
+ }
2034
2334
  }
2035
2335
  return success;
2036
- }
2336
+ },
2037
2337
  );
2038
2338
  } catch (e: any) {
2039
- await this.rollback('nymph-save');
2339
+ this.nymph.config.debugError('sqlite3', `Save entity error: "${e}"`);
2340
+ if (inTransaction) {
2341
+ await this.rollback('nymph-save');
2342
+ }
2040
2343
  throw e;
2041
2344
  }
2042
2345
  }
@@ -2045,37 +2348,45 @@ export default class SQLite3Driver extends NymphDriver {
2045
2348
  if (name == null) {
2046
2349
  throw new InvalidParametersError('Name not given for UID.');
2047
2350
  }
2048
- this.checkReadOnlyMode();
2351
+ await this.startTransaction('nymph-set-uid');
2049
2352
  this.queryRun(
2050
2353
  `DELETE FROM ${SQLite3Driver.escape(
2051
- `${this.prefix}uids`
2354
+ `${this.prefix}uids`,
2052
2355
  )} WHERE "name"=@name;`,
2053
2356
  {
2054
2357
  params: {
2055
2358
  name,
2056
2359
  },
2057
- }
2360
+ },
2058
2361
  );
2059
2362
  this.queryRun(
2060
2363
  `INSERT INTO ${SQLite3Driver.escape(
2061
- `${this.prefix}uids`
2364
+ `${this.prefix}uids`,
2062
2365
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2063
2366
  {
2064
2367
  params: {
2065
2368
  name,
2066
2369
  curUid,
2067
2370
  },
2068
- }
2371
+ },
2069
2372
  );
2373
+ await this.commit('nymph-set-uid');
2070
2374
  return true;
2071
2375
  }
2072
2376
 
2377
+ public async internalTransaction(name: string) {
2378
+ await this.startTransaction(name);
2379
+ }
2380
+
2073
2381
  public async startTransaction(name: string) {
2074
2382
  if (name == null || typeof name !== 'string' || name.length === 0) {
2075
2383
  throw new InvalidParametersError(
2076
- 'Transaction start attempted without a name.'
2384
+ 'Transaction start attempted without a name.',
2077
2385
  );
2078
2386
  }
2387
+ if (!this.config.explicitWrite && !this.store.linkWrite) {
2388
+ this._connect(true);
2389
+ }
2079
2390
  this.queryRun(`SAVEPOINT ${SQLite3Driver.escape(name)};`);
2080
2391
  this.store.transactionsStarted++;
2081
2392
  return this.nymph;