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

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