@nymphjs/driver-sqlite3 1.0.0-beta.11 → 1.0.0-beta.110

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,26 @@
1
1
  import SQLite3 from 'better-sqlite3';
2
+ import type {
3
+ SearchTerm,
4
+ SearchOrTerm,
5
+ SearchNotTerm,
6
+ SearchSeriesTerm,
7
+ } from '@sciactive/tokenizer';
2
8
  import {
3
9
  NymphDriver,
4
- EntityConstructor,
5
- EntityData,
6
- EntityInterface,
7
- SerializedEntityData,
10
+ type EntityConstructor,
11
+ type EntityData,
12
+ type EntityObjectType,
13
+ type EntityInterface,
14
+ type EntityInstanceType,
15
+ type SerializedEntityData,
16
+ type FormattedSelector,
17
+ type Options,
18
+ type Selector,
19
+ EntityUniqueConstraintError,
8
20
  InvalidParametersError,
9
21
  NotConfiguredError,
10
22
  QueryFailedError,
11
23
  UnableToConnectError,
12
- FormattedSelector,
13
- Options,
14
- Selector,
15
24
  xor,
16
25
  } from '@nymphjs/nymph';
17
26
  import { makeTableSuffix } from '@nymphjs/guid';
@@ -19,10 +28,11 @@ import { makeTableSuffix } from '@nymphjs/guid';
19
28
  import {
20
29
  SQLite3DriverConfig,
21
30
  SQLite3DriverConfigDefaults as defaults,
22
- } from './conf';
31
+ } from './conf/index.js';
23
32
 
24
33
  class InternalStore {
25
34
  public link: SQLite3.Database;
35
+ public linkWrite?: SQLite3.Database;
26
36
  public connected: boolean = false;
27
37
  public transactionsStarted = 0;
28
38
 
@@ -43,16 +53,23 @@ export default class SQLite3Driver extends NymphDriver {
43
53
  static escape(input: string) {
44
54
  if (input.indexOf('\x00') !== -1) {
45
55
  throw new InvalidParametersError(
46
- 'SQLite3 identifiers (like entity ETYPE) cannot contain null characters.'
56
+ 'SQLite3 identifiers (like entity ETYPE) cannot contain null characters.',
47
57
  );
48
58
  }
49
59
 
50
60
  return '"' + input.replace(/"/g, () => '""') + '"';
51
61
  }
52
62
 
63
+ static escapeValue(input: string) {
64
+ return "'" + input.replace(/'/g, () => "''") + "'";
65
+ }
66
+
53
67
  constructor(config: Partial<SQLite3DriverConfig>, store?: InternalStore) {
54
68
  super();
55
69
  this.config = { ...defaults, ...config };
70
+ if (this.config.filename === ':memory:') {
71
+ this.config.explicitWrite = true;
72
+ }
56
73
  this.prefix = this.config.prefix;
57
74
  if (store) {
58
75
  this.store = store;
@@ -75,57 +92,107 @@ export default class SQLite3Driver extends NymphDriver {
75
92
  *
76
93
  * @returns Whether this instance is connected to a SQLite3 database.
77
94
  */
78
- public async connect() {
79
- const { filename, fileMustExist, timeout, readonly, wal, verbose } =
80
- this.config;
81
-
95
+ public connect() {
82
96
  if (this.store && this.store.connected) {
83
- return true;
97
+ return Promise.resolve(true);
84
98
  }
85
99
 
86
100
  // Connecting
101
+ this._connect(false);
102
+
103
+ return Promise.resolve(this.store.connected);
104
+ }
105
+
106
+ private _connect(write: boolean) {
107
+ const { filename, fileMustExist, timeout, explicitWrite, wal, verbose } =
108
+ this.config;
109
+
87
110
  try {
88
- const link = new SQLite3(filename, {
89
- readonly,
90
- fileMustExist,
91
- timeout,
92
- verbose,
93
- });
111
+ const setOptions = (link: SQLite3.Database) => {
112
+ // Set database and connection options.
113
+ if (wal) {
114
+ link.pragma('journal_mode = WAL;');
115
+ }
116
+ link.pragma('encoding = "UTF-8";');
117
+ link.pragma('foreign_keys = 1;');
118
+ link.pragma('case_sensitive_like = 1;');
119
+ for (let pragma of this.config.pragmas) {
120
+ link.pragma(pragma);
121
+ }
122
+ // Create the preg_match and regexp functions.
123
+ link.function('regexp', { deterministic: true }, ((
124
+ pattern: string,
125
+ subject: string,
126
+ ) => (this.posixRegexMatch(pattern, subject) ? 1 : 0)) as (
127
+ ...params: any[]
128
+ ) => any);
129
+ };
130
+
131
+ let link: SQLite3.Database;
132
+ try {
133
+ link = new SQLite3(filename, {
134
+ readonly: !explicitWrite && !write,
135
+ fileMustExist,
136
+ timeout,
137
+ verbose,
138
+ });
139
+ } catch (e: any) {
140
+ if (
141
+ e.code === 'SQLITE_CANTOPEN' &&
142
+ !explicitWrite &&
143
+ !write &&
144
+ !this.config.fileMustExist
145
+ ) {
146
+ // This happens when the file doesn't exist and we attempt to open it
147
+ // readonly.
148
+ // First open it in write mode.
149
+ const writeLink = new SQLite3(filename, {
150
+ readonly: false,
151
+ fileMustExist,
152
+ timeout,
153
+ verbose,
154
+ });
155
+ setOptions(writeLink);
156
+ writeLink.close();
157
+ // Now open in readonly.
158
+ link = new SQLite3(filename, {
159
+ readonly: true,
160
+ fileMustExist,
161
+ timeout,
162
+ verbose,
163
+ });
164
+ } else {
165
+ throw e;
166
+ }
167
+ }
94
168
 
95
169
  if (!this.store) {
170
+ if (write) {
171
+ throw new Error(
172
+ 'Tried to open in write without opening in read first.',
173
+ );
174
+ }
96
175
  this.store = new InternalStore(link);
176
+ } else if (write) {
177
+ this.store.linkWrite = link;
97
178
  } else {
98
179
  this.store.link = link;
99
180
  }
100
181
  this.store.connected = true;
101
- // Set database and connection options.
102
- if (wal) {
103
- this.store.link.pragma('journal_mode = WAL;');
104
- }
105
- this.store.link.pragma('encoding = "UTF-8";');
106
- this.store.link.pragma('foreign_keys = 1;');
107
- this.store.link.pragma('case_sensitive_like = 1;');
108
- // Create the preg_match and regexp functions.
109
- this.store.link.function(
110
- 'regexp',
111
- { deterministic: true },
112
- (pattern: string, subject: string) =>
113
- this.posixRegexMatch(pattern, subject) ? 1 : 0
114
- );
182
+ setOptions(link);
115
183
  } catch (e: any) {
116
184
  if (this.store) {
117
185
  this.store.connected = false;
118
186
  }
119
187
  if (filename === ':memory:') {
120
188
  throw new NotConfiguredError(
121
- "It seems the config hasn't been set up correctly."
189
+ "It seems the config hasn't been set up correctly. Could not connect: " +
190
+ e?.message,
122
191
  );
123
192
  } else {
124
193
  throw new UnableToConnectError('Could not connect: ' + e?.message);
125
194
  }
126
195
  }
127
-
128
- return this.store.connected;
129
196
  }
130
197
 
131
198
  /**
@@ -135,8 +202,16 @@ export default class SQLite3Driver extends NymphDriver {
135
202
  */
136
203
  public async disconnect() {
137
204
  if (this.store.connected) {
138
- this.store.link.exec('PRAGMA optimize;');
205
+ if (this.store.linkWrite && !this.config.explicitWrite) {
206
+ this.store.linkWrite.exec('PRAGMA optimize;');
207
+ this.store.linkWrite.close();
208
+ this.store.linkWrite = undefined;
209
+ }
210
+ if (this.config.explicitWrite) {
211
+ this.store.link.exec('PRAGMA optimize;');
212
+ }
139
213
  this.store.link.close();
214
+ this.store.transactionsStarted = 0;
140
215
  this.store.connected = false;
141
216
  }
142
217
  return this.store.connected;
@@ -155,15 +230,346 @@ export default class SQLite3Driver extends NymphDriver {
155
230
  return this.store.connected;
156
231
  }
157
232
 
158
- /**
159
- * Check if SQLite3 DB is read only and throw error if so.
160
- */
161
- private checkReadOnlyMode() {
162
- if (this.config.readonly) {
163
- throw new InvalidParametersError(
164
- 'Attempt to write to SQLite3 DB in read only mode.'
165
- );
166
- }
233
+ private createEntitiesTable(etype: string) {
234
+ // Create the entity table.
235
+ this.queryRun(
236
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
237
+ `${this.prefix}entities_${etype}`,
238
+ )} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL, "user" CHARACTER(24), "group" CHARACTER(24), "acUser" INT(1), "acGroup" INT(1), "acOther" INT(1), "acRead" TEXT, "acWrite" TEXT, "acFull" TEXT);`,
239
+ );
240
+ this.queryRun(
241
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
242
+ `${this.prefix}entities_${etype}_id_cdate`,
243
+ )} ON ${SQLite3Driver.escape(
244
+ `${this.prefix}entities_${etype}`,
245
+ )} ("cdate");`,
246
+ );
247
+ this.queryRun(
248
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
249
+ `${this.prefix}entities_${etype}_id_mdate`,
250
+ )} ON ${SQLite3Driver.escape(
251
+ `${this.prefix}entities_${etype}`,
252
+ )} ("mdate");`,
253
+ );
254
+ this.queryRun(
255
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
256
+ `${this.prefix}entities_${etype}_id_tags`,
257
+ )} ON ${SQLite3Driver.escape(
258
+ `${this.prefix}entities_${etype}`,
259
+ )} ("tags");`,
260
+ );
261
+ this.createEntitiesTilmeldIndexes(etype);
262
+ }
263
+
264
+ private addTilmeldColumnsAndIndexes(etype: string) {
265
+ this.queryRun(
266
+ `ALTER TABLE ${SQLite3Driver.escape(
267
+ `${this.prefix}entities_${etype}`,
268
+ )} ADD COLUMN "user" CHARACTER(24);`,
269
+ );
270
+ this.queryRun(
271
+ `ALTER TABLE ${SQLite3Driver.escape(
272
+ `${this.prefix}entities_${etype}`,
273
+ )} ADD COLUMN "group" CHARACTER(24);`,
274
+ );
275
+ this.queryRun(
276
+ `ALTER TABLE ${SQLite3Driver.escape(
277
+ `${this.prefix}entities_${etype}`,
278
+ )} ADD COLUMN "acUser" INT(1);`,
279
+ );
280
+ this.queryRun(
281
+ `ALTER TABLE ${SQLite3Driver.escape(
282
+ `${this.prefix}entities_${etype}`,
283
+ )} ADD COLUMN "acGroup" INT(1);`,
284
+ );
285
+ this.queryRun(
286
+ `ALTER TABLE ${SQLite3Driver.escape(
287
+ `${this.prefix}entities_${etype}`,
288
+ )} ADD COLUMN "acOther" INT(1);`,
289
+ );
290
+ this.queryRun(
291
+ `ALTER TABLE ${SQLite3Driver.escape(
292
+ `${this.prefix}entities_${etype}`,
293
+ )} ADD COLUMN "acRead" TEXT;`,
294
+ );
295
+ this.queryRun(
296
+ `ALTER TABLE ${SQLite3Driver.escape(
297
+ `${this.prefix}entities_${etype}`,
298
+ )} ADD COLUMN "acWrite" TEXT;`,
299
+ );
300
+ this.queryRun(
301
+ `ALTER TABLE ${SQLite3Driver.escape(
302
+ `${this.prefix}entities_${etype}`,
303
+ )} ADD COLUMN "acFull" TEXT;`,
304
+ );
305
+ this.createEntitiesTilmeldIndexes(etype);
306
+ }
307
+
308
+ private createEntitiesTilmeldIndexes(etype: string) {
309
+ this.queryRun(
310
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
311
+ `${this.prefix}entities_${etype}_id_user_acUser`,
312
+ )} ON ${SQLite3Driver.escape(
313
+ `${this.prefix}entities_${etype}`,
314
+ )} ("user", "acUser");`,
315
+ );
316
+ this.queryRun(
317
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
318
+ `${this.prefix}entities_${etype}_id_group_acGroup`,
319
+ )} ON ${SQLite3Driver.escape(
320
+ `${this.prefix}entities_${etype}`,
321
+ )} ("group", "acGroup");`,
322
+ );
323
+ this.queryRun(
324
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
325
+ `${this.prefix}entities_${etype}_id_acUser`,
326
+ )} ON ${SQLite3Driver.escape(
327
+ `${this.prefix}entities_${etype}`,
328
+ )} ("acUser");`,
329
+ );
330
+ this.queryRun(
331
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
332
+ `${this.prefix}entities_${etype}_id_acGroup`,
333
+ )} ON ${SQLite3Driver.escape(
334
+ `${this.prefix}entities_${etype}`,
335
+ )} ("acGroup");`,
336
+ );
337
+ this.queryRun(
338
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
339
+ `${this.prefix}entities_${etype}_id_acOther`,
340
+ )} ON ${SQLite3Driver.escape(
341
+ `${this.prefix}entities_${etype}`,
342
+ )} ("acOther");`,
343
+ );
344
+ this.queryRun(
345
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
346
+ `${this.prefix}entities_${etype}_id_acRead`,
347
+ )} ON ${SQLite3Driver.escape(
348
+ `${this.prefix}entities_${etype}`,
349
+ )} ("acRead");`,
350
+ );
351
+ this.queryRun(
352
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
353
+ `${this.prefix}entities_${etype}_id_acWrite`,
354
+ )} ON ${SQLite3Driver.escape(
355
+ `${this.prefix}entities_${etype}`,
356
+ )} ("acWrite");`,
357
+ );
358
+ this.queryRun(
359
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
360
+ `${this.prefix}entities_${etype}_id_acFull`,
361
+ )} ON ${SQLite3Driver.escape(
362
+ `${this.prefix}entities_${etype}`,
363
+ )} ("acFull");`,
364
+ );
365
+ }
366
+
367
+ private createDataTable(etype: string) {
368
+ // Create the data table.
369
+ this.queryRun(
370
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
371
+ `${this.prefix}data_${etype}`,
372
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
373
+ `${this.prefix}entities_${etype}`,
374
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "value" CHARACTER(1) NOT NULL, "json" BLOB, "string" TEXT, "number" REAL, "truthy" INTEGER, PRIMARY KEY("guid", "name"));`,
375
+ );
376
+ this.queryRun(
377
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
378
+ `${this.prefix}data_${etype}_id_guid`,
379
+ )} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid");`,
380
+ );
381
+ this.queryRun(
382
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
383
+ `${this.prefix}data_${etype}_id_guid_name`,
384
+ )} ON ${SQLite3Driver.escape(
385
+ `${this.prefix}data_${etype}`,
386
+ )} ("guid", "name");`,
387
+ );
388
+ this.queryRun(
389
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
390
+ `${this.prefix}data_${etype}_id_name`,
391
+ )} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("name");`,
392
+ );
393
+ this.queryRun(
394
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
395
+ `${this.prefix}data_${etype}_id_name_string`,
396
+ )} ON ${SQLite3Driver.escape(
397
+ `${this.prefix}data_${etype}`,
398
+ )} ("name", "string");`,
399
+ );
400
+ this.queryRun(
401
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
402
+ `${this.prefix}data_${etype}_id_name_number`,
403
+ )} ON ${SQLite3Driver.escape(
404
+ `${this.prefix}data_${etype}`,
405
+ )} ("name", "number");`,
406
+ );
407
+ this.queryRun(
408
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
409
+ `${this.prefix}data_${etype}_id_guid_name_number`,
410
+ )} ON ${SQLite3Driver.escape(
411
+ `${this.prefix}data_${etype}`,
412
+ )} ("guid", "name", "number");`,
413
+ );
414
+ this.queryRun(
415
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
416
+ `${this.prefix}data_${etype}_id_name_truthy`,
417
+ )} ON ${SQLite3Driver.escape(
418
+ `${this.prefix}data_${etype}`,
419
+ )} ("name", "truthy");`,
420
+ );
421
+ this.queryRun(
422
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
423
+ `${this.prefix}data_${etype}_id_guid_name_truthy`,
424
+ )} ON ${SQLite3Driver.escape(
425
+ `${this.prefix}data_${etype}`,
426
+ )} ("guid", "name", "truthy");`,
427
+ );
428
+ this.queryRun(
429
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
430
+ `${this.prefix}data_${etype}_id_acuserread`,
431
+ )} ON ${SQLite3Driver.escape(
432
+ `${this.prefix}data_${etype}`,
433
+ )} ("guid") WHERE "name"=\'acUser\' AND "number" >= 1;`,
434
+ );
435
+ this.queryRun(
436
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
437
+ `${this.prefix}data_${etype}_id_acgroupread`,
438
+ )} ON ${SQLite3Driver.escape(
439
+ `${this.prefix}data_${etype}`,
440
+ )} ("guid") WHERE "name"=\'acGroup\' AND "number" >= 1;`,
441
+ );
442
+ this.queryRun(
443
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
444
+ `${this.prefix}data_${etype}_id_acotherread`,
445
+ )} ON ${SQLite3Driver.escape(
446
+ `${this.prefix}data_${etype}`,
447
+ )} ("guid") WHERE "name"=\'acOther\' AND "number" >= 1;`,
448
+ );
449
+ this.queryRun(
450
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
451
+ `${this.prefix}data_${etype}_id_acuser`,
452
+ )} ON ${SQLite3Driver.escape(
453
+ `${this.prefix}data_${etype}`,
454
+ )} ("guid") WHERE "name"=\'user\';`,
455
+ );
456
+ this.queryRun(
457
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
458
+ `${this.prefix}data_${etype}_id_acgroup`,
459
+ )} ON ${SQLite3Driver.escape(
460
+ `${this.prefix}data_${etype}`,
461
+ )} ("guid") WHERE "name"=\'group\';`,
462
+ );
463
+ }
464
+
465
+ private createReferencesTable(etype: string) {
466
+ // Create the references table.
467
+ this.queryRun(
468
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
469
+ `${this.prefix}references_${etype}`,
470
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
471
+ `${this.prefix}entities_${etype}`,
472
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`,
473
+ );
474
+ this.queryRun(
475
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
476
+ `${this.prefix}references_${etype}_id_guid`,
477
+ )} ON ${SQLite3Driver.escape(
478
+ `${this.prefix}references_${etype}`,
479
+ )} ("guid");`,
480
+ );
481
+ this.queryRun(
482
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
483
+ `${this.prefix}references_${etype}_id_name`,
484
+ )} ON ${SQLite3Driver.escape(
485
+ `${this.prefix}references_${etype}`,
486
+ )} ("name");`,
487
+ );
488
+ this.queryRun(
489
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
490
+ `${this.prefix}references_${etype}_id_name_reference`,
491
+ )} ON ${SQLite3Driver.escape(
492
+ `${this.prefix}references_${etype}`,
493
+ )} ("name", "reference");`,
494
+ );
495
+ this.queryRun(
496
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
497
+ `${this.prefix}references_${etype}_id_reference`,
498
+ )} ON ${SQLite3Driver.escape(
499
+ `${this.prefix}references_${etype}`,
500
+ )} ("reference");`,
501
+ );
502
+ this.queryRun(
503
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
504
+ `${this.prefix}references_${etype}_id_guid_name`,
505
+ )} ON ${SQLite3Driver.escape(
506
+ `${this.prefix}references_${etype}`,
507
+ )} ("guid", "name");`,
508
+ );
509
+ this.queryRun(
510
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
511
+ `${this.prefix}references_${etype}_id_guid_name_reference`,
512
+ )} ON ${SQLite3Driver.escape(
513
+ `${this.prefix}references_${etype}`,
514
+ )} ("guid", "name", "reference");`,
515
+ );
516
+ this.queryRun(
517
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
518
+ `${this.prefix}references_${etype}_id_reference_name_guid`,
519
+ )} ON ${SQLite3Driver.escape(
520
+ `${this.prefix}references_${etype}`,
521
+ )} ("reference", "name", "guid");`,
522
+ );
523
+ this.queryRun(
524
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
525
+ `${this.prefix}references_${etype}_id_reference_guid_name`,
526
+ )} ON ${SQLite3Driver.escape(
527
+ `${this.prefix}references_${etype}`,
528
+ )} ("reference", "guid", "name");`,
529
+ );
530
+ this.queryRun(
531
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
532
+ `${this.prefix}references_${etype}_id_reference_guid_nameuser`,
533
+ )} ON ${SQLite3Driver.escape(
534
+ `${this.prefix}references_${etype}`,
535
+ )} ("reference", "guid") WHERE "name"=\'user\';`,
536
+ );
537
+ this.queryRun(
538
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
539
+ `${this.prefix}references_${etype}_id_reference_guid_namegroup`,
540
+ )} ON ${SQLite3Driver.escape(
541
+ `${this.prefix}references_${etype}`,
542
+ )} ("reference", "guid") WHERE "name"=\'group\';`,
543
+ );
544
+ }
545
+
546
+ private createTokensTable(etype: string) {
547
+ // Create the tokens table.
548
+ this.queryRun(
549
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
550
+ `${this.prefix}tokens_${etype}`,
551
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
552
+ `${this.prefix}entities_${etype}`,
553
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "token" INTEGER NOT NULL, "position" INTEGER NOT NULL, "stem" INTEGER NOT NULL, PRIMARY KEY("guid", "name", "token", "position"));`,
554
+ );
555
+ this.queryRun(
556
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
557
+ `${this.prefix}tokens_${etype}_id_name_token`,
558
+ )} ON ${SQLite3Driver.escape(
559
+ `${this.prefix}tokens_${etype}`,
560
+ )} ("name", "token");`,
561
+ );
562
+ }
563
+
564
+ private createUniquesTable(etype: string) {
565
+ // Create the unique strings table.
566
+ this.queryRun(
567
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
568
+ `${this.prefix}uniques_${etype}`,
569
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
570
+ `${this.prefix}entities_${etype}`,
571
+ )} ("guid") ON DELETE CASCADE, "unique" TEXT NOT NULL UNIQUE, PRIMARY KEY("guid", "unique"));`,
572
+ );
167
573
  }
168
574
 
169
575
  /**
@@ -172,160 +578,38 @@ export default class SQLite3Driver extends NymphDriver {
172
578
  * @param etype The entity type to create a table for. If this is blank, the default tables are created.
173
579
  */
174
580
  private createTables(etype: string | null = null) {
175
- this.checkReadOnlyMode();
176
581
  this.startTransaction('nymph-tablecreation');
177
582
  try {
178
583
  if (etype != null) {
179
- // Create the entity table.
180
- this.queryRun(
181
- `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
182
- `${this.prefix}entities_${etype}`
183
- )} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL);`
184
- );
185
- this.queryRun(
186
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
187
- `${this.prefix}entities_${etype}_id_cdate`
188
- )} ON ${SQLite3Driver.escape(
189
- `${this.prefix}entities_${etype}`
190
- )} ("cdate");`
191
- );
192
- this.queryRun(
193
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
194
- `${this.prefix}entities_${etype}_id_mdate`
195
- )} ON ${SQLite3Driver.escape(
196
- `${this.prefix}entities_${etype}`
197
- )} ("mdate");`
198
- );
199
- this.queryRun(
200
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
201
- `${this.prefix}entities_${etype}_id_tags`
202
- )} ON ${SQLite3Driver.escape(
203
- `${this.prefix}entities_${etype}`
204
- )} ("tags");`
205
- );
206
- // Create the data table.
207
- this.queryRun(
208
- `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
209
- `${this.prefix}data_${etype}`
210
- )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
211
- `${this.prefix}entities_${etype}`
212
- )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY("guid", "name"));`
213
- );
214
- this.queryRun(
215
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
216
- `${this.prefix}data_${etype}_id_guid`
217
- )} ON ${SQLite3Driver.escape(
218
- `${this.prefix}data_${etype}`
219
- )} ("guid");`
220
- );
221
- this.queryRun(
222
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
223
- `${this.prefix}data_${etype}_id_name`
224
- )} ON ${SQLite3Driver.escape(
225
- `${this.prefix}data_${etype}`
226
- )} ("name");`
227
- );
228
- this.queryRun(
229
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
230
- `${this.prefix}data_${etype}_id_value`
231
- )} ON ${SQLite3Driver.escape(
232
- `${this.prefix}data_${etype}`
233
- )} ("value");`
234
- );
235
- this.queryRun(
236
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
237
- `${this.prefix}data_${etype}_id_guid__name_user`
238
- )} ON ${SQLite3Driver.escape(
239
- `${this.prefix}data_${etype}`
240
- )} ("guid") WHERE "name" = \'user\';`
241
- );
242
- this.queryRun(
243
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
244
- `${this.prefix}data_${etype}_id_guid__name_group`
245
- )} ON ${SQLite3Driver.escape(
246
- `${this.prefix}data_${etype}`
247
- )} ("guid") WHERE "name" = \'group\';`
248
- );
249
- // Create the comparisons table.
250
- this.queryRun(
251
- `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
252
- `${this.prefix}comparisons_${etype}`
253
- )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
254
- `${this.prefix}entities_${etype}`
255
- )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "truthy" INTEGER, "string" TEXT, "number" REAL, PRIMARY KEY("guid", "name"));`
256
- );
257
- this.queryRun(
258
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
259
- `${this.prefix}comparisons_${etype}_id_guid`
260
- )} ON ${SQLite3Driver.escape(
261
- `${this.prefix}comparisons_${etype}`
262
- )} ("guid");`
263
- );
264
- this.queryRun(
265
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
266
- `${this.prefix}comparisons_${etype}_id_name`
267
- )} ON ${SQLite3Driver.escape(
268
- `${this.prefix}comparisons_${etype}`
269
- )} ("name");`
270
- );
271
- this.queryRun(
272
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
273
- `${this.prefix}comparisons_${etype}_id_name__truthy`
274
- )} ON ${SQLite3Driver.escape(
275
- `${this.prefix}comparisons_${etype}`
276
- )} ("name") WHERE "truthy" = 1;`
277
- );
278
- // Create the references table.
279
- this.queryRun(
280
- `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
281
- `${this.prefix}references_${etype}`
282
- )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
283
- `${this.prefix}entities_${etype}`
284
- )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`
285
- );
286
- this.queryRun(
287
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
288
- `${this.prefix}references_${etype}_id_guid`
289
- )} ON ${SQLite3Driver.escape(
290
- `${this.prefix}references_${etype}`
291
- )} ("guid");`
292
- );
293
- this.queryRun(
294
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
295
- `${this.prefix}references_${etype}_id_name`
296
- )} ON ${SQLite3Driver.escape(
297
- `${this.prefix}references_${etype}`
298
- )} ("name");`
299
- );
300
- this.queryRun(
301
- `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
302
- `${this.prefix}references_${etype}_id_reference`
303
- )} ON ${SQLite3Driver.escape(
304
- `${this.prefix}references_${etype}`
305
- )} ("reference");`
306
- );
584
+ this.createEntitiesTable(etype);
585
+ this.createDataTable(etype);
586
+ this.createReferencesTable(etype);
587
+ this.createTokensTable(etype);
588
+ this.createUniquesTable(etype);
307
589
  } else {
308
590
  // Create the UID table.
309
591
  this.queryRun(
310
592
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
311
- `${this.prefix}uids`
312
- )} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`
593
+ `${this.prefix}uids`,
594
+ )} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`,
313
595
  );
314
596
  }
315
- this.commit('nymph-tablecreation');
316
- return true;
317
597
  } catch (e: any) {
318
598
  this.rollback('nymph-tablecreation');
319
599
  throw e;
320
600
  }
601
+
602
+ this.commit('nymph-tablecreation');
603
+ return true;
321
604
  }
322
605
 
323
606
  private query<T extends () => any>(
324
607
  runQuery: T,
325
608
  query: string,
326
- etypes: string[] = []
609
+ etypes: string[] = [],
327
610
  ): ReturnType<T> {
328
611
  try {
612
+ this.nymph.config.debugInfo('sqlite3:query', query);
329
613
  return runQuery();
330
614
  } catch (e: any) {
331
615
  const errorCode = e?.code;
@@ -343,29 +627,39 @@ export default class SQLite3Driver extends NymphDriver {
343
627
  } catch (e2: any) {
344
628
  throw new QueryFailedError(
345
629
  'Query failed: ' + e2?.code + ' - ' + e2?.message,
346
- query
630
+ query,
631
+ e2?.code,
347
632
  );
348
633
  }
634
+ } else if (
635
+ errorCode === 'SQLITE_CONSTRAINT_UNIQUE' &&
636
+ errorMsg.match(/^UNIQUE constraint failed: /)
637
+ ) {
638
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
349
639
  } else {
350
640
  throw new QueryFailedError(
351
641
  'Query failed: ' + e?.code + ' - ' + e?.message,
352
- query
642
+ query,
643
+ e?.code,
353
644
  );
354
645
  }
355
646
  }
356
647
  }
357
648
 
358
- private queryIter(
649
+ private queryArray(
359
650
  query: string,
360
651
  {
361
652
  etypes = [],
362
653
  params = {},
363
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
654
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
364
655
  ) {
365
656
  return this.query(
366
- () => this.store.link.prepare(query).iterate(params),
657
+ () =>
658
+ (this.store.linkWrite || this.store.link)
659
+ .prepare(query)
660
+ .iterate(params),
367
661
  `${query} -- ${JSON.stringify(params)}`,
368
- etypes
662
+ etypes,
369
663
  );
370
664
  }
371
665
 
@@ -374,12 +668,13 @@ export default class SQLite3Driver extends NymphDriver {
374
668
  {
375
669
  etypes = [],
376
670
  params = {},
377
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
671
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
378
672
  ) {
379
673
  return this.query(
380
- () => this.store.link.prepare(query).get(params),
674
+ () =>
675
+ (this.store.linkWrite || this.store.link).prepare(query).get(params),
381
676
  `${query} -- ${JSON.stringify(params)}`,
382
- etypes
677
+ etypes,
383
678
  );
384
679
  }
385
680
 
@@ -388,19 +683,20 @@ export default class SQLite3Driver extends NymphDriver {
388
683
  {
389
684
  etypes = [],
390
685
  params = {},
391
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
686
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
392
687
  ) {
393
688
  return this.query(
394
- () => this.store.link.prepare(query).run(params),
689
+ () =>
690
+ (this.store.linkWrite || this.store.link).prepare(query).run(params),
395
691
  `${query} -- ${JSON.stringify(params)}`,
396
- etypes
692
+ etypes,
397
693
  );
398
694
  }
399
695
 
400
696
  public async commit(name: string) {
401
697
  if (name == null || typeof name !== 'string' || name.length === 0) {
402
698
  throw new InvalidParametersError(
403
- 'Transaction commit attempted without a name.'
699
+ 'Transaction commit attempted without a name.',
404
700
  );
405
701
  }
406
702
  if (this.store.transactionsStarted === 0) {
@@ -408,12 +704,23 @@ export default class SQLite3Driver extends NymphDriver {
408
704
  }
409
705
  this.queryRun(`RELEASE SAVEPOINT ${SQLite3Driver.escape(name)};`);
410
706
  this.store.transactionsStarted--;
707
+
708
+ if (
709
+ this.store.transactionsStarted === 0 &&
710
+ this.store.linkWrite &&
711
+ !this.config.explicitWrite
712
+ ) {
713
+ this.store.linkWrite.exec('PRAGMA optimize;');
714
+ this.store.linkWrite.close();
715
+ this.store.linkWrite = undefined;
716
+ }
717
+
411
718
  return true;
412
719
  }
413
720
 
414
721
  public async deleteEntityByID(
415
722
  guid: string,
416
- className?: EntityConstructor | string | null
723
+ className?: EntityConstructor | string | null,
417
724
  ) {
418
725
  let EntityClass: EntityConstructor;
419
726
  if (typeof className === 'string' || className == null) {
@@ -423,168 +730,408 @@ export default class SQLite3Driver extends NymphDriver {
423
730
  EntityClass = className;
424
731
  }
425
732
  const etype = EntityClass.ETYPE;
426
- this.checkReadOnlyMode();
427
733
  await this.startTransaction('nymph-delete');
428
734
  try {
429
735
  this.queryRun(
430
736
  `DELETE FROM ${SQLite3Driver.escape(
431
- `${this.prefix}entities_${etype}`
737
+ `${this.prefix}entities_${etype}`,
432
738
  )} WHERE "guid"=@guid;`,
433
739
  {
434
740
  etypes: [etype],
435
741
  params: {
436
742
  guid,
437
743
  },
438
- }
744
+ },
439
745
  );
440
746
  this.queryRun(
441
747
  `DELETE FROM ${SQLite3Driver.escape(
442
- `${this.prefix}data_${etype}`
748
+ `${this.prefix}data_${etype}`,
443
749
  )} WHERE "guid"=@guid;`,
444
750
  {
445
751
  etypes: [etype],
446
752
  params: {
447
753
  guid,
448
754
  },
449
- }
755
+ },
450
756
  );
451
757
  this.queryRun(
452
758
  `DELETE FROM ${SQLite3Driver.escape(
453
- `${this.prefix}comparisons_${etype}`
759
+ `${this.prefix}references_${etype}`,
454
760
  )} WHERE "guid"=@guid;`,
455
761
  {
456
762
  etypes: [etype],
457
763
  params: {
458
764
  guid,
459
765
  },
460
- }
766
+ },
461
767
  );
462
768
  this.queryRun(
463
769
  `DELETE FROM ${SQLite3Driver.escape(
464
- `${this.prefix}references_${etype}`
770
+ `${this.prefix}tokens_${etype}`,
465
771
  )} WHERE "guid"=@guid;`,
466
772
  {
467
773
  etypes: [etype],
468
774
  params: {
469
775
  guid,
470
776
  },
471
- }
777
+ },
778
+ );
779
+ this.queryRun(
780
+ `DELETE FROM ${SQLite3Driver.escape(
781
+ `${this.prefix}uniques_${etype}`,
782
+ )} WHERE "guid"=@guid;`,
783
+ {
784
+ etypes: [etype],
785
+ params: {
786
+ guid,
787
+ },
788
+ },
472
789
  );
473
- await this.commit('nymph-delete');
474
- // Remove any cached versions of this entity.
475
- if (this.nymph.config.cache) {
476
- this.cleanCache(guid);
477
- }
478
- return true;
479
790
  } catch (e: any) {
791
+ this.nymph.config.debugError('sqlite3', `Delete entity error: "${e}"`);
480
792
  await this.rollback('nymph-delete');
481
793
  throw e;
482
794
  }
795
+
796
+ await this.commit('nymph-delete');
797
+ // Remove any cached versions of this entity.
798
+ if (this.nymph.config.cache) {
799
+ this.cleanCache(guid);
800
+ }
801
+ return true;
483
802
  }
484
803
 
485
804
  public async deleteUID(name: string) {
486
805
  if (!name) {
487
806
  throw new InvalidParametersError('Name not given for UID');
488
807
  }
489
- this.checkReadOnlyMode();
808
+ await this.startTransaction('nymph-delete-uid');
490
809
  this.queryRun(
491
810
  `DELETE FROM ${SQLite3Driver.escape(
492
- `${this.prefix}uids`
811
+ `${this.prefix}uids`,
493
812
  )} WHERE "name"=@name;`,
494
813
  {
495
814
  params: {
496
815
  name,
497
816
  },
498
- }
817
+ },
499
818
  );
819
+ await this.commit('nymph-delete-uid');
500
820
  return true;
501
821
  }
502
822
 
503
- protected async exportEntities(writeLine: (line: string) => void) {
504
- writeLine('#nex2');
505
- writeLine('# Nymph Entity Exchange v2');
506
- writeLine('# http://nymph.io');
507
- writeLine('#');
508
- writeLine('# Generation Time: ' + new Date().toLocaleString());
509
- writeLine('');
510
-
511
- writeLine('#');
512
- writeLine('# UIDs');
513
- writeLine('#');
514
- writeLine('');
515
-
516
- // Export UIDs.
517
- let uids = this.queryIter(
518
- `SELECT * FROM ${SQLite3Driver.escape(
519
- `${this.prefix}uids`
520
- )} ORDER BY "name";`
521
- );
522
- for (const uid of uids) {
523
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
524
- }
525
-
526
- writeLine('');
527
- writeLine('#');
528
- writeLine('# Entities');
529
- writeLine('#');
530
- writeLine('');
531
-
532
- // Get the etypes.
533
- const tables = this.queryIter(
534
- "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;"
535
- );
536
- const etypes = [];
537
- for (const table of tables) {
538
- if (table.name.startsWith(this.prefix + 'entities_')) {
539
- etypes.push(table.name.substr((this.prefix + 'entities_').length));
540
- }
541
- }
823
+ public async getIndexes(etype: string) {
824
+ const indexes: {
825
+ scope: 'data' | 'references' | 'tokens';
826
+ name: string;
827
+ property: string;
828
+ }[] = [];
542
829
 
543
- for (const etype of etypes) {
544
- // Export entities.
545
- const dataIterator = this.queryIter(
546
- `SELECT e.*, d."name" AS "dname", d."value" AS "dvalue", c."string", c."number" FROM ${SQLite3Driver.escape(
547
- `${this.prefix}entities_${etype}`
548
- )} e LEFT JOIN ${SQLite3Driver.escape(
549
- `${this.prefix}data_${etype}`
550
- )} d USING ("guid") INNER JOIN ${SQLite3Driver.escape(
551
- `${this.prefix}comparisons_${etype}`
552
- )} c USING ("guid", "name") ORDER BY e."guid";`
553
- )[Symbol.iterator]();
554
- let datum = dataIterator.next();
555
- while (!datum.done) {
556
- const guid = datum.value.guid;
557
- const tags = datum.value.tags.slice(1, -1);
558
- const cdate = datum.value.cdate;
559
- const mdate = datum.value.mdate;
560
- writeLine(`{${guid}}<${etype}>[${tags}]`);
561
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
562
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
563
- if (datum.value.dname != null) {
564
- // This do will keep going and adding the data until the
565
- // next entity is reached. datum will end on the next entity.
566
- do {
567
- const value =
568
- datum.value.dvalue === 'N'
569
- ? JSON.stringify(datum.value.number)
570
- : datum.value.dvalue === 'S'
571
- ? JSON.stringify(datum.value.string)
572
- : datum.value.dvalue;
573
- writeLine(`\t${datum.value.dname}=${value}`);
574
- datum = dataIterator.next();
575
- } while (!datum.done && datum.value.guid === guid);
576
- } else {
577
- // Make sure that datum is incremented :)
578
- datum = dataIterator.next();
579
- }
830
+ for (let [scope, suffix] of [
831
+ ['data', '_json'],
832
+ ['references', '_reference_guid'],
833
+ ['tokens', '_token_position_stem'],
834
+ ] as (
835
+ | ['data', '_json']
836
+ | ['references', '_reference_guid']
837
+ | ['tokens', '_token_position_stem']
838
+ )[]) {
839
+ const indexDefinitions: IterableIterator<any> = this.queryArray(
840
+ `SELECT "name", "sql" FROM "sqlite_master" WHERE "type"='index' AND "name" LIKE @pattern;`,
841
+ {
842
+ params: {
843
+ pattern: `${this.prefix}${scope}_${etype}_id_custom_%${suffix}`,
844
+ },
845
+ },
846
+ );
847
+ for (const indexDefinition of indexDefinitions) {
848
+ indexes.push({
849
+ scope,
850
+ name: (indexDefinition.name as string).substring(
851
+ `${this.prefix}${scope}_${etype}_id_custom_`.length,
852
+ indexDefinition.name.length - suffix.length,
853
+ ),
854
+ property:
855
+ ((indexDefinition.sql as string).match(
856
+ /WHERE\s+"name"\s*=\s*'(.*)'/,
857
+ ) ?? [])[1] ?? '',
858
+ });
580
859
  }
581
860
  }
582
861
 
583
- return;
862
+ return indexes;
584
863
  }
585
864
 
586
- /**
587
- * Generate the SQLite3 query.
865
+ public async addIndex(
866
+ etype: string,
867
+ definition: {
868
+ scope: 'data' | 'references' | 'tokens';
869
+ name: string;
870
+ property: string;
871
+ },
872
+ ) {
873
+ this.checkIndexName(definition.name);
874
+ await this.deleteIndex(etype, definition.scope, definition.name);
875
+ if (definition.scope === 'data') {
876
+ this.queryRun(
877
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
878
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_json`,
879
+ )} ON ${SQLite3Driver.escape(
880
+ `${this.prefix}data_${etype}`,
881
+ )} ("json") WHERE "name"=${SQLite3Driver.escapeValue(definition.property)};`,
882
+ );
883
+ this.queryRun(
884
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
885
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_string`,
886
+ )} ON ${SQLite3Driver.escape(
887
+ `${this.prefix}data_${etype}`,
888
+ )} ("string") WHERE "name"=${SQLite3Driver.escapeValue(definition.property)};`,
889
+ );
890
+ this.queryRun(
891
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
892
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_number`,
893
+ )} ON ${SQLite3Driver.escape(
894
+ `${this.prefix}data_${etype}`,
895
+ )} ("number") WHERE "name"=${SQLite3Driver.escapeValue(definition.property)};`,
896
+ );
897
+ this.queryRun(
898
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
899
+ `${this.prefix}data_${etype}_id_custom_${definition.name}_truthy`,
900
+ )} ON ${SQLite3Driver.escape(
901
+ `${this.prefix}data_${etype}`,
902
+ )} ("truthy") WHERE "name"=${SQLite3Driver.escapeValue(definition.property)};`,
903
+ );
904
+ } else if (definition.scope === 'references') {
905
+ this.queryRun(
906
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
907
+ `${this.prefix}references_${etype}_id_custom_${definition.name}_reference_guid`,
908
+ )} ON ${SQLite3Driver.escape(
909
+ `${this.prefix}references_${etype}`,
910
+ )} ("reference", "guid") WHERE "name"=${SQLite3Driver.escapeValue(definition.property)};`,
911
+ );
912
+ } else if (definition.scope === 'tokens') {
913
+ this.queryRun(
914
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
915
+ `${this.prefix}tokens_${etype}_id_custom_${definition.name}_token_position_stem`,
916
+ )} ON ${SQLite3Driver.escape(
917
+ `${this.prefix}tokens_${etype}`,
918
+ )} ("token", "position", "stem") WHERE "name"=${SQLite3Driver.escapeValue(definition.property)};`,
919
+ );
920
+ }
921
+ return true;
922
+ }
923
+
924
+ public async deleteIndex(
925
+ etype: string,
926
+ scope: 'data' | 'references' | 'tokens',
927
+ name: string,
928
+ ) {
929
+ this.checkIndexName(name);
930
+ if (scope === 'data') {
931
+ this.queryRun(
932
+ `DROP INDEX IF EXISTS ${SQLite3Driver.escape(
933
+ `${this.prefix}data_${etype}_id_custom_${name}_json`,
934
+ )};`,
935
+ );
936
+ this.queryRun(
937
+ `DROP INDEX IF EXISTS ${SQLite3Driver.escape(
938
+ `${this.prefix}data_${etype}_id_custom_${name}_string`,
939
+ )};`,
940
+ );
941
+ this.queryRun(
942
+ `DROP INDEX IF EXISTS ${SQLite3Driver.escape(
943
+ `${this.prefix}data_${etype}_id_custom_${name}_number`,
944
+ )};`,
945
+ );
946
+ this.queryRun(
947
+ `DROP INDEX IF EXISTS ${SQLite3Driver.escape(
948
+ `${this.prefix}data_${etype}_id_custom_${name}_truthy`,
949
+ )};`,
950
+ );
951
+ } else if (scope === 'references') {
952
+ this.queryRun(
953
+ `DROP INDEX IF EXISTS ${SQLite3Driver.escape(
954
+ `${this.prefix}references_${etype}_id_custom_${name}_reference_guid`,
955
+ )};`,
956
+ );
957
+ } else if (scope === 'tokens') {
958
+ this.queryRun(
959
+ `DROP INDEX IF EXISTS ${SQLite3Driver.escape(
960
+ `${this.prefix}tokens_${etype}_id_custom_${name}_token_position_stem`,
961
+ )};`,
962
+ );
963
+ }
964
+ return true;
965
+ }
966
+
967
+ public async getEtypes() {
968
+ const tables: IterableIterator<any> = this.queryArray(
969
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @prefix;",
970
+ {
971
+ params: {
972
+ prefix: this.prefix + 'entities_' + '%',
973
+ },
974
+ },
975
+ );
976
+ const etypes: string[] = [];
977
+ for (const table of tables) {
978
+ etypes.push(table.name.substr((this.prefix + 'entities_').length));
979
+ }
980
+
981
+ return etypes;
982
+ }
983
+
984
+ public async *exportDataIterator(): AsyncGenerator<
985
+ { type: 'comment' | 'uid' | 'entity'; content: string },
986
+ void,
987
+ false | undefined
988
+ > {
989
+ if (
990
+ yield {
991
+ type: 'comment',
992
+ content: `#nex2
993
+ # Nymph Entity Exchange v2
994
+ # http://nymph.io
995
+ #
996
+ # Generation Time: ${new Date().toLocaleString()}
997
+ `,
998
+ }
999
+ ) {
1000
+ return;
1001
+ }
1002
+
1003
+ if (
1004
+ yield {
1005
+ type: 'comment',
1006
+ content: `
1007
+
1008
+ #
1009
+ # UIDs
1010
+ #
1011
+
1012
+ `,
1013
+ }
1014
+ ) {
1015
+ return;
1016
+ }
1017
+
1018
+ // Export UIDs.
1019
+ let uids: IterableIterator<any> = this.queryArray(
1020
+ `SELECT * FROM ${SQLite3Driver.escape(
1021
+ `${this.prefix}uids`,
1022
+ )} ORDER BY "name";`,
1023
+ );
1024
+ for (const uid of uids) {
1025
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
1026
+ return;
1027
+ }
1028
+ }
1029
+
1030
+ if (
1031
+ yield {
1032
+ type: 'comment',
1033
+ content: `
1034
+
1035
+ #
1036
+ # Entities
1037
+ #
1038
+
1039
+ `,
1040
+ }
1041
+ ) {
1042
+ return;
1043
+ }
1044
+
1045
+ // Get the etypes.
1046
+ const etypes = await this.getEtypes();
1047
+
1048
+ for (const etype of etypes) {
1049
+ // Export entities.
1050
+ const dataIterator: IterableIterator<any> = this.queryArray(
1051
+ `SELECT e."guid", e."tags", e."cdate", e."mdate", e."user", e."group", e."acUser", e."acGroup", e."acOther", e."acRead", e."acWrite", e."acFull", d."name", d."value", json(d."json") as "json", d."string", d."number" FROM ${SQLite3Driver.escape(
1052
+ `${this.prefix}entities_${etype}`,
1053
+ )} e LEFT JOIN ${SQLite3Driver.escape(
1054
+ `${this.prefix}data_${etype}`,
1055
+ )} d USING ("guid") ORDER BY e."guid";`,
1056
+ )[Symbol.iterator]();
1057
+ let datum = dataIterator.next();
1058
+ while (!datum.done) {
1059
+ const guid = datum.value.guid;
1060
+ const tags = datum.value.tags.slice(1, -1);
1061
+ const cdate = datum.value.cdate;
1062
+ const mdate = datum.value.mdate;
1063
+ const user = datum.value.user;
1064
+ const group = datum.value.group;
1065
+ const acUser = datum.value.acUser;
1066
+ const acGroup = datum.value.acGroup;
1067
+ const acOther = datum.value.acOther;
1068
+ const acRead = datum.value.acRead?.slice(1, -1).split(',');
1069
+ const acWrite = datum.value.acWrite?.slice(1, -1).split(',');
1070
+ const acFull = datum.value.acFull?.slice(1, -1).split(',');
1071
+ let currentEntityExport: string[] = [];
1072
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
1073
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
1074
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
1075
+ if (this.nymph.tilmeld != null) {
1076
+ if (user != null) {
1077
+ currentEntityExport.push(
1078
+ `\tuser=${JSON.stringify(['nymph_entity_reference', user, 'User'])}`,
1079
+ );
1080
+ }
1081
+ if (group != null) {
1082
+ currentEntityExport.push(
1083
+ `\tgroup=${JSON.stringify(['nymph_entity_reference', group, 'Group'])}`,
1084
+ );
1085
+ }
1086
+ if (acUser != null) {
1087
+ currentEntityExport.push(`\tacUser=${JSON.stringify(acUser)}`);
1088
+ }
1089
+ if (acGroup != null) {
1090
+ currentEntityExport.push(`\tacGroup=${JSON.stringify(acGroup)}`);
1091
+ }
1092
+ if (acOther != null) {
1093
+ currentEntityExport.push(`\tacOther=${JSON.stringify(acOther)}`);
1094
+ }
1095
+ if (acRead != null) {
1096
+ currentEntityExport.push(`\tacRead=${JSON.stringify(acRead)}`);
1097
+ }
1098
+ if (acWrite != null) {
1099
+ currentEntityExport.push(`\tacWrite=${JSON.stringify(acWrite)}`);
1100
+ }
1101
+ if (acFull != null) {
1102
+ currentEntityExport.push(`\tacFull=${JSON.stringify(acFull)}`);
1103
+ }
1104
+ }
1105
+ if (datum.value.name != null) {
1106
+ // This do will keep going and adding the data until the
1107
+ // next entity is reached. datum will end on the next entity.
1108
+ do {
1109
+ const value =
1110
+ datum.value.value === 'N'
1111
+ ? JSON.stringify(datum.value.number)
1112
+ : datum.value.value === 'S'
1113
+ ? JSON.stringify(datum.value.string)
1114
+ : datum.value.value === 'J'
1115
+ ? datum.value.json
1116
+ : datum.value.value;
1117
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
1118
+ datum = dataIterator.next();
1119
+ } while (!datum.done && datum.value.guid === guid);
1120
+ } else {
1121
+ // Make sure that datum is incremented :)
1122
+ datum = dataIterator.next();
1123
+ }
1124
+ currentEntityExport.push('');
1125
+
1126
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
1127
+ return;
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ /**
1134
+ * Generate the SQLite3 query.
588
1135
  * @param options The options array.
589
1136
  * @param formattedSelectors The formatted selector array.
590
1137
  * @param etype
@@ -601,20 +1148,21 @@ export default class SQLite3Driver extends NymphDriver {
601
1148
  params: { [k: string]: any } = {},
602
1149
  subquery = false,
603
1150
  tableSuffix = '',
604
- etypes: string[] = []
1151
+ etypes: string[] = [],
1152
+ guidSelector: string | undefined = undefined,
605
1153
  ) {
606
1154
  if (typeof options.class?.alterOptions === 'function') {
607
1155
  options = options.class.alterOptions(options);
608
1156
  }
609
1157
  const eTable = `e${tableSuffix}`;
610
1158
  const dTable = `d${tableSuffix}`;
611
- const cTable = `c${tableSuffix}`;
612
1159
  const fTable = `f${tableSuffix}`;
613
1160
  const ieTable = `ie${tableSuffix}`;
614
- const sort = options.sort ?? 'cdate';
1161
+ const sTable = `s${tableSuffix}`;
1162
+ const sort = options.sort === undefined ? 'cdate' : options.sort;
615
1163
  const queryParts = this.iterateSelectorsForQuery(
616
1164
  formattedSelectors,
617
- (key, value, typeIsOr, typeIsNot) => {
1165
+ ({ key, value, typeIsOr, typeIsNot }) => {
618
1166
  const clauseNot = key.startsWith('!');
619
1167
  let curQuery = '';
620
1168
  for (const curValue of value) {
@@ -662,17 +1210,39 @@ export default class SQLite3Driver extends NymphDriver {
662
1210
  if (curQuery) {
663
1211
  curQuery += typeIsOr ? ' OR ' : ' AND ';
664
1212
  }
665
- const name = `param${++count.i}`;
666
- curQuery +=
667
- ieTable +
668
- '."guid" ' +
669
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
670
- 'IN (SELECT "guid" FROM ' +
671
- SQLite3Driver.escape(this.prefix + 'data_' + etype) +
672
- ' WHERE "name"=@' +
673
- name +
674
- ')';
675
- params[name] = curVar;
1213
+ if (
1214
+ curVar === 'cdate' ||
1215
+ curVar === 'mdate' ||
1216
+ (this.nymph.tilmeld != null &&
1217
+ (curVar === 'user' ||
1218
+ curVar === 'group' ||
1219
+ curVar === 'acUser' ||
1220
+ curVar === 'acGroup' ||
1221
+ curVar === 'acOther' ||
1222
+ curVar === 'acRead' ||
1223
+ curVar === 'acWrite' ||
1224
+ curVar === 'acFull'))
1225
+ ) {
1226
+ curQuery +=
1227
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1228
+ '(' +
1229
+ ieTable +
1230
+ '.' +
1231
+ SQLite3Driver.escape(curVar) +
1232
+ ' IS NOT NULL)';
1233
+ } else {
1234
+ const name = `param${++count.i}`;
1235
+ curQuery +=
1236
+ ieTable +
1237
+ '."guid" ' +
1238
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1239
+ 'IN (SELECT "guid" FROM ' +
1240
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1241
+ ' WHERE "name"=@' +
1242
+ name +
1243
+ ')';
1244
+ params[name] = curVar;
1245
+ }
676
1246
  }
677
1247
  break;
678
1248
  case 'truthy':
@@ -681,28 +1251,35 @@ export default class SQLite3Driver extends NymphDriver {
681
1251
  if (curQuery) {
682
1252
  curQuery += typeIsOr ? ' OR ' : ' AND ';
683
1253
  }
684
- if (curVar === 'cdate') {
685
- curQuery +=
686
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
687
- '(' +
688
- ieTable +
689
- '."cdate" NOT NULL)';
690
- break;
691
- } else if (curVar === 'mdate') {
1254
+ if (
1255
+ curVar === 'cdate' ||
1256
+ curVar === 'mdate' ||
1257
+ (this.nymph.tilmeld != null &&
1258
+ (curVar === 'user' ||
1259
+ curVar === 'group' ||
1260
+ curVar === 'acUser' ||
1261
+ curVar === 'acGroup' ||
1262
+ curVar === 'acOther' ||
1263
+ curVar === 'acRead' ||
1264
+ curVar === 'acWrite' ||
1265
+ curVar === 'acFull'))
1266
+ ) {
692
1267
  curQuery +=
693
1268
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
694
1269
  '(' +
695
1270
  ieTable +
696
- '."mdate" NOT NULL)';
697
- break;
1271
+ '.' +
1272
+ SQLite3Driver.escape(curVar) +
1273
+ ' IS NOT NULL)';
698
1274
  } else {
699
1275
  const name = `param${++count.i}`;
700
1276
  curQuery +=
701
1277
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1278
+ 'EXISTS (SELECT "guid" FROM ' +
1279
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1280
+ ' WHERE "guid"=' +
702
1281
  ieTable +
703
- '."guid" IN (SELECT "guid" FROM ' +
704
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
705
- ' WHERE "name"=@' +
1282
+ '."guid" AND "name"=@' +
706
1283
  name +
707
1284
  ' AND "truthy"=1)';
708
1285
  params[name] = curVar;
@@ -711,30 +1288,62 @@ export default class SQLite3Driver extends NymphDriver {
711
1288
  break;
712
1289
  case 'equal':
713
1290
  case '!equal':
714
- if (curValue[0] === 'cdate') {
1291
+ if (
1292
+ curValue[0] === 'cdate' ||
1293
+ curValue[0] === 'mdate' ||
1294
+ (this.nymph.tilmeld != null &&
1295
+ (curValue[0] === 'acUser' ||
1296
+ curValue[0] === 'acGroup' ||
1297
+ curValue[0] === 'acOther'))
1298
+ ) {
1299
+ if (curQuery) {
1300
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1301
+ }
1302
+ const value = `param${++count.i}`;
1303
+ curQuery +=
1304
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1305
+ ieTable +
1306
+ '.' +
1307
+ SQLite3Driver.escape(curValue[0]) +
1308
+ '=@' +
1309
+ value;
1310
+ params[value] = Number(curValue[1]);
1311
+ } else if (
1312
+ this.nymph.tilmeld != null &&
1313
+ (curValue[0] === 'user' || curValue[0] === 'group')
1314
+ ) {
715
1315
  if (curQuery) {
716
1316
  curQuery += typeIsOr ? ' OR ' : ' AND ';
717
1317
  }
718
- const cdate = `param${++count.i}`;
1318
+ const value = `param${++count.i}`;
719
1319
  curQuery +=
720
1320
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
721
1321
  ieTable +
722
- '."cdate"=@' +
723
- cdate;
724
- params[cdate] = Number(curValue[1]);
725
- break;
726
- } else if (curValue[0] === 'mdate') {
1322
+ '.' +
1323
+ SQLite3Driver.escape(curValue[0]) +
1324
+ '=@' +
1325
+ value;
1326
+ params[value] = `${curValue[1]}`;
1327
+ } else if (
1328
+ this.nymph.tilmeld != null &&
1329
+ (curValue[0] === 'acRead' ||
1330
+ curValue[0] === 'acWrite' ||
1331
+ curValue[0] === 'acFull')
1332
+ ) {
727
1333
  if (curQuery) {
728
1334
  curQuery += typeIsOr ? ' OR ' : ' AND ';
729
1335
  }
730
- const mdate = `param${++count.i}`;
1336
+ const value = `param${++count.i}`;
731
1337
  curQuery +=
732
1338
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
733
1339
  ieTable +
734
- '."mdate"=@' +
735
- mdate;
736
- params[mdate] = Number(curValue[1]);
737
- break;
1340
+ '.' +
1341
+ SQLite3Driver.escape(curValue[0]) +
1342
+ '=@' +
1343
+ value;
1344
+ params[value] = Array.isArray(curValue[1])
1345
+ ? ',' + curValue[1].join(',') + ','
1346
+ : '';
738
1347
  } else if (typeof curValue[1] === 'number') {
739
1348
  if (curQuery) {
740
1349
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -743,10 +1352,11 @@ export default class SQLite3Driver extends NymphDriver {
743
1352
  const value = `param${++count.i}`;
744
1353
  curQuery +=
745
1354
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1355
+ 'EXISTS (SELECT "guid" FROM ' +
1356
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1357
+ ' WHERE "guid"=' +
746
1358
  ieTable +
747
- '."guid" IN (SELECT "guid" FROM ' +
748
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
749
- ' WHERE "name"=@' +
1359
+ '."guid" AND "name"=@' +
750
1360
  name +
751
1361
  ' AND "number"=@' +
752
1362
  value +
@@ -761,10 +1371,11 @@ export default class SQLite3Driver extends NymphDriver {
761
1371
  const value = `param${++count.i}`;
762
1372
  curQuery +=
763
1373
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1374
+ 'EXISTS (SELECT "guid" FROM ' +
1375
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1376
+ ' WHERE "guid"=' +
764
1377
  ieTable +
765
- '."guid" IN (SELECT "guid" FROM ' +
766
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
767
- ' WHERE "name"=@' +
1378
+ '."guid" AND "name"=@' +
768
1379
  name +
769
1380
  ' AND "string"=@' +
770
1381
  value +
@@ -788,130 +1399,302 @@ export default class SQLite3Driver extends NymphDriver {
788
1399
  const value = `param${++count.i}`;
789
1400
  curQuery +=
790
1401
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
791
- ieTable +
792
- '."guid" IN (SELECT "guid" FROM ' +
1402
+ 'EXISTS (SELECT "guid" FROM ' +
793
1403
  SQLite3Driver.escape(this.prefix + 'data_' + etype) +
794
- ' WHERE "name"=@' +
1404
+ ' WHERE "guid"=' +
1405
+ ieTable +
1406
+ '."guid" AND "name"=@' +
795
1407
  name +
796
- ' AND "value"=@' +
1408
+ ' AND "json"=jsonb(@' +
797
1409
  value +
798
- ')';
1410
+ '))';
799
1411
  params[name] = curValue[0];
800
1412
  params[value] = svalue;
801
1413
  }
802
1414
  break;
803
1415
  case 'contain':
804
1416
  case '!contain':
805
- if (curValue[0] === 'cdate') {
1417
+ if (
1418
+ curValue[0] === 'cdate' ||
1419
+ curValue[0] === 'mdate' ||
1420
+ (this.nymph.tilmeld != null &&
1421
+ (curValue[0] === 'acUser' ||
1422
+ curValue[0] === 'acGroup' ||
1423
+ curValue[0] === 'acOther'))
1424
+ ) {
1425
+ if (curQuery) {
1426
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1427
+ }
1428
+ const value = `param${++count.i}`;
1429
+ curQuery +=
1430
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1431
+ ieTable +
1432
+ '.' +
1433
+ SQLite3Driver.escape(curValue[0]) +
1434
+ '=@' +
1435
+ value;
1436
+ params[value] = Number(curValue[1]);
1437
+ } else if (
1438
+ this.nymph.tilmeld != null &&
1439
+ (curValue[0] === 'user' || curValue[0] === 'group')
1440
+ ) {
806
1441
  if (curQuery) {
807
1442
  curQuery += typeIsOr ? ' OR ' : ' AND ';
808
1443
  }
809
- const cdate = `param${++count.i}`;
1444
+ const value = `param${++count.i}`;
810
1445
  curQuery +=
811
1446
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
812
1447
  ieTable +
813
- '."cdate"=' +
814
- cdate;
815
- params[cdate] = Number(curValue[1]);
816
- break;
817
- } else if (curValue[0] === 'mdate') {
1448
+ '.' +
1449
+ SQLite3Driver.escape(curValue[0]) +
1450
+ '=@' +
1451
+ value;
1452
+ params[value] = `${curValue[1]}`;
1453
+ } else if (
1454
+ this.nymph.tilmeld != null &&
1455
+ (curValue[0] === 'acRead' ||
1456
+ curValue[0] === 'acWrite' ||
1457
+ curValue[0] === 'acFull')
1458
+ ) {
818
1459
  if (curQuery) {
819
1460
  curQuery += typeIsOr ? ' OR ' : ' AND ';
820
1461
  }
821
- const mdate = `param${++count.i}`;
1462
+ const id = `param${++count.i}`;
822
1463
  curQuery +=
823
1464
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
824
1465
  ieTable +
825
- '."mdate"=' +
826
- mdate;
827
- params[mdate] = Number(curValue[1]);
828
- break;
1466
+ '.' +
1467
+ SQLite3Driver.escape(curValue[0]) +
1468
+ ' LIKE @' +
1469
+ id +
1470
+ " ESCAPE '\\'";
1471
+ params[id] =
1472
+ '%,' +
1473
+ curValue[1]
1474
+ .replace('\\', '\\\\')
1475
+ .replace('%', '\\%')
1476
+ .replace('_', '\\_') +
1477
+ ',%';
829
1478
  } else {
1479
+ const containTableSuffix = makeTableSuffix();
830
1480
  if (curQuery) {
831
1481
  curQuery += typeIsOr ? ' OR ' : ' AND ';
832
1482
  }
833
1483
  let svalue: string;
834
- let stringValue: string;
835
1484
  if (
836
1485
  curValue[1] instanceof Object &&
837
1486
  typeof curValue[1].toReference === 'function'
838
1487
  ) {
839
1488
  svalue = JSON.stringify(curValue[1].toReference());
840
- stringValue = `${curValue[1].toReference()}`;
841
1489
  } else {
842
1490
  svalue = JSON.stringify(curValue[1]);
843
- stringValue = `${curValue[1]}`;
844
1491
  }
845
1492
  const name = `param${++count.i}`;
846
1493
  const value = `param${++count.i}`;
847
- if (typeof curValue[1] === 'string') {
848
- const stringParam = `param${++count.i}`;
849
- curQuery +=
850
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
851
- '(' +
852
- ieTable +
853
- '."guid" IN (SELECT "guid" FROM ' +
854
- SQLite3Driver.escape(this.prefix + 'data_' + etype) +
855
- ' WHERE "name"=@' +
856
- name +
857
- ' AND instr("value", @' +
858
- value +
859
- ')) OR ' +
860
- ieTable +
861
- '."guid" IN (SELECT "guid" FROM ' +
862
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
863
- ' WHERE "name"=@' +
864
- name +
865
- ' AND "string"=@' +
866
- stringParam +
867
- '))';
868
- params[stringParam] = stringValue;
1494
+ curQuery +=
1495
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1496
+ 'EXISTS (SELECT "guid" FROM ' +
1497
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1498
+ ' d' +
1499
+ containTableSuffix +
1500
+ ' WHERE "guid"=' +
1501
+ ieTable +
1502
+ '."guid" AND "name"=@' +
1503
+ name +
1504
+ ' AND json(@' +
1505
+ value +
1506
+ ') IN (SELECT json_quote("value") FROM json_each(d' +
1507
+ containTableSuffix +
1508
+ '."json")))';
1509
+ params[name] = curValue[0];
1510
+ params[value] = svalue;
1511
+ }
1512
+ break;
1513
+ case 'search':
1514
+ case '!search':
1515
+ if (
1516
+ curValue[0] === 'cdate' ||
1517
+ curValue[0] === 'mdate' ||
1518
+ (this.nymph.tilmeld != null &&
1519
+ (curValue[0] === 'user' ||
1520
+ curValue[0] === 'group' ||
1521
+ curValue[0] === 'acUser' ||
1522
+ curValue[0] === 'acGroup' ||
1523
+ curValue[0] === 'acOther' ||
1524
+ curValue[0] === 'acRead' ||
1525
+ curValue[0] === 'acWrite' ||
1526
+ curValue[0] === 'acFull'))
1527
+ ) {
1528
+ if (curQuery) {
1529
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1530
+ }
1531
+ curQuery += (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(0)';
1532
+ } else {
1533
+ if (curQuery) {
1534
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1535
+ }
1536
+
1537
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(
1538
+ curValue[1],
1539
+ );
1540
+
1541
+ if (!parsedFTSQuery.length) {
1542
+ curQuery += (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(0)';
869
1543
  } else {
1544
+ const name = `param${++count.i}`;
1545
+
1546
+ const queryPartToken = (term: SearchTerm) => {
1547
+ const value = `param${++count.i}`;
1548
+ params[value] = term.token;
1549
+ return (
1550
+ 'EXISTS (SELECT "guid" FROM ' +
1551
+ SQLite3Driver.escape(this.prefix + 'tokens_' + etype) +
1552
+ ' WHERE "guid"=' +
1553
+ ieTable +
1554
+ '."guid" AND "name"=@' +
1555
+ name +
1556
+ ' AND "token"=@' +
1557
+ value +
1558
+ (term.nostemmed ? ' AND "stem"=0' : '') +
1559
+ ')'
1560
+ );
1561
+ };
1562
+
1563
+ const queryPartSeries = (series: SearchSeriesTerm) => {
1564
+ const tokenTableSuffix = makeTableSuffix();
1565
+ const tokenParts = series.tokens.map((token, i) => {
1566
+ const value = `param${++count.i}`;
1567
+ params[value] = token.token;
1568
+ return {
1569
+ fromClause:
1570
+ i === 0
1571
+ ? 'FROM ' +
1572
+ SQLite3Driver.escape(
1573
+ this.prefix + 'tokens_' + etype,
1574
+ ) +
1575
+ ' t' +
1576
+ tokenTableSuffix +
1577
+ '0'
1578
+ : 'JOIN ' +
1579
+ SQLite3Driver.escape(
1580
+ this.prefix + 'tokens_' + etype,
1581
+ ) +
1582
+ ' t' +
1583
+ tokenTableSuffix +
1584
+ i +
1585
+ ' ON t' +
1586
+ tokenTableSuffix +
1587
+ i +
1588
+ '."guid" = t' +
1589
+ tokenTableSuffix +
1590
+ '0."guid" AND t' +
1591
+ tokenTableSuffix +
1592
+ i +
1593
+ '."name" = t' +
1594
+ tokenTableSuffix +
1595
+ '0."name" AND t' +
1596
+ tokenTableSuffix +
1597
+ i +
1598
+ '."position" = t' +
1599
+ tokenTableSuffix +
1600
+ '0."position" + ' +
1601
+ i,
1602
+ whereClause:
1603
+ 't' +
1604
+ tokenTableSuffix +
1605
+ i +
1606
+ '."token"=@' +
1607
+ value +
1608
+ (token.nostemmed
1609
+ ? ' AND t' + tokenTableSuffix + i + '."stem"=0'
1610
+ : ''),
1611
+ };
1612
+ });
1613
+ return (
1614
+ 'EXISTS (SELECT t' +
1615
+ tokenTableSuffix +
1616
+ '0."guid" ' +
1617
+ tokenParts.map((part) => part.fromClause).join(' ') +
1618
+ ' WHERE t' +
1619
+ tokenTableSuffix +
1620
+ '0."guid"=' +
1621
+ ieTable +
1622
+ '."guid" AND t' +
1623
+ tokenTableSuffix +
1624
+ '0."name"=@' +
1625
+ name +
1626
+ ' AND ' +
1627
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
1628
+ ')'
1629
+ );
1630
+ };
1631
+
1632
+ const queryPartTerm = (
1633
+ term:
1634
+ | SearchTerm
1635
+ | SearchOrTerm
1636
+ | SearchNotTerm
1637
+ | SearchSeriesTerm,
1638
+ ): string => {
1639
+ if (term.type === 'series') {
1640
+ return queryPartSeries(term);
1641
+ } else if (term.type === 'not') {
1642
+ return 'NOT ' + queryPartTerm(term.operand);
1643
+ } else if (term.type === 'or') {
1644
+ let queryParts: string[] = [];
1645
+ for (let operand of term.operands) {
1646
+ queryParts.push(queryPartTerm(operand));
1647
+ }
1648
+ return '(' + queryParts.join(' OR ') + ')';
1649
+ }
1650
+ return queryPartToken(term);
1651
+ };
1652
+
1653
+ // Run through the query and add terms.
1654
+ let termStrings: string[] = [];
1655
+ for (let term of parsedFTSQuery) {
1656
+ termStrings.push(queryPartTerm(term));
1657
+ }
1658
+
870
1659
  curQuery +=
871
1660
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
872
- ieTable +
873
- '."guid" IN (SELECT "guid" FROM ' +
874
- SQLite3Driver.escape(this.prefix + 'data_' + etype) +
875
- ' WHERE "name"=@' +
876
- name +
877
- ' AND instr("value", @' +
878
- value +
879
- '))';
1661
+ '(' +
1662
+ termStrings.join(' AND ') +
1663
+ ')';
1664
+
1665
+ params[name] = curValue[0];
880
1666
  }
881
- params[name] = curValue[0];
882
- params[value] = svalue;
883
1667
  }
884
1668
  break;
885
1669
  case 'match':
886
1670
  case '!match':
887
- if (curValue[0] === 'cdate') {
888
- if (curQuery) {
889
- curQuery += typeIsOr ? ' OR ' : ' AND ';
890
- }
891
- const cdate = `param${++count.i}`;
892
- curQuery +=
893
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
894
- '(' +
895
- ieTable +
896
- '."cdate" REGEXP @' +
897
- cdate +
898
- ')';
899
- params[cdate] = curValue[1];
900
- break;
901
- } else if (curValue[0] === 'mdate') {
1671
+ if (
1672
+ curValue[0] === 'cdate' ||
1673
+ curValue[0] === 'mdate' ||
1674
+ (this.nymph.tilmeld != null &&
1675
+ (curValue[0] === 'user' ||
1676
+ curValue[0] === 'group' ||
1677
+ curValue[0] === 'acUser' ||
1678
+ curValue[0] === 'acGroup' ||
1679
+ curValue[0] === 'acOther' ||
1680
+ curValue[0] === 'acRead' ||
1681
+ curValue[0] === 'acWrite' ||
1682
+ curValue[0] === 'acFull'))
1683
+ ) {
902
1684
  if (curQuery) {
903
1685
  curQuery += typeIsOr ? ' OR ' : ' AND ';
904
1686
  }
905
- const mdate = `param${++count.i}`;
1687
+ const value = `param${++count.i}`;
906
1688
  curQuery +=
907
1689
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
908
1690
  '(' +
909
1691
  ieTable +
910
- '."mdate" REGEXP @' +
911
- mdate +
1692
+ '.' +
1693
+ SQLite3Driver.escape(curValue[0]) +
1694
+ ' REGEXP @' +
1695
+ value +
912
1696
  ')';
913
- params[mdate] = curValue[1];
914
- break;
1697
+ params[value] = curValue[1];
915
1698
  } else {
916
1699
  if (curQuery) {
917
1700
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -920,10 +1703,11 @@ export default class SQLite3Driver extends NymphDriver {
920
1703
  const value = `param${++count.i}`;
921
1704
  curQuery +=
922
1705
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1706
+ 'EXISTS (SELECT "guid" FROM ' +
1707
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1708
+ ' WHERE "guid"=' +
923
1709
  ieTable +
924
- '."guid" IN (SELECT "guid" FROM ' +
925
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
926
- ' WHERE "name"=@' +
1710
+ '."guid" AND "name"=@' +
927
1711
  name +
928
1712
  ' AND "string" REGEXP @' +
929
1713
  value +
@@ -934,34 +1718,33 @@ export default class SQLite3Driver extends NymphDriver {
934
1718
  break;
935
1719
  case 'imatch':
936
1720
  case '!imatch':
937
- if (curValue[0] === 'cdate') {
938
- if (curQuery) {
939
- curQuery += typeIsOr ? ' OR ' : ' AND ';
940
- }
941
- const cdate = `param${++count.i}`;
942
- curQuery +=
943
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
944
- '(' +
945
- ieTable +
946
- '."cdate" REGEXP @' +
947
- cdate +
948
- ')';
949
- params[cdate] = curValue[1];
950
- break;
951
- } else if (curValue[0] === 'mdate') {
1721
+ if (
1722
+ curValue[0] === 'cdate' ||
1723
+ curValue[0] === 'mdate' ||
1724
+ (this.nymph.tilmeld != null &&
1725
+ (curValue[0] === 'user' ||
1726
+ curValue[0] === 'group' ||
1727
+ curValue[0] === 'acUser' ||
1728
+ curValue[0] === 'acGroup' ||
1729
+ curValue[0] === 'acOther' ||
1730
+ curValue[0] === 'acRead' ||
1731
+ curValue[0] === 'acWrite' ||
1732
+ curValue[0] === 'acFull'))
1733
+ ) {
952
1734
  if (curQuery) {
953
1735
  curQuery += typeIsOr ? ' OR ' : ' AND ';
954
1736
  }
955
- const mdate = `param${++count.i}`;
1737
+ const value = `param${++count.i}`;
956
1738
  curQuery +=
957
1739
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
958
- '(' +
1740
+ '(lower(' +
959
1741
  ieTable +
960
- '."mdate" REGEXP @' +
961
- mdate +
962
- ')';
963
- params[mdate] = curValue[1];
964
- break;
1742
+ '.' +
1743
+ SQLite3Driver.escape(curValue[0]) +
1744
+ ') REGEXP lower(@' +
1745
+ value +
1746
+ '))';
1747
+ params[value] = curValue[1];
965
1748
  } else {
966
1749
  if (curQuery) {
967
1750
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -970,10 +1753,11 @@ export default class SQLite3Driver extends NymphDriver {
970
1753
  const value = `param${++count.i}`;
971
1754
  curQuery +=
972
1755
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1756
+ 'EXISTS (SELECT "guid" FROM ' +
1757
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1758
+ ' WHERE "guid"=' +
973
1759
  ieTable +
974
- '."guid" IN (SELECT "guid" FROM ' +
975
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
976
- ' WHERE "name"=@' +
1760
+ '."guid" AND "name"=@' +
977
1761
  name +
978
1762
  ' AND lower("string") REGEXP lower(@' +
979
1763
  value +
@@ -984,34 +1768,33 @@ export default class SQLite3Driver extends NymphDriver {
984
1768
  break;
985
1769
  case 'like':
986
1770
  case '!like':
987
- if (curValue[0] === 'cdate') {
988
- if (curQuery) {
989
- curQuery += typeIsOr ? ' OR ' : ' AND ';
990
- }
991
- const cdate = `param${++count.i}`;
992
- curQuery +=
993
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
994
- '(' +
995
- ieTable +
996
- '."cdate" LIKE @' +
997
- cdate +
998
- " ESCAPE '\\')";
999
- params[cdate] = curValue[1];
1000
- break;
1001
- } else if (curValue[0] === 'mdate') {
1771
+ if (
1772
+ curValue[0] === 'cdate' ||
1773
+ curValue[0] === 'mdate' ||
1774
+ (this.nymph.tilmeld != null &&
1775
+ (curValue[0] === 'user' ||
1776
+ curValue[0] === 'group' ||
1777
+ curValue[0] === 'acUser' ||
1778
+ curValue[0] === 'acGroup' ||
1779
+ curValue[0] === 'acOther' ||
1780
+ curValue[0] === 'acRead' ||
1781
+ curValue[0] === 'acWrite' ||
1782
+ curValue[0] === 'acFull'))
1783
+ ) {
1002
1784
  if (curQuery) {
1003
1785
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1004
1786
  }
1005
- const mdate = `param${++count.i}`;
1787
+ const value = `param${++count.i}`;
1006
1788
  curQuery +=
1007
1789
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1008
1790
  '(' +
1009
1791
  ieTable +
1010
- '."mdate" LIKE @' +
1011
- mdate +
1792
+ '.' +
1793
+ SQLite3Driver.escape(curValue[0]) +
1794
+ ' LIKE @' +
1795
+ value +
1012
1796
  " ESCAPE '\\')";
1013
- params[mdate] = curValue[1];
1014
- break;
1797
+ params[value] = curValue[1];
1015
1798
  } else {
1016
1799
  if (curQuery) {
1017
1800
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1020,10 +1803,11 @@ export default class SQLite3Driver extends NymphDriver {
1020
1803
  const value = `param${++count.i}`;
1021
1804
  curQuery +=
1022
1805
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1806
+ 'EXISTS (SELECT "guid" FROM ' +
1807
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1808
+ ' WHERE "guid"=' +
1023
1809
  ieTable +
1024
- '."guid" IN (SELECT "guid" FROM ' +
1025
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1026
- ' WHERE "name"=@' +
1810
+ '."guid" AND "name"=@' +
1027
1811
  name +
1028
1812
  ' AND "string" LIKE @' +
1029
1813
  value +
@@ -1034,34 +1818,33 @@ export default class SQLite3Driver extends NymphDriver {
1034
1818
  break;
1035
1819
  case 'ilike':
1036
1820
  case '!ilike':
1037
- if (curValue[0] === 'cdate') {
1821
+ if (
1822
+ curValue[0] === 'cdate' ||
1823
+ curValue[0] === 'mdate' ||
1824
+ (this.nymph.tilmeld != null &&
1825
+ (curValue[0] === 'user' ||
1826
+ curValue[0] === 'group' ||
1827
+ curValue[0] === 'acUser' ||
1828
+ curValue[0] === 'acGroup' ||
1829
+ curValue[0] === 'acOther' ||
1830
+ curValue[0] === 'acRead' ||
1831
+ curValue[0] === 'acWrite' ||
1832
+ curValue[0] === 'acFull'))
1833
+ ) {
1038
1834
  if (curQuery) {
1039
1835
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1040
1836
  }
1041
- const cdate = `param${++count.i}`;
1837
+ const value = `param${++count.i}`;
1042
1838
  curQuery +=
1043
1839
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1044
- '(' +
1840
+ '(lower(' +
1045
1841
  ieTable +
1046
- '."cdate" LIKE @' +
1047
- cdate +
1048
- " ESCAPE '\\')";
1049
- params[cdate] = curValue[1];
1050
- break;
1051
- } else if (curValue[0] === 'mdate') {
1052
- if (curQuery) {
1053
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1054
- }
1055
- const mdate = `param${++count.i}`;
1056
- curQuery +=
1057
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1058
- '(' +
1059
- ieTable +
1060
- '."mdate" LIKE @' +
1061
- mdate +
1062
- " ESCAPE '\\')";
1063
- params[mdate] = curValue[1];
1064
- break;
1842
+ '.' +
1843
+ SQLite3Driver.escape(curValue[0]) +
1844
+ ') LIKE lower(@' +
1845
+ value +
1846
+ ") ESCAPE '\\')";
1847
+ params[value] = curValue[1];
1065
1848
  } else {
1066
1849
  if (curQuery) {
1067
1850
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1070,10 +1853,11 @@ export default class SQLite3Driver extends NymphDriver {
1070
1853
  const value = `param${++count.i}`;
1071
1854
  curQuery +=
1072
1855
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1856
+ 'EXISTS (SELECT "guid" FROM ' +
1857
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1858
+ ' WHERE "guid"=' +
1073
1859
  ieTable +
1074
- '."guid" IN (SELECT "guid" FROM ' +
1075
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1076
- ' WHERE "name"=@' +
1860
+ '."guid" AND "name"=@' +
1077
1861
  name +
1078
1862
  ' AND lower("string") LIKE lower(@' +
1079
1863
  value +
@@ -1084,30 +1868,31 @@ export default class SQLite3Driver extends NymphDriver {
1084
1868
  break;
1085
1869
  case 'gt':
1086
1870
  case '!gt':
1087
- if (curValue[0] === 'cdate') {
1871
+ if (
1872
+ curValue[0] === 'cdate' ||
1873
+ curValue[0] === 'mdate' ||
1874
+ (this.nymph.tilmeld != null &&
1875
+ (curValue[0] === 'user' ||
1876
+ curValue[0] === 'group' ||
1877
+ curValue[0] === 'acUser' ||
1878
+ curValue[0] === 'acGroup' ||
1879
+ curValue[0] === 'acOther' ||
1880
+ curValue[0] === 'acRead' ||
1881
+ curValue[0] === 'acWrite' ||
1882
+ curValue[0] === 'acFull'))
1883
+ ) {
1088
1884
  if (curQuery) {
1089
1885
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1090
1886
  }
1091
- const cdate = `param${++count.i}`;
1092
- curQuery +=
1093
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1094
- ieTable +
1095
- '."cdate">@' +
1096
- cdate;
1097
- params[cdate] = Number(curValue[1]);
1098
- break;
1099
- } else if (curValue[0] === 'mdate') {
1100
- if (curQuery) {
1101
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1102
- }
1103
- const mdate = `param${++count.i}`;
1887
+ const value = `param${++count.i}`;
1104
1888
  curQuery +=
1105
1889
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1106
1890
  ieTable +
1107
- '."mdate">@' +
1108
- mdate;
1109
- params[mdate] = Number(curValue[1]);
1110
- break;
1891
+ '.' +
1892
+ SQLite3Driver.escape(curValue[0]) +
1893
+ '>@' +
1894
+ value;
1895
+ params[value] = Number(curValue[1]);
1111
1896
  } else {
1112
1897
  if (curQuery) {
1113
1898
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1116,10 +1901,11 @@ export default class SQLite3Driver extends NymphDriver {
1116
1901
  const value = `param${++count.i}`;
1117
1902
  curQuery +=
1118
1903
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1904
+ 'EXISTS (SELECT "guid" FROM ' +
1905
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1906
+ ' WHERE "guid"=' +
1119
1907
  ieTable +
1120
- '."guid" IN (SELECT "guid" FROM ' +
1121
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1122
- ' WHERE "name"=@' +
1908
+ '."guid" AND "name"=@' +
1123
1909
  name +
1124
1910
  ' AND "number">@' +
1125
1911
  value +
@@ -1130,30 +1916,31 @@ export default class SQLite3Driver extends NymphDriver {
1130
1916
  break;
1131
1917
  case 'gte':
1132
1918
  case '!gte':
1133
- if (curValue[0] === 'cdate') {
1919
+ if (
1920
+ curValue[0] === 'cdate' ||
1921
+ curValue[0] === 'mdate' ||
1922
+ (this.nymph.tilmeld != null &&
1923
+ (curValue[0] === 'user' ||
1924
+ curValue[0] === 'group' ||
1925
+ curValue[0] === 'acUser' ||
1926
+ curValue[0] === 'acGroup' ||
1927
+ curValue[0] === 'acOther' ||
1928
+ curValue[0] === 'acRead' ||
1929
+ curValue[0] === 'acWrite' ||
1930
+ curValue[0] === 'acFull'))
1931
+ ) {
1134
1932
  if (curQuery) {
1135
1933
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1136
1934
  }
1137
- const cdate = `param${++count.i}`;
1138
- curQuery +=
1139
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1140
- ieTable +
1141
- '."cdate">=@' +
1142
- cdate;
1143
- params[cdate] = Number(curValue[1]);
1144
- break;
1145
- } else if (curValue[0] === 'mdate') {
1146
- if (curQuery) {
1147
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1148
- }
1149
- const mdate = `param${++count.i}`;
1935
+ const value = `param${++count.i}`;
1150
1936
  curQuery +=
1151
1937
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1152
1938
  ieTable +
1153
- '."mdate">=@' +
1154
- mdate;
1155
- params[mdate] = Number(curValue[1]);
1156
- break;
1939
+ '.' +
1940
+ SQLite3Driver.escape(curValue[0]) +
1941
+ '>=@' +
1942
+ value;
1943
+ params[value] = Number(curValue[1]);
1157
1944
  } else {
1158
1945
  if (curQuery) {
1159
1946
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1162,10 +1949,11 @@ export default class SQLite3Driver extends NymphDriver {
1162
1949
  const value = `param${++count.i}`;
1163
1950
  curQuery +=
1164
1951
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1952
+ 'EXISTS (SELECT "guid" FROM ' +
1953
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1954
+ ' WHERE "guid"=' +
1165
1955
  ieTable +
1166
- '."guid" IN (SELECT "guid" FROM ' +
1167
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1168
- ' WHERE "name"=@' +
1956
+ '."guid" AND "name"=@' +
1169
1957
  name +
1170
1958
  ' AND "number">=@' +
1171
1959
  value +
@@ -1176,30 +1964,31 @@ export default class SQLite3Driver extends NymphDriver {
1176
1964
  break;
1177
1965
  case 'lt':
1178
1966
  case '!lt':
1179
- if (curValue[0] === 'cdate') {
1180
- if (curQuery) {
1181
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1182
- }
1183
- const cdate = `param${++count.i}`;
1184
- curQuery +=
1185
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1186
- ieTable +
1187
- '."cdate"<@' +
1188
- cdate;
1189
- params[cdate] = Number(curValue[1]);
1190
- break;
1191
- } else if (curValue[0] === 'mdate') {
1967
+ if (
1968
+ curValue[0] === 'cdate' ||
1969
+ curValue[0] === 'mdate' ||
1970
+ (this.nymph.tilmeld != null &&
1971
+ (curValue[0] === 'user' ||
1972
+ curValue[0] === 'group' ||
1973
+ curValue[0] === 'acUser' ||
1974
+ curValue[0] === 'acGroup' ||
1975
+ curValue[0] === 'acOther' ||
1976
+ curValue[0] === 'acRead' ||
1977
+ curValue[0] === 'acWrite' ||
1978
+ curValue[0] === 'acFull'))
1979
+ ) {
1192
1980
  if (curQuery) {
1193
1981
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1194
1982
  }
1195
- const mdate = `param${++count.i}`;
1983
+ const value = `param${++count.i}`;
1196
1984
  curQuery +=
1197
1985
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1198
1986
  ieTable +
1199
- '."mdate"<@' +
1200
- mdate;
1201
- params[mdate] = Number(curValue[1]);
1202
- break;
1987
+ '.' +
1988
+ SQLite3Driver.escape(curValue[0]) +
1989
+ '<@' +
1990
+ value;
1991
+ params[value] = Number(curValue[1]);
1203
1992
  } else {
1204
1993
  if (curQuery) {
1205
1994
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1208,10 +1997,11 @@ export default class SQLite3Driver extends NymphDriver {
1208
1997
  const value = `param${++count.i}`;
1209
1998
  curQuery +=
1210
1999
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2000
+ 'EXISTS (SELECT "guid" FROM ' +
2001
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
2002
+ ' WHERE "guid"=' +
1211
2003
  ieTable +
1212
- '."guid" IN (SELECT "guid" FROM ' +
1213
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1214
- ' WHERE "name"=@' +
2004
+ '."guid" AND "name"=@' +
1215
2005
  name +
1216
2006
  ' AND "number"<@' +
1217
2007
  value +
@@ -1222,30 +2012,31 @@ export default class SQLite3Driver extends NymphDriver {
1222
2012
  break;
1223
2013
  case 'lte':
1224
2014
  case '!lte':
1225
- if (curValue[0] === 'cdate') {
2015
+ if (
2016
+ curValue[0] === 'cdate' ||
2017
+ curValue[0] === 'mdate' ||
2018
+ (this.nymph.tilmeld != null &&
2019
+ (curValue[0] === 'user' ||
2020
+ curValue[0] === 'group' ||
2021
+ curValue[0] === 'acUser' ||
2022
+ curValue[0] === 'acGroup' ||
2023
+ curValue[0] === 'acOther' ||
2024
+ curValue[0] === 'acRead' ||
2025
+ curValue[0] === 'acWrite' ||
2026
+ curValue[0] === 'acFull'))
2027
+ ) {
1226
2028
  if (curQuery) {
1227
2029
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1228
2030
  }
1229
- const cdate = `param${++count.i}`;
1230
- curQuery +=
1231
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1232
- ieTable +
1233
- '."cdate"<=@' +
1234
- cdate;
1235
- params[cdate] = Number(curValue[1]);
1236
- break;
1237
- } else if (curValue[0] === 'mdate') {
1238
- if (curQuery) {
1239
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1240
- }
1241
- const mdate = `param${++count.i}`;
2031
+ const value = `param${++count.i}`;
1242
2032
  curQuery +=
1243
2033
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1244
2034
  ieTable +
1245
- '."mdate"<=@' +
1246
- mdate;
1247
- params[mdate] = Number(curValue[1]);
1248
- break;
2035
+ '.' +
2036
+ SQLite3Driver.escape(curValue[0]) +
2037
+ '<=@' +
2038
+ value;
2039
+ params[value] = Number(curValue[1]);
1249
2040
  } else {
1250
2041
  if (curQuery) {
1251
2042
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1254,10 +2045,11 @@ export default class SQLite3Driver extends NymphDriver {
1254
2045
  const value = `param${++count.i}`;
1255
2046
  curQuery +=
1256
2047
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2048
+ 'EXISTS (SELECT "guid" FROM ' +
2049
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
2050
+ ' WHERE "guid"=' +
1257
2051
  ieTable +
1258
- '."guid" IN (SELECT "guid" FROM ' +
1259
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1260
- ' WHERE "name"=@' +
2052
+ '."guid" AND "name"=@' +
1261
2053
  name +
1262
2054
  ' AND "number"<=@' +
1263
2055
  value +
@@ -1279,32 +2071,70 @@ export default class SQLite3Driver extends NymphDriver {
1279
2071
  if (curQuery) {
1280
2072
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1281
2073
  }
1282
- const name = `param${++count.i}`;
1283
- const guid = `param${++count.i}`;
1284
- curQuery +=
1285
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1286
- ieTable +
1287
- '."guid" IN (SELECT "guid" FROM ' +
1288
- SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1289
- ' WHERE "name"=@' +
1290
- name +
1291
- ' AND "reference"=@' +
1292
- guid +
1293
- ')';
1294
- params[name] = curValue[0];
1295
- params[guid] = curQguid;
2074
+ if (
2075
+ this.nymph.tilmeld != null &&
2076
+ (curValue[0] === 'user' || curValue[0] === 'group')
2077
+ ) {
2078
+ const guid = `param${++count.i}`;
2079
+ curQuery +=
2080
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2081
+ ieTable +
2082
+ '.' +
2083
+ SQLite3Driver.escape(curValue[0]) +
2084
+ '=@' +
2085
+ guid;
2086
+ params[guid] = curQguid;
2087
+ } else if (
2088
+ this.nymph.tilmeld != null &&
2089
+ (curValue[0] === 'acRead' ||
2090
+ curValue[0] === 'acWrite' ||
2091
+ curValue[0] === 'acFull')
2092
+ ) {
2093
+ const guid = `param${++count.i}`;
2094
+ curQuery +=
2095
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2096
+ ieTable +
2097
+ '.' +
2098
+ SQLite3Driver.escape(curValue[0]) +
2099
+ ' LIKE @' +
2100
+ guid +
2101
+ " ESCAPE '\\'";
2102
+ params[guid] =
2103
+ '%,' +
2104
+ curQguid
2105
+ .replace('\\', '\\\\')
2106
+ .replace('%', '\\%')
2107
+ .replace('_', '\\_') +
2108
+ ',%';
2109
+ } else {
2110
+ const name = `param${++count.i}`;
2111
+ const guid = `param${++count.i}`;
2112
+ curQuery +=
2113
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2114
+ 'EXISTS (SELECT "guid" FROM ' +
2115
+ SQLite3Driver.escape(this.prefix + 'references_' + etype) +
2116
+ ' WHERE "guid"=' +
2117
+ ieTable +
2118
+ '."guid" AND "name"=@' +
2119
+ name +
2120
+ ' AND "reference"=@' +
2121
+ guid +
2122
+ ')';
2123
+ params[name] = curValue[0];
2124
+ params[guid] = curQguid;
2125
+ }
1296
2126
  break;
1297
2127
  case 'selector':
1298
2128
  case '!selector':
1299
- const subquery = this.makeEntityQuery(
1300
- options,
2129
+ const innerquery = this.makeEntityQuery(
2130
+ { ...options, sort: null, limit: undefined },
1301
2131
  [curValue],
1302
2132
  etype,
1303
2133
  count,
1304
2134
  params,
1305
2135
  true,
1306
2136
  tableSuffix,
1307
- etypes
2137
+ etypes,
1308
2138
  );
1309
2139
  if (curQuery) {
1310
2140
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1312,61 +2142,184 @@ export default class SQLite3Driver extends NymphDriver {
1312
2142
  curQuery +=
1313
2143
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1314
2144
  '(' +
1315
- subquery.query +
2145
+ innerquery.query +
1316
2146
  ')';
1317
2147
  break;
1318
2148
  case 'qref':
1319
2149
  case '!qref':
2150
+ const referenceTableSuffix = makeTableSuffix();
1320
2151
  const [qrefOptions, ...qrefSelectors] = curValue[1] as [
1321
2152
  Options,
1322
- ...FormattedSelector[]
2153
+ ...FormattedSelector[],
1323
2154
  ];
1324
2155
  const QrefEntityClass = qrefOptions.class as EntityConstructor;
1325
2156
  etypes.push(QrefEntityClass.ETYPE);
1326
- const qrefQuery = this.makeEntityQuery(
1327
- { ...qrefOptions, return: 'guid', class: QrefEntityClass },
1328
- qrefSelectors,
1329
- QrefEntityClass.ETYPE,
1330
- count,
1331
- params,
1332
- false,
1333
- makeTableSuffix(),
1334
- etypes
1335
- );
1336
2157
  if (curQuery) {
1337
2158
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1338
2159
  }
1339
- const qrefName = `param${++count.i}`;
1340
- curQuery +=
1341
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1342
- ieTable +
1343
- '."guid" IN (SELECT "guid" FROM ' +
1344
- SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1345
- ' WHERE "name"=@' +
1346
- qrefName +
1347
- ' AND "reference" IN (' +
1348
- qrefQuery.query +
1349
- '))';
1350
- params[qrefName] = curValue[0];
2160
+ if (
2161
+ this.nymph.tilmeld != null &&
2162
+ (curValue[0] === 'user' || curValue[0] === 'group')
2163
+ ) {
2164
+ const qrefQuery = this.makeEntityQuery(
2165
+ {
2166
+ ...qrefOptions,
2167
+ sort: qrefOptions.sort ?? null,
2168
+ return: 'guid',
2169
+ class: QrefEntityClass,
2170
+ },
2171
+ qrefSelectors,
2172
+ QrefEntityClass.ETYPE,
2173
+ count,
2174
+ params,
2175
+ false,
2176
+ makeTableSuffix(),
2177
+ etypes,
2178
+ 'r' +
2179
+ referenceTableSuffix +
2180
+ '.' +
2181
+ SQLite3Driver.escape(curValue[0]),
2182
+ );
2183
+
2184
+ curQuery +=
2185
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2186
+ 'EXISTS (SELECT "guid" FROM ' +
2187
+ SQLite3Driver.escape(this.prefix + 'entities_' + etype) +
2188
+ ' r' +
2189
+ referenceTableSuffix +
2190
+ ' WHERE r' +
2191
+ referenceTableSuffix +
2192
+ '."guid"=' +
2193
+ ieTable +
2194
+ '."guid" AND EXISTS (' +
2195
+ qrefQuery.query +
2196
+ '))';
2197
+ } else if (
2198
+ this.nymph.tilmeld != null &&
2199
+ (curValue[0] === 'acRead' ||
2200
+ curValue[0] === 'acWrite' ||
2201
+ curValue[0] === 'acFull')
2202
+ ) {
2203
+ const qrefQuery = this.makeEntityQuery(
2204
+ {
2205
+ ...qrefOptions,
2206
+ sort: qrefOptions.sort ?? null,
2207
+ return: 'guid',
2208
+ class: QrefEntityClass,
2209
+ },
2210
+ qrefSelectors,
2211
+ QrefEntityClass.ETYPE,
2212
+ count,
2213
+ params,
2214
+ false,
2215
+ makeTableSuffix(),
2216
+ etypes,
2217
+ 'r' + referenceTableSuffix + '."ref"',
2218
+ );
2219
+
2220
+ const splitTableSuffix = makeTableSuffix();
2221
+ curQuery +=
2222
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2223
+ `EXISTS (SELECT "guid", "ref" FROM (
2224
+ WITH RECURSIVE "spl${splitTableSuffix}" AS (
2225
+ SELECT
2226
+ "guid",
2227
+ SUBSTR(${SQLite3Driver.escape(curValue[0])}, 1, INSTR(${SQLite3Driver.escape(curValue[0])}, ',') - 1) AS "ref",
2228
+ SUBSTR(${SQLite3Driver.escape(curValue[0])}, INSTR(${SQLite3Driver.escape(curValue[0])}, ',') + 1) AS "remainder"
2229
+ FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)}
2230
+ UNION ALL
2231
+ SELECT
2232
+ "guid",
2233
+ SUBSTR("remainder", 1, INSTR("remainder", ',') - 1) AS "ref",
2234
+ SUBSTR("remainder", INSTR("remainder", ',') + 1) AS "remainder"
2235
+ FROM "spl${splitTableSuffix}" WHERE "remainder" != ''
2236
+ )
2237
+ SELECT "guid", "ref" FROM "spl${splitTableSuffix}" WHERE "ref" != ''
2238
+ ) ` +
2239
+ ' r' +
2240
+ referenceTableSuffix +
2241
+ ' WHERE r' +
2242
+ referenceTableSuffix +
2243
+ '."guid"=' +
2244
+ ieTable +
2245
+ '."guid" AND EXISTS (' +
2246
+ qrefQuery.query +
2247
+ '))';
2248
+ } else {
2249
+ const qrefQuery = this.makeEntityQuery(
2250
+ {
2251
+ ...qrefOptions,
2252
+ sort: qrefOptions.sort ?? null,
2253
+ return: 'guid',
2254
+ class: QrefEntityClass,
2255
+ },
2256
+ qrefSelectors,
2257
+ QrefEntityClass.ETYPE,
2258
+ count,
2259
+ params,
2260
+ false,
2261
+ makeTableSuffix(),
2262
+ etypes,
2263
+ 'r' + referenceTableSuffix + '."reference"',
2264
+ );
2265
+
2266
+ const qrefName = `param${++count.i}`;
2267
+ curQuery +=
2268
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2269
+ 'EXISTS (SELECT "guid" FROM ' +
2270
+ SQLite3Driver.escape(this.prefix + 'references_' + etype) +
2271
+ ' r' +
2272
+ referenceTableSuffix +
2273
+ ' WHERE r' +
2274
+ referenceTableSuffix +
2275
+ '."guid"=' +
2276
+ ieTable +
2277
+ '."guid" AND r' +
2278
+ referenceTableSuffix +
2279
+ '."name"=@' +
2280
+ qrefName +
2281
+ ' AND EXISTS (' +
2282
+ qrefQuery.query +
2283
+ '))';
2284
+ params[qrefName] = curValue[0];
2285
+ }
1351
2286
  break;
1352
2287
  }
1353
2288
  }
1354
2289
  return curQuery;
1355
- }
2290
+ },
1356
2291
  );
1357
2292
 
1358
2293
  let sortBy: string;
1359
- switch (sort) {
1360
- case 'mdate':
1361
- sortBy = '"mdate"';
1362
- break;
1363
- case 'cdate':
1364
- default:
1365
- sortBy = '"cdate"';
1366
- break;
1367
- }
1368
- if (options.reverse) {
1369
- sortBy += ' DESC';
2294
+ let sortByInner: string;
2295
+ let sortJoin = '';
2296
+ const order = options.reverse ? ' DESC' : '';
2297
+ if (sort == null) {
2298
+ sortBy = '';
2299
+ sortByInner = '';
2300
+ } else {
2301
+ switch (sort) {
2302
+ case 'mdate':
2303
+ sortBy = `ORDER BY ${eTable}."mdate"${order}`;
2304
+ sortByInner = `ORDER BY ${ieTable}."mdate"${order}`;
2305
+ break;
2306
+ case 'cdate':
2307
+ sortBy = `ORDER BY ${eTable}."cdate"${order}`;
2308
+ sortByInner = `ORDER BY ${ieTable}."cdate"${order}`;
2309
+ break;
2310
+ default:
2311
+ const name = `param${++count.i}`;
2312
+ sortJoin = `LEFT JOIN (
2313
+ SELECT "guid", "string", "number"
2314
+ FROM ${SQLite3Driver.escape(this.prefix + 'data_' + etype)}
2315
+ WHERE "name"=@${name}
2316
+ ORDER BY "number"${order}, "string"${order}
2317
+ ) ${sTable} USING ("guid")`;
2318
+ sortBy = `ORDER BY ${sTable}."number"${order}, ${sTable}."string"${order}`;
2319
+ sortByInner = sortBy;
2320
+ params[name] = sort;
2321
+ break;
2322
+ }
1370
2323
  }
1371
2324
 
1372
2325
  let query: string;
@@ -1383,57 +2336,69 @@ export default class SQLite3Driver extends NymphDriver {
1383
2336
  offset = ` OFFSET ${Math.floor(Number(options.offset))}`;
1384
2337
  }
1385
2338
  const whereClause = queryParts.join(') AND (');
2339
+ const guidClause = guidSelector
2340
+ ? `${ieTable}."guid"=${guidSelector} AND `
2341
+ : '';
1386
2342
  if (options.return === 'count') {
1387
2343
  if (limit || offset) {
1388
2344
  query = `SELECT COUNT("guid") AS "count" FROM (
1389
2345
  SELECT "guid"
1390
2346
  FROM ${SQLite3Driver.escape(
1391
- this.prefix + 'entities_' + etype
2347
+ this.prefix + 'entities_' + etype,
1392
2348
  )} ${ieTable}
1393
- WHERE (${whereClause})${limit}${offset}
2349
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1394
2350
  )`;
1395
2351
  } else {
1396
2352
  query = `SELECT COUNT("guid") AS "count"
1397
2353
  FROM ${SQLite3Driver.escape(
1398
- this.prefix + 'entities_' + etype
2354
+ this.prefix + 'entities_' + etype,
1399
2355
  )} ${ieTable}
1400
- WHERE (${whereClause})`;
2356
+ WHERE ${guidClause}(${whereClause})`;
1401
2357
  }
1402
2358
  } else if (options.return === 'guid') {
1403
2359
  query = `SELECT "guid"
1404
2360
  FROM ${SQLite3Driver.escape(
1405
- this.prefix + 'entities_' + etype
2361
+ this.prefix + 'entities_' + etype,
1406
2362
  )} ${ieTable}
1407
- WHERE (${whereClause})
1408
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
2363
+ ${sortJoin}
2364
+ WHERE ${guidClause}(${whereClause})
2365
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}"guid"${limit}${offset}`;
1409
2366
  } else {
1410
2367
  query = `SELECT
1411
2368
  ${eTable}."guid",
1412
2369
  ${eTable}."tags",
1413
2370
  ${eTable}."cdate",
1414
2371
  ${eTable}."mdate",
2372
+ ${eTable}."user",
2373
+ ${eTable}."group",
2374
+ ${eTable}."acUser",
2375
+ ${eTable}."acGroup",
2376
+ ${eTable}."acOther",
2377
+ ${eTable}."acRead",
2378
+ ${eTable}."acWrite",
2379
+ ${eTable}."acFull",
1415
2380
  ${dTable}."name",
1416
2381
  ${dTable}."value",
1417
- ${cTable}."string",
1418
- ${cTable}."number"
2382
+ json(${dTable}."json") as "json",
2383
+ ${dTable}."string",
2384
+ ${dTable}."number"
1419
2385
  FROM ${SQLite3Driver.escape(
1420
- this.prefix + 'entities_' + etype
2386
+ this.prefix + 'entities_' + etype,
1421
2387
  )} ${eTable}
1422
2388
  LEFT JOIN ${SQLite3Driver.escape(
1423
- this.prefix + 'data_' + etype
2389
+ this.prefix + 'data_' + etype,
1424
2390
  )} ${dTable} USING ("guid")
1425
- INNER JOIN ${SQLite3Driver.escape(
1426
- this.prefix + 'comparisons_' + etype
1427
- )} ${cTable} USING ("guid", "name")
2391
+ ${sortJoin}
1428
2392
  INNER JOIN (
1429
2393
  SELECT "guid"
1430
2394
  FROM ${SQLite3Driver.escape(
1431
- this.prefix + 'entities_' + etype
2395
+ this.prefix + 'entities_' + etype,
1432
2396
  )} ${ieTable}
1433
- WHERE (${whereClause})
1434
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
2397
+ ${sortJoin}
2398
+ WHERE ${guidClause}(${whereClause})
2399
+ ${sortByInner}${limit}${offset}
1435
2400
  ) ${fTable} USING ("guid")
1436
- ORDER BY ${eTable}.${sortBy}`;
2401
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1437
2402
  }
1438
2403
  }
1439
2404
  } else {
@@ -1448,26 +2413,31 @@ export default class SQLite3Driver extends NymphDriver {
1448
2413
  if ('offset' in options) {
1449
2414
  offset = ` OFFSET ${Math.floor(Number(options.offset))}`;
1450
2415
  }
2416
+ const guidClause = guidSelector
2417
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
2418
+ : '';
1451
2419
  if (options.return === 'count') {
1452
2420
  if (limit || offset) {
1453
2421
  query = `SELECT COUNT("guid") AS "count" FROM (
1454
2422
  SELECT "guid"
1455
2423
  FROM ${SQLite3Driver.escape(
1456
- this.prefix + 'entities_' + etype
1457
- )} ${ieTable}${limit}${offset}
2424
+ this.prefix + 'entities_' + etype,
2425
+ )} ${ieTable}${guidClause}${limit}${offset}
1458
2426
  )`;
1459
2427
  } else {
1460
2428
  query = `SELECT COUNT("guid") AS "count"
1461
2429
  FROM ${SQLite3Driver.escape(
1462
- this.prefix + 'entities_' + etype
1463
- )} ${ieTable}`;
2430
+ this.prefix + 'entities_' + etype,
2431
+ )} ${ieTable}${guidClause}`;
1464
2432
  }
1465
2433
  } else if (options.return === 'guid') {
1466
2434
  query = `SELECT "guid"
1467
2435
  FROM ${SQLite3Driver.escape(
1468
- this.prefix + 'entities_' + etype
2436
+ this.prefix + 'entities_' + etype,
1469
2437
  )} ${ieTable}
1470
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
2438
+ ${sortJoin}
2439
+ ${guidClause}
2440
+ ${sortByInner ? sortByInner + ', ' : 'ORDER BY '}"guid"${limit}${offset}`;
1471
2441
  } else {
1472
2442
  if (limit || offset) {
1473
2443
  query = `SELECT
@@ -1475,47 +2445,64 @@ export default class SQLite3Driver extends NymphDriver {
1475
2445
  ${eTable}."tags",
1476
2446
  ${eTable}."cdate",
1477
2447
  ${eTable}."mdate",
2448
+ ${eTable}."user",
2449
+ ${eTable}."group",
2450
+ ${eTable}."acUser",
2451
+ ${eTable}."acGroup",
2452
+ ${eTable}."acOther",
2453
+ ${eTable}."acRead",
2454
+ ${eTable}."acWrite",
2455
+ ${eTable}."acFull",
1478
2456
  ${dTable}."name",
1479
2457
  ${dTable}."value",
1480
- ${cTable}."string",
1481
- ${cTable}."number"
2458
+ json(${dTable}."json") as "json",
2459
+ ${dTable}."string",
2460
+ ${dTable}."number"
1482
2461
  FROM ${SQLite3Driver.escape(
1483
- this.prefix + 'entities_' + etype
2462
+ this.prefix + 'entities_' + etype,
1484
2463
  )} ${eTable}
1485
2464
  LEFT JOIN ${SQLite3Driver.escape(
1486
- this.prefix + 'data_' + etype
2465
+ this.prefix + 'data_' + etype,
1487
2466
  )} ${dTable} USING ("guid")
1488
- INNER JOIN ${SQLite3Driver.escape(
1489
- this.prefix + 'comparisons_' + etype
1490
- )} c USING ("guid", "name")
2467
+ ${sortJoin}
1491
2468
  INNER JOIN (
1492
2469
  SELECT "guid"
1493
2470
  FROM ${SQLite3Driver.escape(
1494
- this.prefix + 'entities_' + etype
2471
+ this.prefix + 'entities_' + etype,
1495
2472
  )} ${ieTable}
1496
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
2473
+ ${sortJoin}
2474
+ ${guidClause}
2475
+ ${sortByInner}${limit}${offset}
1497
2476
  ) ${fTable} USING ("guid")
1498
- ORDER BY ${eTable}.${sortBy}`;
2477
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1499
2478
  } else {
1500
2479
  query = `SELECT
1501
2480
  ${eTable}."guid",
1502
2481
  ${eTable}."tags",
1503
2482
  ${eTable}."cdate",
1504
2483
  ${eTable}."mdate",
2484
+ ${eTable}."user",
2485
+ ${eTable}."group",
2486
+ ${eTable}."acUser",
2487
+ ${eTable}."acGroup",
2488
+ ${eTable}."acOther",
2489
+ ${eTable}."acRead",
2490
+ ${eTable}."acWrite",
2491
+ ${eTable}."acFull",
1505
2492
  ${dTable}."name",
1506
2493
  ${dTable}."value",
1507
- ${cTable}."string",
1508
- ${cTable}."number"
2494
+ json(${dTable}."json") as "json",
2495
+ ${dTable}."string",
2496
+ ${dTable}."number"
1509
2497
  FROM ${SQLite3Driver.escape(
1510
- this.prefix + 'entities_' + etype
2498
+ this.prefix + 'entities_' + etype,
1511
2499
  )} ${eTable}
1512
2500
  LEFT JOIN ${SQLite3Driver.escape(
1513
- this.prefix + 'data_' + etype
2501
+ this.prefix + 'data_' + etype,
1514
2502
  )} ${dTable} USING ("guid")
1515
- INNER JOIN ${SQLite3Driver.escape(
1516
- this.prefix + 'comparisons_' + etype
1517
- )} ${cTable} USING ("guid", "name")
1518
- ORDER BY ${eTable}.${sortBy}`;
2503
+ ${sortJoin}
2504
+ ${guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
2505
+ ${sortBy ? sortBy + ', ' : 'ORDER BY '}${eTable}."guid"`;
1519
2506
  }
1520
2507
  }
1521
2508
  }
@@ -1535,16 +2522,18 @@ export default class SQLite3Driver extends NymphDriver {
1535
2522
  protected performQuery(
1536
2523
  options: Options,
1537
2524
  formattedSelectors: FormattedSelector[],
1538
- etype: string
2525
+ etype: string,
1539
2526
  ): {
1540
2527
  result: any;
1541
2528
  } {
1542
2529
  const { query, params, etypes } = this.makeEntityQuery(
1543
2530
  options,
1544
2531
  formattedSelectors,
1545
- etype
2532
+ etype,
1546
2533
  );
1547
- const result = this.queryIter(query, { etypes, params })[Symbol.iterator]();
2534
+ const result = this.queryArray(query, { etypes, params })[
2535
+ Symbol.iterator
2536
+ ]();
1548
2537
  return {
1549
2538
  result,
1550
2539
  };
@@ -1559,37 +2548,24 @@ export default class SQLite3Driver extends NymphDriver {
1559
2548
  ...selectors: Selector[]
1560
2549
  ): Promise<string[]>;
1561
2550
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1562
- options?: Options<T>,
2551
+ options: Options<T> & { return: 'object' },
1563
2552
  ...selectors: Selector[]
1564
- ): Promise<ReturnType<T['factorySync']>[]>;
2553
+ ): Promise<EntityObjectType<T>[]>;
1565
2554
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1566
- options: Options<T> = {},
1567
- ...selectors: Selector[]
1568
- ): Promise<ReturnType<T['factorySync']>[] | string[] | number> {
1569
- return this.getEntitiesSync(options, ...selectors);
1570
- }
1571
-
1572
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1573
- options: Options<T> & { return: 'count' },
1574
- ...selectors: Selector[]
1575
- ): number;
1576
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1577
- options: Options<T> & { return: 'guid' },
1578
- ...selectors: Selector[]
1579
- ): string[];
1580
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1581
2555
  options?: Options<T>,
1582
2556
  ...selectors: Selector[]
1583
- ): ReturnType<T['factorySync']>[];
1584
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
2557
+ ): Promise<EntityInstanceType<T>[]>;
2558
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
1585
2559
  options: Options<T> = {},
1586
2560
  ...selectors: Selector[]
1587
- ): ReturnType<T['factorySync']>[] | string[] | number {
1588
- const { result, process } = this.getEntitesRowLike<T>(
2561
+ ): Promise<
2562
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
2563
+ > {
2564
+ const { result, process } = this.getEntitiesRowLike<T>(
1589
2565
  options,
1590
2566
  selectors,
1591
- (options, formattedSelectors, etype) =>
1592
- this.performQuery(options, formattedSelectors, etype),
2567
+ ({ options, selectors, etype }) =>
2568
+ this.performQuery(options, selectors, etype),
1593
2569
  () => {
1594
2570
  const next: any = result.next();
1595
2571
  return next.done ? null : next.value;
@@ -1598,9 +2574,23 @@ export default class SQLite3Driver extends NymphDriver {
1598
2574
  (row) => Number(row.count),
1599
2575
  (row) => row.guid,
1600
2576
  (row) => ({
1601
- tags: row.tags.length > 2 ? row.tags.slice(1, -1).split(',') : [],
2577
+ tags:
2578
+ row.tags.length > 2
2579
+ ? row.tags
2580
+ .slice(1, -1)
2581
+ .split(',')
2582
+ .filter((tag: string) => tag)
2583
+ : [],
1602
2584
  cdate: Number(row.cdate),
1603
2585
  mdate: Number(row.mdate),
2586
+ user: row.user,
2587
+ group: row.group,
2588
+ acUser: row.acUser,
2589
+ acGroup: row.acGroup,
2590
+ acOther: row.acOther,
2591
+ acRead: row.acRead?.slice(1, -1).split(',') ?? [],
2592
+ acWrite: row.acWrite?.slice(1, -1).split(',') ?? [],
2593
+ acFull: row.acFull?.slice(1, -1).split(',') ?? [],
1604
2594
  }),
1605
2595
  (row) => ({
1606
2596
  name: row.name,
@@ -1608,9 +2598,11 @@ export default class SQLite3Driver extends NymphDriver {
1608
2598
  row.value === 'N'
1609
2599
  ? JSON.stringify(row.number)
1610
2600
  : row.value === 'S'
1611
- ? JSON.stringify(row.string)
1612
- : row.value,
1613
- })
2601
+ ? JSON.stringify(row.string)
2602
+ : row.value === 'J'
2603
+ ? row.json
2604
+ : row.value,
2605
+ }),
1614
2606
  );
1615
2607
  const value = process();
1616
2608
  if (value instanceof Error) {
@@ -1623,175 +2615,355 @@ export default class SQLite3Driver extends NymphDriver {
1623
2615
  if (name == null) {
1624
2616
  throw new InvalidParametersError('Name not given for UID.');
1625
2617
  }
1626
- const result = this.queryGet(
2618
+ const result: any = this.queryGet(
1627
2619
  `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
1628
- `${this.prefix}uids`
2620
+ `${this.prefix}uids`,
1629
2621
  )} WHERE "name"=@name;`,
1630
2622
  {
1631
2623
  params: {
1632
2624
  name: name,
1633
2625
  },
1634
- }
2626
+ },
1635
2627
  );
1636
2628
  return (result?.cur_uid as number | null) ?? null;
1637
2629
  }
1638
2630
 
1639
- public async import(filename: string) {
1640
- this.checkReadOnlyMode();
2631
+ public async importEntity(entity: {
2632
+ guid: string;
2633
+ cdate: number;
2634
+ mdate: number;
2635
+ tags: string[];
2636
+ sdata: SerializedEntityData;
2637
+ etype: string;
2638
+ }) {
2639
+ return await this.importEntityInternal(entity);
2640
+ }
2641
+
2642
+ public async importEntityTokens(entity: {
2643
+ guid: string;
2644
+ cdate: number;
2645
+ mdate: number;
2646
+ tags: string[];
2647
+ sdata: SerializedEntityData;
2648
+ etype: string;
2649
+ }) {
2650
+ return await this.importEntityInternal(entity, { only: 'tokens' });
2651
+ }
2652
+
2653
+ public async importEntityTilmeldAC(entity: {
2654
+ guid: string;
2655
+ cdate: number;
2656
+ mdate: number;
2657
+ tags: string[];
2658
+ sdata: SerializedEntityData;
2659
+ etype: string;
2660
+ }) {
2661
+ return await this.importEntityInternal(entity, { only: 'tilmeldAC' });
2662
+ }
2663
+
2664
+ private async importEntityInternal(
2665
+ {
2666
+ guid,
2667
+ cdate,
2668
+ mdate,
2669
+ tags,
2670
+ sdata,
2671
+ etype,
2672
+ }: {
2673
+ guid: string;
2674
+ cdate: number;
2675
+ mdate: number;
2676
+ tags: string[];
2677
+ sdata: SerializedEntityData;
2678
+ etype: string;
2679
+ },
2680
+ { only = undefined }: { only?: 'tokens' | 'tilmeldAC' } = {},
2681
+ ) {
1641
2682
  try {
1642
- return this.importFromFile(
1643
- filename,
1644
- async (guid, tags, sdata, etype) => {
1645
- this.queryRun(
1646
- `DELETE FROM ${SQLite3Driver.escape(
1647
- `${this.prefix}entities_${etype}`
1648
- )} WHERE "guid"=@guid;`,
1649
- {
1650
- etypes: [etype],
1651
- params: {
1652
- guid,
1653
- },
1654
- }
1655
- );
1656
- this.queryRun(
1657
- `DELETE FROM ${SQLite3Driver.escape(
1658
- `${this.prefix}data_${etype}`
1659
- )} WHERE "guid"=@guid;`,
1660
- {
1661
- etypes: [etype],
1662
- params: {
1663
- guid,
1664
- },
1665
- }
1666
- );
1667
- this.queryRun(
1668
- `DELETE FROM ${SQLite3Driver.escape(
1669
- `${this.prefix}comparisons_${etype}`
1670
- )} WHERE "guid"=@guid;`,
1671
- {
1672
- etypes: [etype],
1673
- params: {
1674
- guid,
1675
- },
1676
- }
1677
- );
1678
- this.queryRun(
1679
- `DELETE FROM ${SQLite3Driver.escape(
1680
- `${this.prefix}references_${etype}`
1681
- )} WHERE "guid"=@guid;`,
1682
- {
1683
- etypes: [etype],
1684
- params: {
1685
- guid,
1686
- },
1687
- }
1688
- );
2683
+ if (only == null) {
2684
+ this.queryRun(
2685
+ `DELETE FROM ${SQLite3Driver.escape(
2686
+ `${this.prefix}entities_${etype}`,
2687
+ )} WHERE "guid"=@guid;`,
2688
+ {
2689
+ etypes: [etype],
2690
+ params: {
2691
+ guid,
2692
+ },
2693
+ },
2694
+ );
2695
+ this.queryRun(
2696
+ `DELETE FROM ${SQLite3Driver.escape(
2697
+ `${this.prefix}data_${etype}`,
2698
+ )} WHERE "guid"=@guid;`,
2699
+ {
2700
+ etypes: [etype],
2701
+ params: {
2702
+ guid,
2703
+ },
2704
+ },
2705
+ );
2706
+ this.queryRun(
2707
+ `DELETE FROM ${SQLite3Driver.escape(
2708
+ `${this.prefix}references_${etype}`,
2709
+ )} WHERE "guid"=@guid;`,
2710
+ {
2711
+ etypes: [etype],
2712
+ params: {
2713
+ guid,
2714
+ },
2715
+ },
2716
+ );
2717
+ }
2718
+ if (only == null || only === 'tokens') {
2719
+ this.queryRun(
2720
+ `DELETE FROM ${SQLite3Driver.escape(
2721
+ `${this.prefix}tokens_${etype}`,
2722
+ )} WHERE "guid"=@guid;`,
2723
+ {
2724
+ etypes: [etype],
2725
+ params: {
2726
+ guid,
2727
+ },
2728
+ },
2729
+ );
2730
+ }
2731
+ if (only == null) {
2732
+ this.queryRun(
2733
+ `DELETE FROM ${SQLite3Driver.escape(
2734
+ `${this.prefix}uniques_${etype}`,
2735
+ )} WHERE "guid"=@guid;`,
2736
+ {
2737
+ etypes: [etype],
2738
+ params: {
2739
+ guid,
2740
+ },
2741
+ },
2742
+ );
2743
+ }
2744
+
2745
+ if (only == null) {
2746
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } =
2747
+ this.removeAndReturnACValues(etype, {}, sdata);
2748
+
2749
+ this.queryRun(
2750
+ `INSERT INTO ${SQLite3Driver.escape(
2751
+ `${this.prefix}entities_${etype}`,
2752
+ )} ("guid", "tags", "cdate", "mdate", "user", "group", "acUser", "acGroup", "acOther", "acRead", "acWrite", "acFull") VALUES (@guid, @tags, @cdate, @mdate, @user, @group, @acUser, @acGroup, @acOther, @acRead, @acWrite, @acFull);`,
2753
+ {
2754
+ etypes: [etype],
2755
+ params: {
2756
+ guid,
2757
+ tags: ',' + tags.join(',') + ',',
2758
+ cdate,
2759
+ mdate,
2760
+ user,
2761
+ group,
2762
+ acUser,
2763
+ acGroup,
2764
+ acOther,
2765
+ acRead: acRead && ',' + acRead.join(',') + ',',
2766
+ acWrite: acWrite && ',' + acWrite.join(',') + ',',
2767
+ acFull: acFull && ',' + acFull.join(',') + ',',
2768
+ },
2769
+ },
2770
+ );
2771
+
2772
+ for (const name in sdata) {
2773
+ const value = sdata[name];
2774
+ const uvalue = JSON.parse(value);
2775
+ if (value === undefined) {
2776
+ continue;
2777
+ }
2778
+ const storageValue =
2779
+ typeof uvalue === 'number'
2780
+ ? 'N'
2781
+ : typeof uvalue === 'string'
2782
+ ? 'S'
2783
+ : 'J';
2784
+ const jsonValue = storageValue === 'J' ? value : null;
2785
+
1689
2786
  this.queryRun(
1690
2787
  `INSERT INTO ${SQLite3Driver.escape(
1691
- `${this.prefix}entities_${etype}`
1692
- )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`,
2788
+ `${this.prefix}data_${etype}`,
2789
+ )} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (@guid, @name, @storageValue, jsonb(@jsonValue), @string, @number, @truthy);`,
1693
2790
  {
1694
2791
  etypes: [etype],
1695
2792
  params: {
1696
2793
  guid,
1697
- tags: ',' + tags.join(',') + ',',
1698
- cdate: Number(JSON.parse(sdata.cdate)),
1699
- mdate: Number(JSON.parse(sdata.mdate)),
2794
+ name,
2795
+ storageValue,
2796
+ jsonValue,
2797
+ string: storageValue === 'J' ? null : `${uvalue}`,
2798
+ number: Number(uvalue),
2799
+ truthy: uvalue ? 1 : 0,
1700
2800
  },
1701
- }
2801
+ },
1702
2802
  );
1703
- delete sdata.cdate;
1704
- delete sdata.mdate;
1705
- for (const name in sdata) {
1706
- const value = sdata[name];
1707
- const uvalue = JSON.parse(value);
1708
- if (value === undefined) {
1709
- continue;
1710
- }
1711
- const storageValue =
1712
- typeof uvalue === 'number'
1713
- ? 'N'
1714
- : typeof uvalue === 'string'
1715
- ? 'S'
1716
- : value;
2803
+
2804
+ const references = this.findReferences(value);
2805
+ for (const reference of references) {
1717
2806
  this.queryRun(
1718
2807
  `INSERT INTO ${SQLite3Driver.escape(
1719
- `${this.prefix}data_${etype}`
1720
- )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
2808
+ `${this.prefix}references_${etype}`,
2809
+ )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1721
2810
  {
1722
2811
  etypes: [etype],
1723
2812
  params: {
1724
2813
  guid,
1725
2814
  name,
1726
- storageValue,
2815
+ reference,
1727
2816
  },
1728
- }
2817
+ },
1729
2818
  );
2819
+ }
2820
+ }
2821
+ }
2822
+
2823
+ if (only === 'tilmeldAC') {
2824
+ let { user, group, acUser, acGroup, acOther, acRead, acWrite, acFull } =
2825
+ this.removeAndReturnACValues(etype, {}, sdata);
2826
+
2827
+ this.queryRun(
2828
+ `UPDATE OR IGNORE ${SQLite3Driver.escape(
2829
+ `${this.prefix}entities_${etype}`,
2830
+ )} SET "user"=@user, "group"=@group, "acUser"=@acUser, "acGroup"=@acGroup, "acOther"=@acOther, "acRead"=@acRead, "acWrite"=@acWrite, "acFull"=@acFull WHERE "guid"=@guid;`,
2831
+ {
2832
+ etypes: [etype],
2833
+ params: {
2834
+ user,
2835
+ group,
2836
+ acUser,
2837
+ acGroup,
2838
+ acOther,
2839
+ acRead: acRead && ',' + acRead.join(',') + ',',
2840
+ acWrite: acWrite && ',' + acWrite.join(',') + ',',
2841
+ acFull: acFull && ',' + acFull.join(',') + ',',
2842
+ guid,
2843
+ },
2844
+ },
2845
+ );
2846
+ }
2847
+
2848
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
2849
+
2850
+ if (only == null || only === 'tokens') {
2851
+ for (let name in sdata) {
2852
+ let tokenString: string | null = null;
2853
+ try {
2854
+ tokenString = EntityClass.getFTSText(name, JSON.parse(sdata[name]));
2855
+ } catch (e: any) {
2856
+ // Ignore error.
2857
+ }
2858
+
2859
+ if (tokenString != null) {
2860
+ const tokens = this.tokenizer.tokenize(tokenString);
2861
+ while (tokens.length) {
2862
+ const currentTokens = tokens.splice(0, 100);
2863
+ const params: { [k: string]: any } = {
2864
+ guid,
2865
+ name,
2866
+ };
2867
+ const values: string[] = [];
2868
+
2869
+ for (let i = 0; i < currentTokens.length; i++) {
2870
+ const token = currentTokens[i];
2871
+ params['token' + i] = token.token;
2872
+ params['position' + i] = token.position;
2873
+ params['stem' + i] = token.stem ? 1 : 0;
2874
+ values.push(
2875
+ '(@guid, @name, @token' +
2876
+ i +
2877
+ ', @position' +
2878
+ i +
2879
+ ', @stem' +
2880
+ i +
2881
+ ')',
2882
+ );
2883
+ }
2884
+
2885
+ this.queryRun(
2886
+ `INSERT INTO ${SQLite3Driver.escape(
2887
+ `${this.prefix}tokens_${etype}`,
2888
+ )} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`,
2889
+ {
2890
+ etypes: [etype],
2891
+ params,
2892
+ },
2893
+ );
2894
+ }
2895
+ }
2896
+ }
2897
+ }
2898
+
2899
+ if (only == null) {
2900
+ const uniques = await EntityClass.getUniques({
2901
+ guid,
2902
+ cdate,
2903
+ mdate,
2904
+ tags,
2905
+ data: {},
2906
+ sdata,
2907
+ });
2908
+ for (const unique of uniques) {
2909
+ try {
1730
2910
  this.queryRun(
1731
2911
  `INSERT INTO ${SQLite3Driver.escape(
1732
- `${this.prefix}comparisons_${etype}`
1733
- )} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`,
2912
+ `${this.prefix}uniques_${etype}`,
2913
+ )} ("guid", "unique") VALUES (@guid, @unique);`,
1734
2914
  {
1735
2915
  etypes: [etype],
1736
2916
  params: {
1737
2917
  guid,
1738
- name,
1739
- truthy: uvalue ? 1 : 0,
1740
- string: `${uvalue}`,
1741
- number: Number(uvalue),
2918
+ unique,
1742
2919
  },
1743
- }
2920
+ },
1744
2921
  );
1745
- const references = this.findReferences(value);
1746
- for (const reference of references) {
1747
- this.queryRun(
1748
- `INSERT INTO ${SQLite3Driver.escape(
1749
- `${this.prefix}references_${etype}`
1750
- )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1751
- {
1752
- etypes: [etype],
1753
- params: {
1754
- guid,
1755
- name,
1756
- reference,
1757
- },
1758
- }
2922
+ } catch (e: any) {
2923
+ if (e instanceof EntityUniqueConstraintError) {
2924
+ this.nymph.config.debugError(
2925
+ 'sqlite3',
2926
+ `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
1759
2927
  );
1760
2928
  }
2929
+ throw e;
1761
2930
  }
2931
+ }
2932
+ }
2933
+ } catch (e: any) {
2934
+ this.nymph.config.debugError('sqlite3', `Import entity error: "${e}"`);
2935
+ throw e;
2936
+ }
2937
+ }
2938
+
2939
+ public async importUID({ name, value }: { name: string; value: number }) {
2940
+ try {
2941
+ await this.startTransaction(`nymph-import-uid-${name}`);
2942
+ this.queryRun(
2943
+ `DELETE FROM ${SQLite3Driver.escape(
2944
+ `${this.prefix}uids`,
2945
+ )} WHERE "name"=@name;`,
2946
+ {
2947
+ params: {
2948
+ name,
2949
+ },
1762
2950
  },
1763
- async (name, curUid) => {
1764
- this.queryRun(
1765
- `DELETE FROM ${SQLite3Driver.escape(
1766
- `${this.prefix}uids`
1767
- )} WHERE "name"=@name;`,
1768
- {
1769
- params: {
1770
- name,
1771
- },
1772
- }
1773
- );
1774
- this.queryRun(
1775
- `INSERT INTO ${SQLite3Driver.escape(
1776
- `${this.prefix}uids`
1777
- )} ("name", "cur_uid") VALUES (@name, @curUid);`,
1778
- {
1779
- params: {
1780
- name,
1781
- curUid,
1782
- },
1783
- }
1784
- );
1785
- },
1786
- async () => {
1787
- await this.startTransaction('nymph-import');
2951
+ );
2952
+ this.queryRun(
2953
+ `INSERT INTO ${SQLite3Driver.escape(
2954
+ `${this.prefix}uids`,
2955
+ )} ("name", "cur_uid") VALUES (@name, @value);`,
2956
+ {
2957
+ params: {
2958
+ name,
2959
+ value,
2960
+ },
1788
2961
  },
1789
- async () => {
1790
- await this.commit('nymph-import');
1791
- }
1792
2962
  );
2963
+ await this.commit(`nymph-import-uid-${name}`);
1793
2964
  } catch (e: any) {
1794
- await this.rollback('nymph-import');
2965
+ this.nymph.config.debugError('sqlite3', `Import UID error: "${e}"`);
2966
+ await this.rollback(`nymph-import-uid-${name}`);
1795
2967
  throw e;
1796
2968
  }
1797
2969
  }
@@ -1800,78 +2972,83 @@ export default class SQLite3Driver extends NymphDriver {
1800
2972
  if (name == null) {
1801
2973
  throw new InvalidParametersError('Name not given for UID.');
1802
2974
  }
1803
- this.checkReadOnlyMode();
1804
2975
  await this.startTransaction('nymph-newuid');
2976
+ let curUid: number | undefined = undefined;
1805
2977
  try {
1806
- let curUid =
1807
- this.queryGet(
1808
- `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
1809
- `${this.prefix}uids`
1810
- )} WHERE "name"=@name;`,
1811
- {
1812
- params: {
1813
- name,
2978
+ curUid =
2979
+ (
2980
+ this.queryGet(
2981
+ `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
2982
+ `${this.prefix}uids`,
2983
+ )} WHERE "name"=@name;`,
2984
+ {
2985
+ params: {
2986
+ name,
2987
+ },
1814
2988
  },
1815
- }
2989
+ ) as any
1816
2990
  )?.cur_uid ?? null;
1817
2991
  if (curUid == null) {
1818
2992
  curUid = 1;
1819
2993
  this.queryRun(
1820
2994
  `INSERT INTO ${SQLite3Driver.escape(
1821
- `${this.prefix}uids`
2995
+ `${this.prefix}uids`,
1822
2996
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
1823
2997
  {
1824
2998
  params: {
1825
2999
  name,
1826
3000
  curUid,
1827
3001
  },
1828
- }
3002
+ },
1829
3003
  );
1830
3004
  } else {
1831
3005
  curUid++;
1832
3006
  this.queryRun(
1833
3007
  `UPDATE ${SQLite3Driver.escape(
1834
- `${this.prefix}uids`
3008
+ `${this.prefix}uids`,
1835
3009
  )} SET "cur_uid"=@curUid WHERE "name"=@name;`,
1836
3010
  {
1837
3011
  params: {
1838
3012
  curUid,
1839
3013
  name,
1840
3014
  },
1841
- }
3015
+ },
1842
3016
  );
1843
3017
  }
1844
- await this.commit('nymph-newuid');
1845
- return curUid as number;
1846
3018
  } catch (e: any) {
3019
+ this.nymph.config.debugError('sqlite3', `New UID error: "${e}"`);
1847
3020
  await this.rollback('nymph-newuid');
1848
3021
  throw e;
1849
3022
  }
3023
+
3024
+ await this.commit('nymph-newuid');
3025
+ return curUid as number;
1850
3026
  }
1851
3027
 
1852
3028
  public async renameUID(oldName: string, newName: string) {
1853
3029
  if (oldName == null || newName == null) {
1854
3030
  throw new InvalidParametersError('Name not given for UID.');
1855
3031
  }
1856
- this.checkReadOnlyMode();
3032
+ await this.startTransaction('nymph-rename-uid');
1857
3033
  this.queryRun(
1858
3034
  `UPDATE ${SQLite3Driver.escape(
1859
- `${this.prefix}uids`
3035
+ `${this.prefix}uids`,
1860
3036
  )} SET "name"=@newName WHERE "name"=@oldName;`,
1861
3037
  {
1862
3038
  params: {
1863
3039
  newName,
1864
3040
  oldName,
1865
3041
  },
1866
- }
3042
+ },
1867
3043
  );
3044
+ await this.commit('nymph-rename-uid');
1868
3045
  return true;
1869
3046
  }
1870
3047
 
1871
3048
  public async rollback(name: string) {
1872
3049
  if (name == null || typeof name !== 'string' || name.length === 0) {
1873
3050
  throw new InvalidParametersError(
1874
- 'Transaction rollback attempted without a name.'
3051
+ 'Transaction rollback attempted without a name.',
1875
3052
  );
1876
3053
  }
1877
3054
  if (this.store.transactionsStarted === 0) {
@@ -1879,17 +3056,30 @@ export default class SQLite3Driver extends NymphDriver {
1879
3056
  }
1880
3057
  this.queryRun(`ROLLBACK TO SAVEPOINT ${SQLite3Driver.escape(name)};`);
1881
3058
  this.store.transactionsStarted--;
3059
+
3060
+ if (
3061
+ this.store.transactionsStarted === 0 &&
3062
+ this.store.linkWrite &&
3063
+ !this.config.explicitWrite
3064
+ ) {
3065
+ this.store.linkWrite.exec('PRAGMA optimize;');
3066
+ this.store.linkWrite.close();
3067
+ this.store.linkWrite = undefined;
3068
+ }
3069
+
1882
3070
  return true;
1883
3071
  }
1884
3072
 
1885
3073
  public async saveEntity(entity: EntityInterface) {
1886
- this.checkReadOnlyMode();
1887
3074
  const insertData = (
1888
3075
  guid: string,
1889
3076
  data: EntityData,
1890
3077
  sdata: SerializedEntityData,
1891
- etype: string
3078
+ uniques: string[],
3079
+ etype: string,
1892
3080
  ) => {
3081
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
3082
+
1893
3083
  const runInsertQuery = (name: string, value: any, svalue: string) => {
1894
3084
  if (value === undefined) {
1895
3085
  return;
@@ -1898,41 +3088,33 @@ export default class SQLite3Driver extends NymphDriver {
1898
3088
  typeof value === 'number'
1899
3089
  ? 'N'
1900
3090
  : typeof value === 'string'
1901
- ? 'S'
1902
- : svalue;
3091
+ ? 'S'
3092
+ : 'J';
3093
+ const jsonValue = storageValue === 'J' ? svalue : null;
3094
+
1903
3095
  this.queryRun(
1904
3096
  `INSERT INTO ${SQLite3Driver.escape(
1905
- `${this.prefix}data_${etype}`
1906
- )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
3097
+ `${this.prefix}data_${etype}`,
3098
+ )} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (@guid, @name, @storageValue, jsonb(@jsonValue), @string, @number, @truthy);`,
1907
3099
  {
1908
3100
  etypes: [etype],
1909
3101
  params: {
1910
3102
  guid,
1911
3103
  name,
1912
3104
  storageValue,
1913
- },
1914
- }
1915
- );
1916
- this.queryRun(
1917
- `INSERT INTO ${SQLite3Driver.escape(
1918
- `${this.prefix}comparisons_${etype}`
1919
- )} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`,
1920
- {
1921
- etypes: [etype],
1922
- params: {
1923
- guid,
1924
- name,
1925
- truthy: value ? 1 : 0,
1926
- string: `${value}`,
3105
+ jsonValue,
3106
+ string: storageValue === 'J' ? null : `${value}`,
1927
3107
  number: Number(value),
3108
+ truthy: value ? 1 : 0,
1928
3109
  },
1929
- }
3110
+ },
1930
3111
  );
3112
+
1931
3113
  const references = this.findReferences(svalue);
1932
3114
  for (const reference of references) {
1933
3115
  this.queryRun(
1934
3116
  `INSERT INTO ${SQLite3Driver.escape(
1935
- `${this.prefix}references_${etype}`
3117
+ `${this.prefix}references_${etype}`,
1936
3118
  )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1937
3119
  {
1938
3120
  etypes: [etype],
@@ -1941,10 +3123,80 @@ export default class SQLite3Driver extends NymphDriver {
1941
3123
  name,
1942
3124
  reference,
1943
3125
  },
1944
- }
3126
+ },
1945
3127
  );
1946
3128
  }
3129
+
3130
+ let tokenString: string | null = null;
3131
+ try {
3132
+ tokenString = EntityClass.getFTSText(name, value);
3133
+ } catch (e: any) {
3134
+ // Ignore error.
3135
+ }
3136
+
3137
+ if (tokenString != null) {
3138
+ const tokens = this.tokenizer.tokenize(tokenString);
3139
+ while (tokens.length) {
3140
+ const currentTokens = tokens.splice(0, 100);
3141
+ const params: { [k: string]: any } = {
3142
+ guid,
3143
+ name,
3144
+ };
3145
+ const values: string[] = [];
3146
+
3147
+ for (let i = 0; i < currentTokens.length; i++) {
3148
+ const token = currentTokens[i];
3149
+ params['token' + i] = token.token;
3150
+ params['position' + i] = token.position;
3151
+ params['stem' + i] = token.stem ? 1 : 0;
3152
+ values.push(
3153
+ '(@guid, @name, @token' +
3154
+ i +
3155
+ ', @position' +
3156
+ i +
3157
+ ', @stem' +
3158
+ i +
3159
+ ')',
3160
+ );
3161
+ }
3162
+
3163
+ this.queryRun(
3164
+ `INSERT INTO ${SQLite3Driver.escape(
3165
+ `${this.prefix}tokens_${etype}`,
3166
+ )} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`,
3167
+ {
3168
+ etypes: [etype],
3169
+ params,
3170
+ },
3171
+ );
3172
+ }
3173
+ }
1947
3174
  };
3175
+
3176
+ for (const unique of uniques) {
3177
+ try {
3178
+ this.queryRun(
3179
+ `INSERT INTO ${SQLite3Driver.escape(
3180
+ `${this.prefix}uniques_${etype}`,
3181
+ )} ("guid", "unique") VALUES (@guid, @unique);`,
3182
+ {
3183
+ etypes: [etype],
3184
+ params: {
3185
+ guid,
3186
+ unique,
3187
+ },
3188
+ },
3189
+ );
3190
+ } catch (e: any) {
3191
+ if (e instanceof EntityUniqueConstraintError) {
3192
+ this.nymph.config.debugError(
3193
+ 'sqlite3',
3194
+ `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
3195
+ );
3196
+ }
3197
+ throw e;
3198
+ }
3199
+ }
1948
3200
  for (const name in data) {
1949
3201
  runInsertQuery(name, data[name], JSON.stringify(data[name]));
1950
3202
  }
@@ -1952,95 +3204,162 @@ export default class SQLite3Driver extends NymphDriver {
1952
3204
  runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1953
3205
  }
1954
3206
  };
3207
+ let inTransaction = false;
1955
3208
  try {
1956
3209
  return this.saveEntityRowLike(
1957
3210
  entity,
1958
- async (_entity, guid, tags, data, sdata, cdate, etype) => {
3211
+ async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
3212
+ if (
3213
+ Object.keys(data).length === 0 &&
3214
+ Object.keys(sdata).length === 0
3215
+ ) {
3216
+ return false;
3217
+ }
3218
+ let {
3219
+ user,
3220
+ group,
3221
+ acUser,
3222
+ acGroup,
3223
+ acOther,
3224
+ acRead,
3225
+ acWrite,
3226
+ acFull,
3227
+ } = this.removeAndReturnACValues(etype, data, sdata);
1959
3228
  this.queryRun(
1960
3229
  `INSERT INTO ${SQLite3Driver.escape(
1961
- `${this.prefix}entities_${etype}`
1962
- )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @cdate);`,
3230
+ `${this.prefix}entities_${etype}`,
3231
+ )} ("guid", "tags", "cdate", "mdate", "user", "group", "acUser", "acGroup", "acOther", "acRead", "acWrite", "acFull") VALUES (@guid, @tags, @cdate, @cdate, @user, @group, @acUser, @acGroup, @acOther, @acRead, @acWrite, @acFull);`,
1963
3232
  {
1964
3233
  etypes: [etype],
1965
3234
  params: {
1966
3235
  guid,
1967
3236
  tags: ',' + tags.join(',') + ',',
1968
3237
  cdate,
3238
+ user,
3239
+ group,
3240
+ acUser,
3241
+ acGroup,
3242
+ acOther,
3243
+ acRead: acRead && ',' + acRead.join(',') + ',',
3244
+ acWrite: acWrite && ',' + acWrite.join(',') + ',',
3245
+ acFull: acFull && ',' + acFull.join(',') + ',',
1969
3246
  },
1970
- }
3247
+ },
1971
3248
  );
1972
- insertData(guid, data, sdata, etype);
3249
+ insertData(guid, data, sdata, uniques, etype);
1973
3250
  return true;
1974
3251
  },
1975
- async (entity, guid, tags, data, sdata, mdate, etype) => {
3252
+ async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
3253
+ if (
3254
+ Object.keys(data).length === 0 &&
3255
+ Object.keys(sdata).length === 0
3256
+ ) {
3257
+ return false;
3258
+ }
3259
+ let {
3260
+ user,
3261
+ group,
3262
+ acUser,
3263
+ acGroup,
3264
+ acOther,
3265
+ acRead,
3266
+ acWrite,
3267
+ acFull,
3268
+ } = this.removeAndReturnACValues(etype, data, sdata);
1976
3269
  const info = this.queryRun(
1977
3270
  `UPDATE ${SQLite3Driver.escape(
1978
- `${this.prefix}entities_${etype}`
1979
- )} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=@guid AND "mdate" <= @emdate;`,
3271
+ `${this.prefix}entities_${etype}`,
3272
+ )} SET "tags"=@tags, "mdate"=@mdate, "user"=@user, "group"=@group, "acUser"=@acUser, "acGroup"=@acGroup, "acOther"=@acOther, "acRead"=@acRead, "acWrite"=@acWrite, "acFull"=@acFull WHERE "guid"=@guid AND "mdate" <= @emdate;`,
1980
3273
  {
1981
3274
  etypes: [etype],
1982
3275
  params: {
1983
3276
  tags: ',' + tags.join(',') + ',',
1984
3277
  mdate,
3278
+ user,
3279
+ group,
3280
+ acUser,
3281
+ acGroup,
3282
+ acOther,
3283
+ acRead: acRead && ',' + acRead.join(',') + ',',
3284
+ acWrite: acWrite && ',' + acWrite.join(',') + ',',
3285
+ acFull: acFull && ',' + acFull.join(',') + ',',
1985
3286
  guid,
1986
3287
  emdate: Number(entity.mdate),
1987
3288
  },
1988
- }
3289
+ },
1989
3290
  );
1990
3291
  let success = false;
1991
3292
  if (info.changes === 1) {
1992
3293
  this.queryRun(
1993
3294
  `DELETE FROM ${SQLite3Driver.escape(
1994
- `${this.prefix}data_${etype}`
3295
+ `${this.prefix}data_${etype}`,
1995
3296
  )} WHERE "guid"=@guid;`,
1996
3297
  {
1997
3298
  etypes: [etype],
1998
3299
  params: {
1999
3300
  guid,
2000
3301
  },
2001
- }
3302
+ },
2002
3303
  );
2003
3304
  this.queryRun(
2004
3305
  `DELETE FROM ${SQLite3Driver.escape(
2005
- `${this.prefix}comparisons_${etype}`
3306
+ `${this.prefix}references_${etype}`,
2006
3307
  )} WHERE "guid"=@guid;`,
2007
3308
  {
2008
3309
  etypes: [etype],
2009
3310
  params: {
2010
3311
  guid,
2011
3312
  },
2012
- }
3313
+ },
2013
3314
  );
2014
3315
  this.queryRun(
2015
3316
  `DELETE FROM ${SQLite3Driver.escape(
2016
- `${this.prefix}references_${etype}`
3317
+ `${this.prefix}tokens_${etype}`,
2017
3318
  )} WHERE "guid"=@guid;`,
2018
3319
  {
2019
3320
  etypes: [etype],
2020
3321
  params: {
2021
3322
  guid,
2022
3323
  },
2023
- }
3324
+ },
3325
+ );
3326
+ this.queryRun(
3327
+ `DELETE FROM ${SQLite3Driver.escape(
3328
+ `${this.prefix}uniques_${etype}`,
3329
+ )} WHERE "guid"=@guid;`,
3330
+ {
3331
+ etypes: [etype],
3332
+ params: {
3333
+ guid,
3334
+ },
3335
+ },
2024
3336
  );
2025
- insertData(guid, data, sdata, etype);
3337
+ insertData(guid, data, sdata, uniques, etype);
2026
3338
  success = true;
2027
3339
  }
2028
3340
  return success;
2029
3341
  },
2030
3342
  async () => {
2031
3343
  await this.startTransaction('nymph-save');
3344
+ inTransaction = true;
2032
3345
  },
2033
3346
  async (success) => {
2034
- if (success) {
2035
- await this.commit('nymph-save');
2036
- } else {
2037
- await this.rollback('nymph-save');
3347
+ if (inTransaction) {
3348
+ inTransaction = false;
3349
+ if (success) {
3350
+ await this.commit('nymph-save');
3351
+ } else {
3352
+ await this.rollback('nymph-save');
3353
+ }
2038
3354
  }
2039
3355
  return success;
2040
- }
3356
+ },
2041
3357
  );
2042
3358
  } catch (e: any) {
2043
- await this.rollback('nymph-save');
3359
+ this.nymph.config.debugError('sqlite3', `Save entity error: "${e}"`);
3360
+ if (inTransaction) {
3361
+ await this.rollback('nymph-save');
3362
+ }
2044
3363
  throw e;
2045
3364
  }
2046
3365
  }
@@ -2049,39 +3368,190 @@ export default class SQLite3Driver extends NymphDriver {
2049
3368
  if (name == null) {
2050
3369
  throw new InvalidParametersError('Name not given for UID.');
2051
3370
  }
2052
- this.checkReadOnlyMode();
3371
+ await this.startTransaction('nymph-set-uid');
2053
3372
  this.queryRun(
2054
3373
  `DELETE FROM ${SQLite3Driver.escape(
2055
- `${this.prefix}uids`
3374
+ `${this.prefix}uids`,
2056
3375
  )} WHERE "name"=@name;`,
2057
3376
  {
2058
3377
  params: {
2059
3378
  name,
2060
3379
  },
2061
- }
3380
+ },
2062
3381
  );
2063
3382
  this.queryRun(
2064
3383
  `INSERT INTO ${SQLite3Driver.escape(
2065
- `${this.prefix}uids`
3384
+ `${this.prefix}uids`,
2066
3385
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2067
3386
  {
2068
3387
  params: {
2069
3388
  name,
2070
3389
  curUid,
2071
3390
  },
2072
- }
3391
+ },
2073
3392
  );
3393
+ await this.commit('nymph-set-uid');
2074
3394
  return true;
2075
3395
  }
2076
3396
 
3397
+ public async internalTransaction(name: string) {
3398
+ await this.startTransaction(name);
3399
+ }
3400
+
2077
3401
  public async startTransaction(name: string) {
2078
3402
  if (name == null || typeof name !== 'string' || name.length === 0) {
2079
3403
  throw new InvalidParametersError(
2080
- 'Transaction start attempted without a name.'
3404
+ 'Transaction start attempted without a name.',
2081
3405
  );
2082
3406
  }
3407
+ if (!this.config.explicitWrite && !this.store.linkWrite) {
3408
+ this._connect(true);
3409
+ }
2083
3410
  this.queryRun(`SAVEPOINT ${SQLite3Driver.escape(name)};`);
2084
3411
  this.store.transactionsStarted++;
2085
3412
  return this.nymph;
2086
3413
  }
3414
+
3415
+ private async removeTilmeldOldRows(etype: string) {
3416
+ await this.startTransaction('nymph-remove-tilmeld-rows');
3417
+ try {
3418
+ for (let name of [
3419
+ 'user',
3420
+ 'group',
3421
+ 'acUser',
3422
+ 'acGroup',
3423
+ 'acOther',
3424
+ 'acRead',
3425
+ 'acWrite',
3426
+ 'acFull',
3427
+ ]) {
3428
+ this.queryRun(
3429
+ `DELETE FROM ${SQLite3Driver.escape(
3430
+ `${this.prefix}data_${etype}`,
3431
+ )} WHERE "name"=@name;`,
3432
+ {
3433
+ etypes: [etype],
3434
+ params: {
3435
+ name,
3436
+ },
3437
+ },
3438
+ );
3439
+ this.queryRun(
3440
+ `DELETE FROM ${SQLite3Driver.escape(
3441
+ `${this.prefix}references_${etype}`,
3442
+ )} WHERE "name"=@name;`,
3443
+ {
3444
+ etypes: [etype],
3445
+ params: {
3446
+ name,
3447
+ },
3448
+ },
3449
+ );
3450
+ this.queryRun(
3451
+ `DELETE FROM ${SQLite3Driver.escape(
3452
+ `${this.prefix}tokens_${etype}`,
3453
+ )} WHERE "name"=@name;`,
3454
+ {
3455
+ etypes: [etype],
3456
+ params: {
3457
+ name,
3458
+ },
3459
+ },
3460
+ );
3461
+ }
3462
+ } catch (e: any) {
3463
+ this.nymph.config.debugError(
3464
+ 'sqlite3',
3465
+ `Remove tilmeld rows error: "${e}"`,
3466
+ );
3467
+ await this.rollback('nymph-remove-tilmeld-rows');
3468
+ throw e;
3469
+ }
3470
+
3471
+ await this.commit('nymph-remove-tilmeld-rows');
3472
+ return true;
3473
+ }
3474
+
3475
+ public async needsMigration(): Promise<
3476
+ 'json' | 'tokens' | 'tilmeldColumns' | false
3477
+ > {
3478
+ const table: any = this.queryGet(
3479
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @prefix LIMIT 1;",
3480
+ {
3481
+ params: {
3482
+ prefix: this.prefix + 'data_' + '%',
3483
+ },
3484
+ },
3485
+ );
3486
+ if (table?.name) {
3487
+ const result: any = this.queryGet(
3488
+ "SELECT 1 AS `exists` FROM pragma_table_info(@table) WHERE `name`='json';",
3489
+ {
3490
+ params: {
3491
+ table: table.name,
3492
+ },
3493
+ },
3494
+ );
3495
+ if (!result?.exists) {
3496
+ return 'json';
3497
+ }
3498
+ }
3499
+ const table2: any = this.queryGet(
3500
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @tokenTable LIMIT 1;",
3501
+ {
3502
+ params: {
3503
+ tokenTable: this.prefix + 'tokens_' + '%',
3504
+ },
3505
+ },
3506
+ );
3507
+ if (!table2 || !table2.name) {
3508
+ return 'tokens';
3509
+ }
3510
+ const table3: any = this.queryGet(
3511
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @prefix LIMIT 1;",
3512
+ {
3513
+ params: {
3514
+ prefix: this.prefix + 'entities_' + '%',
3515
+ },
3516
+ },
3517
+ );
3518
+ if (table3?.name) {
3519
+ const result: any = this.queryGet(
3520
+ "SELECT 1 AS `exists` FROM pragma_table_info(@table) WHERE `name`='user';",
3521
+ {
3522
+ params: {
3523
+ table: table3.name,
3524
+ },
3525
+ },
3526
+ );
3527
+ if (!result?.exists) {
3528
+ return 'tilmeldColumns';
3529
+ }
3530
+ }
3531
+ return false;
3532
+ }
3533
+
3534
+ public async liveMigration(
3535
+ migrationType: 'tokenTables' | 'tilmeldColumns' | 'tilmeldRemoveOldRows',
3536
+ ) {
3537
+ if (migrationType === 'tokenTables') {
3538
+ const etypes = await this.getEtypes();
3539
+
3540
+ for (let etype of etypes) {
3541
+ this.createTokensTable(etype);
3542
+ }
3543
+ } else if (migrationType === 'tilmeldColumns') {
3544
+ const etypes = await this.getEtypes();
3545
+
3546
+ for (let etype of etypes) {
3547
+ this.addTilmeldColumnsAndIndexes(etype);
3548
+ }
3549
+ } else if (migrationType === 'tilmeldRemoveOldRows') {
3550
+ const etypes = await this.getEtypes();
3551
+
3552
+ for (let etype of etypes) {
3553
+ await this.removeTilmeldOldRows(etype);
3554
+ }
3555
+ }
3556
+ }
2087
3557
  }