@nymphjs/driver-sqlite3 1.0.0-beta.10 → 1.0.0-beta.101

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,7 +53,7 @@ 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
 
@@ -53,6 +63,9 @@ export default class SQLite3Driver extends NymphDriver {
53
63
  constructor(config: Partial<SQLite3DriverConfig>, store?: InternalStore) {
54
64
  super();
55
65
  this.config = { ...defaults, ...config };
66
+ if (this.config.filename === ':memory:') {
67
+ this.config.explicitWrite = true;
68
+ }
56
69
  this.prefix = this.config.prefix;
57
70
  if (store) {
58
71
  this.store = store;
@@ -75,57 +88,107 @@ export default class SQLite3Driver extends NymphDriver {
75
88
  *
76
89
  * @returns Whether this instance is connected to a SQLite3 database.
77
90
  */
78
- public async connect() {
79
- const { filename, fileMustExist, timeout, readonly, wal, verbose } =
80
- this.config;
81
-
91
+ public connect() {
82
92
  if (this.store && this.store.connected) {
83
- return true;
93
+ return Promise.resolve(true);
84
94
  }
85
95
 
86
96
  // Connecting
97
+ this._connect(false);
98
+
99
+ return Promise.resolve(this.store.connected);
100
+ }
101
+
102
+ private _connect(write: boolean) {
103
+ const { filename, fileMustExist, timeout, explicitWrite, wal, verbose } =
104
+ this.config;
105
+
87
106
  try {
88
- const link = new SQLite3(filename, {
89
- readonly,
90
- fileMustExist,
91
- timeout,
92
- verbose,
93
- });
107
+ const setOptions = (link: SQLite3.Database) => {
108
+ // Set database and connection options.
109
+ if (wal) {
110
+ link.pragma('journal_mode = WAL;');
111
+ }
112
+ link.pragma('encoding = "UTF-8";');
113
+ link.pragma('foreign_keys = 1;');
114
+ link.pragma('case_sensitive_like = 1;');
115
+ for (let pragma of this.config.pragmas) {
116
+ link.pragma(pragma);
117
+ }
118
+ // Create the preg_match and regexp functions.
119
+ link.function('regexp', { deterministic: true }, ((
120
+ pattern: string,
121
+ subject: string,
122
+ ) => (this.posixRegexMatch(pattern, subject) ? 1 : 0)) as (
123
+ ...params: any[]
124
+ ) => any);
125
+ };
126
+
127
+ let link: SQLite3.Database;
128
+ try {
129
+ link = new SQLite3(filename, {
130
+ readonly: !explicitWrite && !write,
131
+ fileMustExist,
132
+ timeout,
133
+ verbose,
134
+ });
135
+ } catch (e: any) {
136
+ if (
137
+ e.code === 'SQLITE_CANTOPEN' &&
138
+ !explicitWrite &&
139
+ !write &&
140
+ !this.config.fileMustExist
141
+ ) {
142
+ // This happens when the file doesn't exist and we attempt to open it
143
+ // readonly.
144
+ // First open it in write mode.
145
+ const writeLink = new SQLite3(filename, {
146
+ readonly: false,
147
+ fileMustExist,
148
+ timeout,
149
+ verbose,
150
+ });
151
+ setOptions(writeLink);
152
+ writeLink.close();
153
+ // Now open in readonly.
154
+ link = new SQLite3(filename, {
155
+ readonly: true,
156
+ fileMustExist,
157
+ timeout,
158
+ verbose,
159
+ });
160
+ } else {
161
+ throw e;
162
+ }
163
+ }
94
164
 
95
165
  if (!this.store) {
166
+ if (write) {
167
+ throw new Error(
168
+ 'Tried to open in write without opening in read first.',
169
+ );
170
+ }
96
171
  this.store = new InternalStore(link);
172
+ } else if (write) {
173
+ this.store.linkWrite = link;
97
174
  } else {
98
175
  this.store.link = link;
99
176
  }
100
177
  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
- );
178
+ setOptions(link);
115
179
  } catch (e: any) {
116
180
  if (this.store) {
117
181
  this.store.connected = false;
118
182
  }
119
183
  if (filename === ':memory:') {
120
184
  throw new NotConfiguredError(
121
- "It seems the config hasn't been set up correctly."
185
+ "It seems the config hasn't been set up correctly. Could not connect: " +
186
+ e?.message,
122
187
  );
123
188
  } else {
124
189
  throw new UnableToConnectError('Could not connect: ' + e?.message);
125
190
  }
126
191
  }
127
-
128
- return this.store.connected;
129
192
  }
130
193
 
131
194
  /**
@@ -135,8 +198,16 @@ export default class SQLite3Driver extends NymphDriver {
135
198
  */
136
199
  public async disconnect() {
137
200
  if (this.store.connected) {
138
- this.store.link.exec('PRAGMA optimize;');
201
+ if (this.store.linkWrite && !this.config.explicitWrite) {
202
+ this.store.linkWrite.exec('PRAGMA optimize;');
203
+ this.store.linkWrite.close();
204
+ this.store.linkWrite = undefined;
205
+ }
206
+ if (this.config.explicitWrite) {
207
+ this.store.link.exec('PRAGMA optimize;');
208
+ }
139
209
  this.store.link.close();
210
+ this.store.transactionsStarted = 0;
140
211
  this.store.connected = false;
141
212
  }
142
213
  return this.store.connected;
@@ -155,15 +226,242 @@ export default class SQLite3Driver extends NymphDriver {
155
226
  return this.store.connected;
156
227
  }
157
228
 
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
- }
229
+ private createEntitiesTable(etype: string) {
230
+ // Create the entity table.
231
+ this.queryRun(
232
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
233
+ `${this.prefix}entities_${etype}`,
234
+ )} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL);`,
235
+ );
236
+ this.queryRun(
237
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
238
+ `${this.prefix}entities_${etype}_id_cdate`,
239
+ )} ON ${SQLite3Driver.escape(
240
+ `${this.prefix}entities_${etype}`,
241
+ )} ("cdate");`,
242
+ );
243
+ this.queryRun(
244
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
245
+ `${this.prefix}entities_${etype}_id_mdate`,
246
+ )} ON ${SQLite3Driver.escape(
247
+ `${this.prefix}entities_${etype}`,
248
+ )} ("mdate");`,
249
+ );
250
+ this.queryRun(
251
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
252
+ `${this.prefix}entities_${etype}_id_tags`,
253
+ )} ON ${SQLite3Driver.escape(
254
+ `${this.prefix}entities_${etype}`,
255
+ )} ("tags");`,
256
+ );
257
+ }
258
+
259
+ private createDataTable(etype: string) {
260
+ // Create the data table.
261
+ this.queryRun(
262
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
263
+ `${this.prefix}data_${etype}`,
264
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
265
+ `${this.prefix}entities_${etype}`,
266
+ )} ("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"));`,
267
+ );
268
+ this.queryRun(
269
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
270
+ `${this.prefix}data_${etype}_id_guid`,
271
+ )} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid");`,
272
+ );
273
+ this.queryRun(
274
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
275
+ `${this.prefix}data_${etype}_id_guid_name`,
276
+ )} ON ${SQLite3Driver.escape(
277
+ `${this.prefix}data_${etype}`,
278
+ )} ("guid", "name");`,
279
+ );
280
+ this.queryRun(
281
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
282
+ `${this.prefix}data_${etype}_id_name`,
283
+ )} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("name");`,
284
+ );
285
+ this.queryRun(
286
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
287
+ `${this.prefix}data_${etype}_id_name_string`,
288
+ )} ON ${SQLite3Driver.escape(
289
+ `${this.prefix}data_${etype}`,
290
+ )} ("name", "string");`,
291
+ );
292
+ this.queryRun(
293
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
294
+ `${this.prefix}data_${etype}_id_name_number`,
295
+ )} ON ${SQLite3Driver.escape(
296
+ `${this.prefix}data_${etype}`,
297
+ )} ("name", "number");`,
298
+ );
299
+ this.queryRun(
300
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
301
+ `${this.prefix}data_${etype}_id_guid_name_number`,
302
+ )} ON ${SQLite3Driver.escape(
303
+ `${this.prefix}data_${etype}`,
304
+ )} ("guid", "name", "number");`,
305
+ );
306
+ this.queryRun(
307
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
308
+ `${this.prefix}data_${etype}_id_name_truthy`,
309
+ )} ON ${SQLite3Driver.escape(
310
+ `${this.prefix}data_${etype}`,
311
+ )} ("name", "truthy");`,
312
+ );
313
+ this.queryRun(
314
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
315
+ `${this.prefix}data_${etype}_id_guid_name_truthy`,
316
+ )} ON ${SQLite3Driver.escape(
317
+ `${this.prefix}data_${etype}`,
318
+ )} ("guid", "name", "truthy");`,
319
+ );
320
+ this.queryRun(
321
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
322
+ `${this.prefix}data_${etype}_id_acuserread`,
323
+ )} ON ${SQLite3Driver.escape(
324
+ `${this.prefix}data_${etype}`,
325
+ )} ("guid") WHERE "name"=\'acUser\' AND "number" >= 1;`,
326
+ );
327
+ this.queryRun(
328
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
329
+ `${this.prefix}data_${etype}_id_acgroupread`,
330
+ )} ON ${SQLite3Driver.escape(
331
+ `${this.prefix}data_${etype}`,
332
+ )} ("guid") WHERE "name"=\'acGroup\' AND "number" >= 1;`,
333
+ );
334
+ this.queryRun(
335
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
336
+ `${this.prefix}data_${etype}_id_acotherread`,
337
+ )} ON ${SQLite3Driver.escape(
338
+ `${this.prefix}data_${etype}`,
339
+ )} ("guid") WHERE "name"=\'acOther\' AND "number" >= 1;`,
340
+ );
341
+ this.queryRun(
342
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
343
+ `${this.prefix}data_${etype}_id_acuser`,
344
+ )} ON ${SQLite3Driver.escape(
345
+ `${this.prefix}data_${etype}`,
346
+ )} ("guid") WHERE "name"=\'user\';`,
347
+ );
348
+ this.queryRun(
349
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
350
+ `${this.prefix}data_${etype}_id_acgroup`,
351
+ )} ON ${SQLite3Driver.escape(
352
+ `${this.prefix}data_${etype}`,
353
+ )} ("guid") WHERE "name"=\'group\';`,
354
+ );
355
+ }
356
+
357
+ private createReferencesTable(etype: string) {
358
+ // Create the references table.
359
+ this.queryRun(
360
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
361
+ `${this.prefix}references_${etype}`,
362
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
363
+ `${this.prefix}entities_${etype}`,
364
+ )} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`,
365
+ );
366
+ this.queryRun(
367
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
368
+ `${this.prefix}references_${etype}_id_guid`,
369
+ )} ON ${SQLite3Driver.escape(
370
+ `${this.prefix}references_${etype}`,
371
+ )} ("guid");`,
372
+ );
373
+ this.queryRun(
374
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
375
+ `${this.prefix}references_${etype}_id_name`,
376
+ )} ON ${SQLite3Driver.escape(
377
+ `${this.prefix}references_${etype}`,
378
+ )} ("name");`,
379
+ );
380
+ this.queryRun(
381
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
382
+ `${this.prefix}references_${etype}_id_name_reference`,
383
+ )} ON ${SQLite3Driver.escape(
384
+ `${this.prefix}references_${etype}`,
385
+ )} ("name", "reference");`,
386
+ );
387
+ this.queryRun(
388
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
389
+ `${this.prefix}references_${etype}_id_reference`,
390
+ )} ON ${SQLite3Driver.escape(
391
+ `${this.prefix}references_${etype}`,
392
+ )} ("reference");`,
393
+ );
394
+ this.queryRun(
395
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
396
+ `${this.prefix}references_${etype}_id_guid_name`,
397
+ )} ON ${SQLite3Driver.escape(
398
+ `${this.prefix}references_${etype}`,
399
+ )} ("guid", "name");`,
400
+ );
401
+ this.queryRun(
402
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
403
+ `${this.prefix}references_${etype}_id_guid_name_reference`,
404
+ )} ON ${SQLite3Driver.escape(
405
+ `${this.prefix}references_${etype}`,
406
+ )} ("guid", "name", "reference");`,
407
+ );
408
+ this.queryRun(
409
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
410
+ `${this.prefix}references_${etype}_id_reference_name_guid`,
411
+ )} ON ${SQLite3Driver.escape(
412
+ `${this.prefix}references_${etype}`,
413
+ )} ("reference", "name", "guid");`,
414
+ );
415
+ this.queryRun(
416
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
417
+ `${this.prefix}references_${etype}_id_reference_guid_name`,
418
+ )} ON ${SQLite3Driver.escape(
419
+ `${this.prefix}references_${etype}`,
420
+ )} ("reference", "guid", "name");`,
421
+ );
422
+ this.queryRun(
423
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
424
+ `${this.prefix}references_${etype}_id_guid_reference_nameuser`,
425
+ )} ON ${SQLite3Driver.escape(
426
+ `${this.prefix}references_${etype}`,
427
+ )} ("guid", "reference") WHERE "name"=\'user\';`,
428
+ );
429
+ this.queryRun(
430
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
431
+ `${this.prefix}references_${etype}_id_guid_reference_namegroup`,
432
+ )} ON ${SQLite3Driver.escape(
433
+ `${this.prefix}references_${etype}`,
434
+ )} ("guid", "reference") WHERE "name"=\'group\';`,
435
+ );
436
+ }
437
+
438
+ private createTokensTable(etype: string) {
439
+ // Create the tokens table.
440
+ this.queryRun(
441
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
442
+ `${this.prefix}tokens_${etype}`,
443
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
444
+ `${this.prefix}entities_${etype}`,
445
+ )} ("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"));`,
446
+ );
447
+ this.queryRun(
448
+ `CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(
449
+ `${this.prefix}tokens_${etype}_id_name_token`,
450
+ )} ON ${SQLite3Driver.escape(
451
+ `${this.prefix}tokens_${etype}`,
452
+ )} ("name", "token");`,
453
+ );
454
+ }
455
+
456
+ private createUniquesTable(etype: string) {
457
+ // Create the unique strings table.
458
+ this.queryRun(
459
+ `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
460
+ `${this.prefix}uniques_${etype}`,
461
+ )} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(
462
+ `${this.prefix}entities_${etype}`,
463
+ )} ("guid") ON DELETE CASCADE, "unique" TEXT NOT NULL UNIQUE, PRIMARY KEY("guid", "unique"));`,
464
+ );
167
465
  }
168
466
 
169
467
  /**
@@ -172,160 +470,38 @@ export default class SQLite3Driver extends NymphDriver {
172
470
  * @param etype The entity type to create a table for. If this is blank, the default tables are created.
173
471
  */
174
472
  private createTables(etype: string | null = null) {
175
- this.checkReadOnlyMode();
176
473
  this.startTransaction('nymph-tablecreation');
177
474
  try {
178
475
  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
- );
476
+ this.createEntitiesTable(etype);
477
+ this.createDataTable(etype);
478
+ this.createReferencesTable(etype);
479
+ this.createTokensTable(etype);
480
+ this.createUniquesTable(etype);
307
481
  } else {
308
482
  // Create the UID table.
309
483
  this.queryRun(
310
484
  `CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(
311
- `${this.prefix}uids`
312
- )} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`
485
+ `${this.prefix}uids`,
486
+ )} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`,
313
487
  );
314
488
  }
315
- this.commit('nymph-tablecreation');
316
- return true;
317
489
  } catch (e: any) {
318
490
  this.rollback('nymph-tablecreation');
319
491
  throw e;
320
492
  }
493
+
494
+ this.commit('nymph-tablecreation');
495
+ return true;
321
496
  }
322
497
 
323
498
  private query<T extends () => any>(
324
499
  runQuery: T,
325
500
  query: string,
326
- etypes: string[] = []
501
+ etypes: string[] = [],
327
502
  ): ReturnType<T> {
328
503
  try {
504
+ this.nymph.config.debugInfo('sqlite3:query', query);
329
505
  return runQuery();
330
506
  } catch (e: any) {
331
507
  const errorCode = e?.code;
@@ -343,29 +519,37 @@ export default class SQLite3Driver extends NymphDriver {
343
519
  } catch (e2: any) {
344
520
  throw new QueryFailedError(
345
521
  'Query failed: ' + e2?.code + ' - ' + e2?.message,
346
- query
522
+ query,
347
523
  );
348
524
  }
525
+ } else if (
526
+ errorCode === 'SQLITE_CONSTRAINT_UNIQUE' &&
527
+ errorMsg.match(/^UNIQUE constraint failed: /)
528
+ ) {
529
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
349
530
  } else {
350
531
  throw new QueryFailedError(
351
532
  'Query failed: ' + e?.code + ' - ' + e?.message,
352
- query
533
+ query,
353
534
  );
354
535
  }
355
536
  }
356
537
  }
357
538
 
358
- private queryIter(
539
+ private queryArray(
359
540
  query: string,
360
541
  {
361
542
  etypes = [],
362
543
  params = {},
363
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
544
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
364
545
  ) {
365
546
  return this.query(
366
- () => this.store.link.prepare(query).iterate(params),
547
+ () =>
548
+ (this.store.linkWrite || this.store.link)
549
+ .prepare(query)
550
+ .iterate(params),
367
551
  `${query} -- ${JSON.stringify(params)}`,
368
- etypes
552
+ etypes,
369
553
  );
370
554
  }
371
555
 
@@ -374,12 +558,13 @@ export default class SQLite3Driver extends NymphDriver {
374
558
  {
375
559
  etypes = [],
376
560
  params = {},
377
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
561
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
378
562
  ) {
379
563
  return this.query(
380
- () => this.store.link.prepare(query).get(params),
564
+ () =>
565
+ (this.store.linkWrite || this.store.link).prepare(query).get(params),
381
566
  `${query} -- ${JSON.stringify(params)}`,
382
- etypes
567
+ etypes,
383
568
  );
384
569
  }
385
570
 
@@ -388,19 +573,20 @@ export default class SQLite3Driver extends NymphDriver {
388
573
  {
389
574
  etypes = [],
390
575
  params = {},
391
- }: { etypes?: string[]; params?: { [k: string]: any } } = {}
576
+ }: { etypes?: string[]; params?: { [k: string]: any } } = {},
392
577
  ) {
393
578
  return this.query(
394
- () => this.store.link.prepare(query).run(params),
579
+ () =>
580
+ (this.store.linkWrite || this.store.link).prepare(query).run(params),
395
581
  `${query} -- ${JSON.stringify(params)}`,
396
- etypes
582
+ etypes,
397
583
  );
398
584
  }
399
585
 
400
586
  public async commit(name: string) {
401
587
  if (name == null || typeof name !== 'string' || name.length === 0) {
402
588
  throw new InvalidParametersError(
403
- 'Transaction commit attempted without a name.'
589
+ 'Transaction commit attempted without a name.',
404
590
  );
405
591
  }
406
592
  if (this.store.transactionsStarted === 0) {
@@ -408,12 +594,23 @@ export default class SQLite3Driver extends NymphDriver {
408
594
  }
409
595
  this.queryRun(`RELEASE SAVEPOINT ${SQLite3Driver.escape(name)};`);
410
596
  this.store.transactionsStarted--;
597
+
598
+ if (
599
+ this.store.transactionsStarted === 0 &&
600
+ this.store.linkWrite &&
601
+ !this.config.explicitWrite
602
+ ) {
603
+ this.store.linkWrite.exec('PRAGMA optimize;');
604
+ this.store.linkWrite.close();
605
+ this.store.linkWrite = undefined;
606
+ }
607
+
411
608
  return true;
412
609
  }
413
610
 
414
611
  public async deleteEntityByID(
415
612
  guid: string,
416
- className?: EntityConstructor | string | null
613
+ className?: EntityConstructor | string | null,
417
614
  ) {
418
615
  let EntityClass: EntityConstructor;
419
616
  if (typeof className === 'string' || className == null) {
@@ -423,133 +620,185 @@ export default class SQLite3Driver extends NymphDriver {
423
620
  EntityClass = className;
424
621
  }
425
622
  const etype = EntityClass.ETYPE;
426
- this.checkReadOnlyMode();
427
623
  await this.startTransaction('nymph-delete');
428
624
  try {
429
625
  this.queryRun(
430
626
  `DELETE FROM ${SQLite3Driver.escape(
431
- `${this.prefix}entities_${etype}`
627
+ `${this.prefix}entities_${etype}`,
432
628
  )} WHERE "guid"=@guid;`,
433
629
  {
434
630
  etypes: [etype],
435
631
  params: {
436
632
  guid,
437
633
  },
438
- }
634
+ },
439
635
  );
440
636
  this.queryRun(
441
637
  `DELETE FROM ${SQLite3Driver.escape(
442
- `${this.prefix}data_${etype}`
638
+ `${this.prefix}data_${etype}`,
443
639
  )} WHERE "guid"=@guid;`,
444
640
  {
445
641
  etypes: [etype],
446
642
  params: {
447
643
  guid,
448
644
  },
449
- }
645
+ },
450
646
  );
451
647
  this.queryRun(
452
648
  `DELETE FROM ${SQLite3Driver.escape(
453
- `${this.prefix}comparisons_${etype}`
649
+ `${this.prefix}references_${etype}`,
454
650
  )} WHERE "guid"=@guid;`,
455
651
  {
456
652
  etypes: [etype],
457
653
  params: {
458
654
  guid,
459
655
  },
460
- }
656
+ },
461
657
  );
462
658
  this.queryRun(
463
659
  `DELETE FROM ${SQLite3Driver.escape(
464
- `${this.prefix}references_${etype}`
660
+ `${this.prefix}tokens_${etype}`,
465
661
  )} WHERE "guid"=@guid;`,
466
662
  {
467
663
  etypes: [etype],
468
664
  params: {
469
665
  guid,
470
666
  },
471
- }
667
+ },
668
+ );
669
+ this.queryRun(
670
+ `DELETE FROM ${SQLite3Driver.escape(
671
+ `${this.prefix}uniques_${etype}`,
672
+ )} WHERE "guid"=@guid;`,
673
+ {
674
+ etypes: [etype],
675
+ params: {
676
+ guid,
677
+ },
678
+ },
472
679
  );
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
680
  } catch (e: any) {
681
+ this.nymph.config.debugError('sqlite3', `Delete entity error: "${e}"`);
480
682
  await this.rollback('nymph-delete');
481
683
  throw e;
482
684
  }
685
+
686
+ await this.commit('nymph-delete');
687
+ // Remove any cached versions of this entity.
688
+ if (this.nymph.config.cache) {
689
+ this.cleanCache(guid);
690
+ }
691
+ return true;
483
692
  }
484
693
 
485
694
  public async deleteUID(name: string) {
486
695
  if (!name) {
487
696
  throw new InvalidParametersError('Name not given for UID');
488
697
  }
489
- this.checkReadOnlyMode();
698
+ await this.startTransaction('nymph-delete-uid');
490
699
  this.queryRun(
491
700
  `DELETE FROM ${SQLite3Driver.escape(
492
- `${this.prefix}uids`
701
+ `${this.prefix}uids`,
493
702
  )} WHERE "name"=@name;`,
494
703
  {
495
704
  params: {
496
705
  name,
497
706
  },
498
- }
707
+ },
499
708
  );
709
+ await this.commit('nymph-delete-uid');
500
710
  return true;
501
711
  }
502
712
 
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('');
713
+ public async getEtypes() {
714
+ const tables: IterableIterator<any> = this.queryArray(
715
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @prefix;",
716
+ {
717
+ params: {
718
+ prefix: this.prefix + 'entities_' + '%',
719
+ },
720
+ },
721
+ );
722
+ const etypes: string[] = [];
723
+ for (const table of tables) {
724
+ etypes.push(table.name.substr((this.prefix + 'entities_').length));
725
+ }
726
+
727
+ return etypes;
728
+ }
729
+
730
+ public async *exportDataIterator(): AsyncGenerator<
731
+ { type: 'comment' | 'uid' | 'entity'; content: string },
732
+ void,
733
+ false | undefined
734
+ > {
735
+ if (
736
+ yield {
737
+ type: 'comment',
738
+ content: `#nex2
739
+ # Nymph Entity Exchange v2
740
+ # http://nymph.io
741
+ #
742
+ # Generation Time: ${new Date().toLocaleString()}
743
+ `,
744
+ }
745
+ ) {
746
+ return;
747
+ }
510
748
 
511
- writeLine('#');
512
- writeLine('# UIDs');
513
- writeLine('#');
514
- writeLine('');
749
+ if (
750
+ yield {
751
+ type: 'comment',
752
+ content: `
753
+
754
+ #
755
+ # UIDs
756
+ #
757
+
758
+ `,
759
+ }
760
+ ) {
761
+ return;
762
+ }
515
763
 
516
764
  // Export UIDs.
517
- let uids = this.queryIter(
765
+ let uids: IterableIterator<any> = this.queryArray(
518
766
  `SELECT * FROM ${SQLite3Driver.escape(
519
- `${this.prefix}uids`
520
- )} ORDER BY "name";`
767
+ `${this.prefix}uids`,
768
+ )} ORDER BY "name";`,
521
769
  );
522
770
  for (const uid of uids) {
523
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
771
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
772
+ return;
773
+ }
524
774
  }
525
775
 
526
- writeLine('');
527
- writeLine('#');
528
- writeLine('# Entities');
529
- writeLine('#');
530
- writeLine('');
776
+ if (
777
+ yield {
778
+ type: 'comment',
779
+ content: `
531
780
 
532
- // Get the etypes.
533
- const tables = this.queryIter(
534
- "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;"
535
- );
536
- const etypes = [];
537
- for (const table of tables) {
538
- if (table.name.startsWith(this.prefix + 'entities_')) {
539
- etypes.push(table.name.substr((this.prefix + 'entities_').length));
781
+ #
782
+ # Entities
783
+ #
784
+
785
+ `,
540
786
  }
787
+ ) {
788
+ return;
541
789
  }
542
790
 
791
+ // Get the etypes.
792
+ const etypes = await this.getEtypes();
793
+
543
794
  for (const etype of etypes) {
544
795
  // 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}`
796
+ const dataIterator: IterableIterator<any> = this.queryArray(
797
+ `SELECT e.*, d."name", d."value", json(d."json") as "json", d."string", d."number" FROM ${SQLite3Driver.escape(
798
+ `${this.prefix}entities_${etype}`,
548
799
  )} 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";`
800
+ `${this.prefix}data_${etype}`,
801
+ )} d USING ("guid") ORDER BY e."guid";`,
553
802
  )[Symbol.iterator]();
554
803
  let datum = dataIterator.next();
555
804
  while (!datum.done) {
@@ -557,30 +806,36 @@ export default class SQLite3Driver extends NymphDriver {
557
806
  const tags = datum.value.tags.slice(1, -1);
558
807
  const cdate = datum.value.cdate;
559
808
  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) {
809
+ let currentEntityExport: string[] = [];
810
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
811
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
812
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
813
+ if (datum.value.name != null) {
564
814
  // This do will keep going and adding the data until the
565
815
  // next entity is reached. datum will end on the next entity.
566
816
  do {
567
817
  const value =
568
- datum.value.dvalue === 'N'
818
+ datum.value.value === 'N'
569
819
  ? 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}`);
820
+ : datum.value.value === 'S'
821
+ ? JSON.stringify(datum.value.string)
822
+ : datum.value.value === 'J'
823
+ ? datum.value.json
824
+ : datum.value.value;
825
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
574
826
  datum = dataIterator.next();
575
827
  } while (!datum.done && datum.value.guid === guid);
576
828
  } else {
577
829
  // Make sure that datum is incremented :)
578
830
  datum = dataIterator.next();
579
831
  }
832
+ currentEntityExport.push('');
833
+
834
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
835
+ return;
836
+ }
580
837
  }
581
838
  }
582
-
583
- return;
584
839
  }
585
840
 
586
841
  /**
@@ -601,20 +856,21 @@ export default class SQLite3Driver extends NymphDriver {
601
856
  params: { [k: string]: any } = {},
602
857
  subquery = false,
603
858
  tableSuffix = '',
604
- etypes: string[] = []
859
+ etypes: string[] = [],
860
+ guidSelector: string | undefined = undefined,
605
861
  ) {
606
862
  if (typeof options.class?.alterOptions === 'function') {
607
863
  options = options.class.alterOptions(options);
608
864
  }
609
865
  const eTable = `e${tableSuffix}`;
610
866
  const dTable = `d${tableSuffix}`;
611
- const cTable = `c${tableSuffix}`;
612
867
  const fTable = `f${tableSuffix}`;
613
868
  const ieTable = `ie${tableSuffix}`;
869
+ const sTable = `s${tableSuffix}`;
614
870
  const sort = options.sort ?? 'cdate';
615
871
  const queryParts = this.iterateSelectorsForQuery(
616
872
  formattedSelectors,
617
- (key, value, typeIsOr, typeIsNot) => {
873
+ ({ key, value, typeIsOr, typeIsNot }) => {
618
874
  const clauseNot = key.startsWith('!');
619
875
  let curQuery = '';
620
876
  for (const curValue of value) {
@@ -699,10 +955,11 @@ export default class SQLite3Driver extends NymphDriver {
699
955
  const name = `param${++count.i}`;
700
956
  curQuery +=
701
957
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
958
+ 'EXISTS (SELECT "guid" FROM ' +
959
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
960
+ ' WHERE "guid"=' +
702
961
  ieTable +
703
- '."guid" IN (SELECT "guid" FROM ' +
704
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
705
- ' WHERE "name"=@' +
962
+ '."guid" AND "name"=@' +
706
963
  name +
707
964
  ' AND "truthy"=1)';
708
965
  params[name] = curVar;
@@ -743,10 +1000,11 @@ export default class SQLite3Driver extends NymphDriver {
743
1000
  const value = `param${++count.i}`;
744
1001
  curQuery +=
745
1002
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1003
+ 'EXISTS (SELECT "guid" FROM ' +
1004
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1005
+ ' WHERE "guid"=' +
746
1006
  ieTable +
747
- '."guid" IN (SELECT "guid" FROM ' +
748
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
749
- ' WHERE "name"=@' +
1007
+ '."guid" AND "name"=@' +
750
1008
  name +
751
1009
  ' AND "number"=@' +
752
1010
  value +
@@ -761,10 +1019,11 @@ export default class SQLite3Driver extends NymphDriver {
761
1019
  const value = `param${++count.i}`;
762
1020
  curQuery +=
763
1021
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1022
+ 'EXISTS (SELECT "guid" FROM ' +
1023
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1024
+ ' WHERE "guid"=' +
764
1025
  ieTable +
765
- '."guid" IN (SELECT "guid" FROM ' +
766
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
767
- ' WHERE "name"=@' +
1026
+ '."guid" AND "name"=@' +
768
1027
  name +
769
1028
  ' AND "string"=@' +
770
1029
  value +
@@ -788,14 +1047,15 @@ export default class SQLite3Driver extends NymphDriver {
788
1047
  const value = `param${++count.i}`;
789
1048
  curQuery +=
790
1049
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
791
- ieTable +
792
- '."guid" IN (SELECT "guid" FROM ' +
1050
+ 'EXISTS (SELECT "guid" FROM ' +
793
1051
  SQLite3Driver.escape(this.prefix + 'data_' + etype) +
794
- ' WHERE "name"=@' +
1052
+ ' WHERE "guid"=' +
1053
+ ieTable +
1054
+ '."guid" AND "name"=@' +
795
1055
  name +
796
- ' AND "value"=@' +
1056
+ ' AND "json"=jsonb(@' +
797
1057
  value +
798
- ')';
1058
+ '))';
799
1059
  params[name] = curValue[0];
800
1060
  params[value] = svalue;
801
1061
  }
@@ -810,7 +1070,7 @@ export default class SQLite3Driver extends NymphDriver {
810
1070
  curQuery +=
811
1071
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
812
1072
  ieTable +
813
- '."cdate"=' +
1073
+ '."cdate"=@' +
814
1074
  cdate;
815
1075
  params[cdate] = Number(curValue[1]);
816
1076
  break;
@@ -822,90 +1082,210 @@ export default class SQLite3Driver extends NymphDriver {
822
1082
  curQuery +=
823
1083
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
824
1084
  ieTable +
825
- '."mdate"=' +
1085
+ '."mdate"=@' +
826
1086
  mdate;
827
1087
  params[mdate] = Number(curValue[1]);
828
1088
  break;
829
1089
  } else {
1090
+ const containTableSuffix = makeTableSuffix();
830
1091
  if (curQuery) {
831
1092
  curQuery += typeIsOr ? ' OR ' : ' AND ';
832
1093
  }
833
1094
  let svalue: string;
834
- let stringValue: string;
835
1095
  if (
836
1096
  curValue[1] instanceof Object &&
837
1097
  typeof curValue[1].toReference === 'function'
838
1098
  ) {
839
1099
  svalue = JSON.stringify(curValue[1].toReference());
840
- stringValue = `${curValue[1].toReference()}`;
841
1100
  } else {
842
1101
  svalue = JSON.stringify(curValue[1]);
843
- stringValue = `${curValue[1]}`;
844
1102
  }
845
1103
  const name = `param${++count.i}`;
846
1104
  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;
869
- } else {
870
- curQuery +=
871
- (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
- '))';
880
- }
1105
+ curQuery +=
1106
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1107
+ 'EXISTS (SELECT "guid" FROM ' +
1108
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1109
+ ' d' +
1110
+ containTableSuffix +
1111
+ ' WHERE "guid"=' +
1112
+ ieTable +
1113
+ '."guid" AND "name"=@' +
1114
+ name +
1115
+ ' AND json(@' +
1116
+ value +
1117
+ ') IN (SELECT json_quote("value") FROM json_each(d' +
1118
+ containTableSuffix +
1119
+ '."json")))';
881
1120
  params[name] = curValue[0];
882
1121
  params[value] = svalue;
883
1122
  }
884
1123
  break;
885
- case 'match':
886
- case '!match':
887
- if (curValue[0] === 'cdate') {
1124
+ case 'search':
1125
+ case '!search':
1126
+ if (curValue[0] === 'cdate' || curValue[0] === 'mdate') {
888
1127
  if (curQuery) {
889
1128
  curQuery += typeIsOr ? ' OR ' : ' AND ';
890
1129
  }
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];
1130
+ curQuery += (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(0)';
900
1131
  break;
901
- } else if (curValue[0] === 'mdate') {
1132
+ } else {
902
1133
  if (curQuery) {
903
1134
  curQuery += typeIsOr ? ' OR ' : ' AND ';
904
1135
  }
905
- const mdate = `param${++count.i}`;
906
- curQuery +=
907
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
908
- '(' +
1136
+
1137
+ const name = `param${++count.i}`;
1138
+
1139
+ const queryPartToken = (term: SearchTerm) => {
1140
+ const value = `param${++count.i}`;
1141
+ params[value] = term.token;
1142
+ return (
1143
+ 'EXISTS (SELECT "guid" FROM ' +
1144
+ SQLite3Driver.escape(this.prefix + 'tokens_' + etype) +
1145
+ ' WHERE "guid"=' +
1146
+ ieTable +
1147
+ '."guid" AND "name"=@' +
1148
+ name +
1149
+ ' AND "token"=@' +
1150
+ value +
1151
+ (term.nostemmed ? ' AND "stem"=0' : '') +
1152
+ ')'
1153
+ );
1154
+ };
1155
+
1156
+ const queryPartSeries = (series: SearchSeriesTerm) => {
1157
+ const tokenTableSuffix = makeTableSuffix();
1158
+ const tokenParts = series.tokens.map((token, i) => {
1159
+ const value = `param${++count.i}`;
1160
+ params[value] = token.token;
1161
+ return {
1162
+ fromClause:
1163
+ i === 0
1164
+ ? 'FROM ' +
1165
+ SQLite3Driver.escape(
1166
+ this.prefix + 'tokens_' + etype,
1167
+ ) +
1168
+ ' t' +
1169
+ tokenTableSuffix +
1170
+ '0'
1171
+ : 'JOIN ' +
1172
+ SQLite3Driver.escape(
1173
+ this.prefix + 'tokens_' + etype,
1174
+ ) +
1175
+ ' t' +
1176
+ tokenTableSuffix +
1177
+ i +
1178
+ ' ON t' +
1179
+ tokenTableSuffix +
1180
+ i +
1181
+ '."guid" = t' +
1182
+ tokenTableSuffix +
1183
+ '0."guid" AND t' +
1184
+ tokenTableSuffix +
1185
+ i +
1186
+ '."name" = t' +
1187
+ tokenTableSuffix +
1188
+ '0."name" AND t' +
1189
+ tokenTableSuffix +
1190
+ i +
1191
+ '."position" = t' +
1192
+ tokenTableSuffix +
1193
+ '0."position" + ' +
1194
+ i,
1195
+ whereClause:
1196
+ 't' +
1197
+ tokenTableSuffix +
1198
+ i +
1199
+ '."token"=@' +
1200
+ value +
1201
+ (token.nostemmed
1202
+ ? ' AND t' + tokenTableSuffix + i + '."stem"=0'
1203
+ : ''),
1204
+ };
1205
+ });
1206
+ return (
1207
+ 'EXISTS (SELECT t' +
1208
+ tokenTableSuffix +
1209
+ '0."guid" ' +
1210
+ tokenParts.map((part) => part.fromClause).join(' ') +
1211
+ ' WHERE t' +
1212
+ tokenTableSuffix +
1213
+ '0."guid"=' +
1214
+ ieTable +
1215
+ '."guid" AND t' +
1216
+ tokenTableSuffix +
1217
+ '0."name"=@' +
1218
+ name +
1219
+ ' AND ' +
1220
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
1221
+ ')'
1222
+ );
1223
+ };
1224
+
1225
+ const queryPartTerm = (
1226
+ term:
1227
+ | SearchTerm
1228
+ | SearchOrTerm
1229
+ | SearchNotTerm
1230
+ | SearchSeriesTerm,
1231
+ ): string => {
1232
+ if (term.type === 'series') {
1233
+ return queryPartSeries(term);
1234
+ } else if (term.type === 'not') {
1235
+ return 'NOT ' + queryPartTerm(term.operand);
1236
+ } else if (term.type === 'or') {
1237
+ let queryParts: string[] = [];
1238
+ for (let operand of term.operands) {
1239
+ queryParts.push(queryPartTerm(operand));
1240
+ }
1241
+ return '(' + queryParts.join(' OR ') + ')';
1242
+ }
1243
+ return queryPartToken(term);
1244
+ };
1245
+
1246
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(
1247
+ curValue[1],
1248
+ );
1249
+
1250
+ // Run through the query and add terms.
1251
+ let termStrings: string[] = [];
1252
+ for (let term of parsedFTSQuery) {
1253
+ termStrings.push(queryPartTerm(term));
1254
+ }
1255
+
1256
+ curQuery +=
1257
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1258
+ '(' +
1259
+ termStrings.join(' AND ') +
1260
+ ')';
1261
+
1262
+ params[name] = curValue[0];
1263
+ }
1264
+ break;
1265
+ case 'match':
1266
+ case '!match':
1267
+ if (curValue[0] === 'cdate') {
1268
+ if (curQuery) {
1269
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1270
+ }
1271
+ const cdate = `param${++count.i}`;
1272
+ curQuery +=
1273
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1274
+ '(' +
1275
+ ieTable +
1276
+ '."cdate" REGEXP @' +
1277
+ cdate +
1278
+ ')';
1279
+ params[cdate] = curValue[1];
1280
+ break;
1281
+ } else if (curValue[0] === 'mdate') {
1282
+ if (curQuery) {
1283
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1284
+ }
1285
+ const mdate = `param${++count.i}`;
1286
+ curQuery +=
1287
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1288
+ '(' +
909
1289
  ieTable +
910
1290
  '."mdate" REGEXP @' +
911
1291
  mdate +
@@ -920,10 +1300,11 @@ export default class SQLite3Driver extends NymphDriver {
920
1300
  const value = `param${++count.i}`;
921
1301
  curQuery +=
922
1302
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1303
+ 'EXISTS (SELECT "guid" FROM ' +
1304
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1305
+ ' WHERE "guid"=' +
923
1306
  ieTable +
924
- '."guid" IN (SELECT "guid" FROM ' +
925
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
926
- ' WHERE "name"=@' +
1307
+ '."guid" AND "name"=@' +
927
1308
  name +
928
1309
  ' AND "string" REGEXP @' +
929
1310
  value +
@@ -970,10 +1351,11 @@ export default class SQLite3Driver extends NymphDriver {
970
1351
  const value = `param${++count.i}`;
971
1352
  curQuery +=
972
1353
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1354
+ 'EXISTS (SELECT "guid" FROM ' +
1355
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1356
+ ' WHERE "guid"=' +
973
1357
  ieTable +
974
- '."guid" IN (SELECT "guid" FROM ' +
975
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
976
- ' WHERE "name"=@' +
1358
+ '."guid" AND "name"=@' +
977
1359
  name +
978
1360
  ' AND lower("string") REGEXP lower(@' +
979
1361
  value +
@@ -1020,10 +1402,11 @@ export default class SQLite3Driver extends NymphDriver {
1020
1402
  const value = `param${++count.i}`;
1021
1403
  curQuery +=
1022
1404
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1405
+ 'EXISTS (SELECT "guid" FROM ' +
1406
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1407
+ ' WHERE "guid"=' +
1023
1408
  ieTable +
1024
- '."guid" IN (SELECT "guid" FROM ' +
1025
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1026
- ' WHERE "name"=@' +
1409
+ '."guid" AND "name"=@' +
1027
1410
  name +
1028
1411
  ' AND "string" LIKE @' +
1029
1412
  value +
@@ -1070,10 +1453,11 @@ export default class SQLite3Driver extends NymphDriver {
1070
1453
  const value = `param${++count.i}`;
1071
1454
  curQuery +=
1072
1455
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1456
+ 'EXISTS (SELECT "guid" FROM ' +
1457
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1458
+ ' WHERE "guid"=' +
1073
1459
  ieTable +
1074
- '."guid" IN (SELECT "guid" FROM ' +
1075
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1076
- ' WHERE "name"=@' +
1460
+ '."guid" AND "name"=@' +
1077
1461
  name +
1078
1462
  ' AND lower("string") LIKE lower(@' +
1079
1463
  value +
@@ -1116,10 +1500,11 @@ export default class SQLite3Driver extends NymphDriver {
1116
1500
  const value = `param${++count.i}`;
1117
1501
  curQuery +=
1118
1502
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1503
+ 'EXISTS (SELECT "guid" FROM ' +
1504
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1505
+ ' WHERE "guid"=' +
1119
1506
  ieTable +
1120
- '."guid" IN (SELECT "guid" FROM ' +
1121
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1122
- ' WHERE "name"=@' +
1507
+ '."guid" AND "name"=@' +
1123
1508
  name +
1124
1509
  ' AND "number">@' +
1125
1510
  value +
@@ -1162,10 +1547,11 @@ export default class SQLite3Driver extends NymphDriver {
1162
1547
  const value = `param${++count.i}`;
1163
1548
  curQuery +=
1164
1549
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1550
+ 'EXISTS (SELECT "guid" FROM ' +
1551
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1552
+ ' WHERE "guid"=' +
1165
1553
  ieTable +
1166
- '."guid" IN (SELECT "guid" FROM ' +
1167
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1168
- ' WHERE "name"=@' +
1554
+ '."guid" AND "name"=@' +
1169
1555
  name +
1170
1556
  ' AND "number">=@' +
1171
1557
  value +
@@ -1208,10 +1594,11 @@ export default class SQLite3Driver extends NymphDriver {
1208
1594
  const value = `param${++count.i}`;
1209
1595
  curQuery +=
1210
1596
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1597
+ 'EXISTS (SELECT "guid" FROM ' +
1598
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1599
+ ' WHERE "guid"=' +
1211
1600
  ieTable +
1212
- '."guid" IN (SELECT "guid" FROM ' +
1213
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1214
- ' WHERE "name"=@' +
1601
+ '."guid" AND "name"=@' +
1215
1602
  name +
1216
1603
  ' AND "number"<@' +
1217
1604
  value +
@@ -1254,10 +1641,11 @@ export default class SQLite3Driver extends NymphDriver {
1254
1641
  const value = `param${++count.i}`;
1255
1642
  curQuery +=
1256
1643
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1644
+ 'EXISTS (SELECT "guid" FROM ' +
1645
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1646
+ ' WHERE "guid"=' +
1257
1647
  ieTable +
1258
- '."guid" IN (SELECT "guid" FROM ' +
1259
- SQLite3Driver.escape(this.prefix + 'comparisons_' + etype) +
1260
- ' WHERE "name"=@' +
1648
+ '."guid" AND "name"=@' +
1261
1649
  name +
1262
1650
  ' AND "number"<=@' +
1263
1651
  value +
@@ -1283,10 +1671,11 @@ export default class SQLite3Driver extends NymphDriver {
1283
1671
  const guid = `param${++count.i}`;
1284
1672
  curQuery +=
1285
1673
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1286
- ieTable +
1287
- '."guid" IN (SELECT "guid" FROM ' +
1674
+ 'EXISTS (SELECT "guid" FROM ' +
1288
1675
  SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1289
- ' WHERE "name"=@' +
1676
+ ' WHERE "guid"=' +
1677
+ ieTable +
1678
+ '."guid" AND "name"=@' +
1290
1679
  name +
1291
1680
  ' AND "reference"=@' +
1292
1681
  guid +
@@ -1304,7 +1693,7 @@ export default class SQLite3Driver extends NymphDriver {
1304
1693
  params,
1305
1694
  true,
1306
1695
  tableSuffix,
1307
- etypes
1696
+ etypes,
1308
1697
  );
1309
1698
  if (curQuery) {
1310
1699
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1317,9 +1706,10 @@ export default class SQLite3Driver extends NymphDriver {
1317
1706
  break;
1318
1707
  case 'qref':
1319
1708
  case '!qref':
1709
+ const referenceTableSuffix = makeTableSuffix();
1320
1710
  const [qrefOptions, ...qrefSelectors] = curValue[1] as [
1321
1711
  Options,
1322
- ...FormattedSelector[]
1712
+ ...FormattedSelector[],
1323
1713
  ];
1324
1714
  const QrefEntityClass = qrefOptions.class as EntityConstructor;
1325
1715
  etypes.push(QrefEntityClass.ETYPE);
@@ -1331,7 +1721,8 @@ export default class SQLite3Driver extends NymphDriver {
1331
1721
  params,
1332
1722
  false,
1333
1723
  makeTableSuffix(),
1334
- etypes
1724
+ etypes,
1725
+ 'r' + referenceTableSuffix + '."reference"',
1335
1726
  );
1336
1727
  if (curQuery) {
1337
1728
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1339,12 +1730,19 @@ export default class SQLite3Driver extends NymphDriver {
1339
1730
  const qrefName = `param${++count.i}`;
1340
1731
  curQuery +=
1341
1732
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1342
- ieTable +
1343
- '."guid" IN (SELECT "guid" FROM ' +
1733
+ 'EXISTS (SELECT "guid" FROM ' +
1344
1734
  SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1345
- ' WHERE "name"=@' +
1735
+ ' r' +
1736
+ referenceTableSuffix +
1737
+ ' WHERE r' +
1738
+ referenceTableSuffix +
1739
+ '."guid"=' +
1740
+ ieTable +
1741
+ '."guid" AND r' +
1742
+ referenceTableSuffix +
1743
+ '."name"=@' +
1346
1744
  qrefName +
1347
- ' AND "reference" IN (' +
1745
+ ' AND EXISTS (' +
1348
1746
  qrefQuery.query +
1349
1747
  '))';
1350
1748
  params[qrefName] = curValue[0];
@@ -1352,22 +1750,35 @@ export default class SQLite3Driver extends NymphDriver {
1352
1750
  }
1353
1751
  }
1354
1752
  return curQuery;
1355
- }
1753
+ },
1356
1754
  );
1357
1755
 
1358
1756
  let sortBy: string;
1757
+ let sortByInner: string;
1758
+ let sortJoin = '';
1759
+ const order = options.reverse ? ' DESC' : '';
1359
1760
  switch (sort) {
1360
1761
  case 'mdate':
1361
- sortBy = '"mdate"';
1762
+ sortBy = `${eTable}."mdate"${order}`;
1763
+ sortByInner = `${ieTable}."mdate"${order}`;
1362
1764
  break;
1363
1765
  case 'cdate':
1766
+ sortBy = `${eTable}."cdate"${order}`;
1767
+ sortByInner = `${ieTable}."cdate"${order}`;
1768
+ break;
1364
1769
  default:
1365
- sortBy = '"cdate"';
1770
+ const name = `param${++count.i}`;
1771
+ sortJoin = `LEFT JOIN (
1772
+ SELECT "guid", "string", "number"
1773
+ FROM ${SQLite3Driver.escape(this.prefix + 'data_' + etype)}
1774
+ WHERE "name"=@${name}
1775
+ ORDER BY "number"${order}, "string"${order}
1776
+ ) ${sTable} USING ("guid")`;
1777
+ sortBy = `${sTable}."number"${order}, ${sTable}."string"${order}`;
1778
+ sortByInner = sortBy;
1779
+ params[name] = sort;
1366
1780
  break;
1367
1781
  }
1368
- if (options.reverse) {
1369
- sortBy += ' DESC';
1370
- }
1371
1782
 
1372
1783
  let query: string;
1373
1784
  if (queryParts.length) {
@@ -1383,29 +1794,33 @@ export default class SQLite3Driver extends NymphDriver {
1383
1794
  offset = ` OFFSET ${Math.floor(Number(options.offset))}`;
1384
1795
  }
1385
1796
  const whereClause = queryParts.join(') AND (');
1797
+ const guidClause = guidSelector
1798
+ ? `${ieTable}."guid"=${guidSelector} AND `
1799
+ : '';
1386
1800
  if (options.return === 'count') {
1387
1801
  if (limit || offset) {
1388
1802
  query = `SELECT COUNT("guid") AS "count" FROM (
1389
1803
  SELECT "guid"
1390
1804
  FROM ${SQLite3Driver.escape(
1391
- this.prefix + 'entities_' + etype
1805
+ this.prefix + 'entities_' + etype,
1392
1806
  )} ${ieTable}
1393
- WHERE (${whereClause})${limit}${offset}
1807
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1394
1808
  )`;
1395
1809
  } else {
1396
1810
  query = `SELECT COUNT("guid") AS "count"
1397
1811
  FROM ${SQLite3Driver.escape(
1398
- this.prefix + 'entities_' + etype
1812
+ this.prefix + 'entities_' + etype,
1399
1813
  )} ${ieTable}
1400
- WHERE (${whereClause})`;
1814
+ WHERE ${guidClause}(${whereClause})`;
1401
1815
  }
1402
1816
  } else if (options.return === 'guid') {
1403
1817
  query = `SELECT "guid"
1404
1818
  FROM ${SQLite3Driver.escape(
1405
- this.prefix + 'entities_' + etype
1819
+ this.prefix + 'entities_' + etype,
1406
1820
  )} ${ieTable}
1407
- WHERE (${whereClause})
1408
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1821
+ ${sortJoin}
1822
+ WHERE ${guidClause}(${whereClause})
1823
+ ORDER BY ${sortByInner}, "guid"${limit}${offset}`;
1409
1824
  } else {
1410
1825
  query = `SELECT
1411
1826
  ${eTable}."guid",
@@ -1414,26 +1829,26 @@ export default class SQLite3Driver extends NymphDriver {
1414
1829
  ${eTable}."mdate",
1415
1830
  ${dTable}."name",
1416
1831
  ${dTable}."value",
1417
- ${cTable}."string",
1418
- ${cTable}."number"
1832
+ json(${dTable}."json") as "json",
1833
+ ${dTable}."string",
1834
+ ${dTable}."number"
1419
1835
  FROM ${SQLite3Driver.escape(
1420
- this.prefix + 'entities_' + etype
1836
+ this.prefix + 'entities_' + etype,
1421
1837
  )} ${eTable}
1422
1838
  LEFT JOIN ${SQLite3Driver.escape(
1423
- this.prefix + 'data_' + etype
1839
+ this.prefix + 'data_' + etype,
1424
1840
  )} ${dTable} USING ("guid")
1425
- INNER JOIN ${SQLite3Driver.escape(
1426
- this.prefix + 'comparisons_' + etype
1427
- )} ${cTable} USING ("guid", "name")
1841
+ ${sortJoin}
1428
1842
  INNER JOIN (
1429
1843
  SELECT "guid"
1430
1844
  FROM ${SQLite3Driver.escape(
1431
- this.prefix + 'entities_' + etype
1845
+ this.prefix + 'entities_' + etype,
1432
1846
  )} ${ieTable}
1433
- WHERE (${whereClause})
1434
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1847
+ ${sortJoin}
1848
+ WHERE ${guidClause}(${whereClause})
1849
+ ORDER BY ${sortByInner}${limit}${offset}
1435
1850
  ) ${fTable} USING ("guid")
1436
- ORDER BY ${eTable}.${sortBy}`;
1851
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1437
1852
  }
1438
1853
  }
1439
1854
  } else {
@@ -1448,26 +1863,31 @@ export default class SQLite3Driver extends NymphDriver {
1448
1863
  if ('offset' in options) {
1449
1864
  offset = ` OFFSET ${Math.floor(Number(options.offset))}`;
1450
1865
  }
1866
+ const guidClause = guidSelector
1867
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
1868
+ : '';
1451
1869
  if (options.return === 'count') {
1452
1870
  if (limit || offset) {
1453
1871
  query = `SELECT COUNT("guid") AS "count" FROM (
1454
1872
  SELECT "guid"
1455
1873
  FROM ${SQLite3Driver.escape(
1456
- this.prefix + 'entities_' + etype
1457
- )} ${ieTable}${limit}${offset}
1874
+ this.prefix + 'entities_' + etype,
1875
+ )} ${ieTable}${guidClause}${limit}${offset}
1458
1876
  )`;
1459
1877
  } else {
1460
1878
  query = `SELECT COUNT("guid") AS "count"
1461
1879
  FROM ${SQLite3Driver.escape(
1462
- this.prefix + 'entities_' + etype
1463
- )} ${ieTable}`;
1880
+ this.prefix + 'entities_' + etype,
1881
+ )} ${ieTable}${guidClause}`;
1464
1882
  }
1465
1883
  } else if (options.return === 'guid') {
1466
1884
  query = `SELECT "guid"
1467
1885
  FROM ${SQLite3Driver.escape(
1468
- this.prefix + 'entities_' + etype
1886
+ this.prefix + 'entities_' + etype,
1469
1887
  )} ${ieTable}
1470
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1888
+ ${sortJoin}
1889
+ ${guidClause}
1890
+ ORDER BY ${sortByInner}, "guid"${limit}${offset}`;
1471
1891
  } else {
1472
1892
  if (limit || offset) {
1473
1893
  query = `SELECT
@@ -1477,25 +1897,26 @@ export default class SQLite3Driver extends NymphDriver {
1477
1897
  ${eTable}."mdate",
1478
1898
  ${dTable}."name",
1479
1899
  ${dTable}."value",
1480
- ${cTable}."string",
1481
- ${cTable}."number"
1900
+ json(${dTable}."json") as "json",
1901
+ ${dTable}."string",
1902
+ ${dTable}."number"
1482
1903
  FROM ${SQLite3Driver.escape(
1483
- this.prefix + 'entities_' + etype
1904
+ this.prefix + 'entities_' + etype,
1484
1905
  )} ${eTable}
1485
1906
  LEFT JOIN ${SQLite3Driver.escape(
1486
- this.prefix + 'data_' + etype
1907
+ this.prefix + 'data_' + etype,
1487
1908
  )} ${dTable} USING ("guid")
1488
- INNER JOIN ${SQLite3Driver.escape(
1489
- this.prefix + 'comparisons_' + etype
1490
- )} c USING ("guid", "name")
1909
+ ${sortJoin}
1491
1910
  INNER JOIN (
1492
1911
  SELECT "guid"
1493
1912
  FROM ${SQLite3Driver.escape(
1494
- this.prefix + 'entities_' + etype
1913
+ this.prefix + 'entities_' + etype,
1495
1914
  )} ${ieTable}
1496
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1915
+ ${sortJoin}
1916
+ ${guidClause}
1917
+ ORDER BY ${sortByInner}${limit}${offset}
1497
1918
  ) ${fTable} USING ("guid")
1498
- ORDER BY ${eTable}.${sortBy}`;
1919
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1499
1920
  } else {
1500
1921
  query = `SELECT
1501
1922
  ${eTable}."guid",
@@ -1504,18 +1925,18 @@ export default class SQLite3Driver extends NymphDriver {
1504
1925
  ${eTable}."mdate",
1505
1926
  ${dTable}."name",
1506
1927
  ${dTable}."value",
1507
- ${cTable}."string",
1508
- ${cTable}."number"
1928
+ json(${dTable}."json") as "json",
1929
+ ${dTable}."string",
1930
+ ${dTable}."number"
1509
1931
  FROM ${SQLite3Driver.escape(
1510
- this.prefix + 'entities_' + etype
1932
+ this.prefix + 'entities_' + etype,
1511
1933
  )} ${eTable}
1512
1934
  LEFT JOIN ${SQLite3Driver.escape(
1513
- this.prefix + 'data_' + etype
1935
+ this.prefix + 'data_' + etype,
1514
1936
  )} ${dTable} USING ("guid")
1515
- INNER JOIN ${SQLite3Driver.escape(
1516
- this.prefix + 'comparisons_' + etype
1517
- )} ${cTable} USING ("guid", "name")
1518
- ORDER BY ${eTable}.${sortBy}`;
1937
+ ${sortJoin}
1938
+ ${guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
1939
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1519
1940
  }
1520
1941
  }
1521
1942
  }
@@ -1535,16 +1956,18 @@ export default class SQLite3Driver extends NymphDriver {
1535
1956
  protected performQuery(
1536
1957
  options: Options,
1537
1958
  formattedSelectors: FormattedSelector[],
1538
- etype: string
1959
+ etype: string,
1539
1960
  ): {
1540
1961
  result: any;
1541
1962
  } {
1542
1963
  const { query, params, etypes } = this.makeEntityQuery(
1543
1964
  options,
1544
1965
  formattedSelectors,
1545
- etype
1966
+ etype,
1546
1967
  );
1547
- const result = this.queryIter(query, { etypes, params })[Symbol.iterator]();
1968
+ const result = this.queryArray(query, { etypes, params })[
1969
+ Symbol.iterator
1970
+ ]();
1548
1971
  return {
1549
1972
  result,
1550
1973
  };
@@ -1559,37 +1982,24 @@ export default class SQLite3Driver extends NymphDriver {
1559
1982
  ...selectors: Selector[]
1560
1983
  ): Promise<string[]>;
1561
1984
  public async getEntities<T extends EntityConstructor = EntityConstructor>(
1562
- options?: Options<T>,
1985
+ options: Options<T> & { return: 'object' },
1563
1986
  ...selectors: Selector[]
1564
- ): Promise<ReturnType<T['factorySync']>[]>;
1987
+ ): Promise<EntityObjectType<T>[]>;
1565
1988
  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
1989
  options?: Options<T>,
1582
1990
  ...selectors: Selector[]
1583
- ): ReturnType<T['factorySync']>[];
1584
- protected getEntitiesSync<T extends EntityConstructor = EntityConstructor>(
1991
+ ): Promise<EntityInstanceType<T>[]>;
1992
+ public async getEntities<T extends EntityConstructor = EntityConstructor>(
1585
1993
  options: Options<T> = {},
1586
1994
  ...selectors: Selector[]
1587
- ): ReturnType<T['factorySync']>[] | string[] | number {
1588
- const { result, process } = this.getEntitesRowLike<T>(
1995
+ ): Promise<
1996
+ EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
1997
+ > {
1998
+ const { result, process } = this.getEntitiesRowLike<T>(
1589
1999
  options,
1590
2000
  selectors,
1591
- (options, formattedSelectors, etype) =>
1592
- this.performQuery(options, formattedSelectors, etype),
2001
+ ({ options, selectors, etype }) =>
2002
+ this.performQuery(options, selectors, etype),
1593
2003
  () => {
1594
2004
  const next: any = result.next();
1595
2005
  return next.done ? null : next.value;
@@ -1598,7 +2008,13 @@ export default class SQLite3Driver extends NymphDriver {
1598
2008
  (row) => Number(row.count),
1599
2009
  (row) => row.guid,
1600
2010
  (row) => ({
1601
- tags: row.tags.length > 2 ? row.tags.slice(1, -1).split(',') : [],
2011
+ tags:
2012
+ row.tags.length > 2
2013
+ ? row.tags
2014
+ .slice(1, -1)
2015
+ .split(',')
2016
+ .filter((tag: string) => tag)
2017
+ : [],
1602
2018
  cdate: Number(row.cdate),
1603
2019
  mdate: Number(row.mdate),
1604
2020
  }),
@@ -1608,9 +2024,11 @@ export default class SQLite3Driver extends NymphDriver {
1608
2024
  row.value === 'N'
1609
2025
  ? JSON.stringify(row.number)
1610
2026
  : row.value === 'S'
1611
- ? JSON.stringify(row.string)
1612
- : row.value,
1613
- })
2027
+ ? JSON.stringify(row.string)
2028
+ : row.value === 'J'
2029
+ ? row.json
2030
+ : row.value,
2031
+ }),
1614
2032
  );
1615
2033
  const value = process();
1616
2034
  if (value instanceof Error) {
@@ -1623,175 +2041,304 @@ export default class SQLite3Driver extends NymphDriver {
1623
2041
  if (name == null) {
1624
2042
  throw new InvalidParametersError('Name not given for UID.');
1625
2043
  }
1626
- const result = this.queryGet(
2044
+ const result: any = this.queryGet(
1627
2045
  `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
1628
- `${this.prefix}uids`
2046
+ `${this.prefix}uids`,
1629
2047
  )} WHERE "name"=@name;`,
1630
2048
  {
1631
2049
  params: {
1632
2050
  name: name,
1633
2051
  },
1634
- }
2052
+ },
1635
2053
  );
1636
2054
  return (result?.cur_uid as number | null) ?? null;
1637
2055
  }
1638
2056
 
1639
- public async import(filename: string) {
1640
- this.checkReadOnlyMode();
2057
+ public async importEntity(entity: {
2058
+ guid: string;
2059
+ cdate: number;
2060
+ mdate: number;
2061
+ tags: string[];
2062
+ sdata: SerializedEntityData;
2063
+ etype: string;
2064
+ }) {
2065
+ return await this.importEntityInternal(entity, false);
2066
+ }
2067
+
2068
+ public async importEntityTokens(entity: {
2069
+ guid: string;
2070
+ cdate: number;
2071
+ mdate: number;
2072
+ tags: string[];
2073
+ sdata: SerializedEntityData;
2074
+ etype: string;
2075
+ }) {
2076
+ return await this.importEntityInternal(entity, true);
2077
+ }
2078
+
2079
+ private async importEntityInternal(
2080
+ {
2081
+ guid,
2082
+ cdate,
2083
+ mdate,
2084
+ tags,
2085
+ sdata,
2086
+ etype,
2087
+ }: {
2088
+ guid: string;
2089
+ cdate: number;
2090
+ mdate: number;
2091
+ tags: string[];
2092
+ sdata: SerializedEntityData;
2093
+ etype: string;
2094
+ },
2095
+ onlyTokens: boolean,
2096
+ ) {
1641
2097
  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
- );
2098
+ if (!onlyTokens) {
2099
+ this.queryRun(
2100
+ `DELETE FROM ${SQLite3Driver.escape(
2101
+ `${this.prefix}entities_${etype}`,
2102
+ )} WHERE "guid"=@guid;`,
2103
+ {
2104
+ etypes: [etype],
2105
+ params: {
2106
+ guid,
2107
+ },
2108
+ },
2109
+ );
2110
+ this.queryRun(
2111
+ `DELETE FROM ${SQLite3Driver.escape(
2112
+ `${this.prefix}data_${etype}`,
2113
+ )} WHERE "guid"=@guid;`,
2114
+ {
2115
+ etypes: [etype],
2116
+ params: {
2117
+ guid,
2118
+ },
2119
+ },
2120
+ );
2121
+ this.queryRun(
2122
+ `DELETE FROM ${SQLite3Driver.escape(
2123
+ `${this.prefix}references_${etype}`,
2124
+ )} WHERE "guid"=@guid;`,
2125
+ {
2126
+ etypes: [etype],
2127
+ params: {
2128
+ guid,
2129
+ },
2130
+ },
2131
+ );
2132
+ }
2133
+ this.queryRun(
2134
+ `DELETE FROM ${SQLite3Driver.escape(
2135
+ `${this.prefix}tokens_${etype}`,
2136
+ )} WHERE "guid"=@guid;`,
2137
+ {
2138
+ etypes: [etype],
2139
+ params: {
2140
+ guid,
2141
+ },
2142
+ },
2143
+ );
2144
+ if (!onlyTokens) {
2145
+ this.queryRun(
2146
+ `DELETE FROM ${SQLite3Driver.escape(
2147
+ `${this.prefix}uniques_${etype}`,
2148
+ )} WHERE "guid"=@guid;`,
2149
+ {
2150
+ etypes: [etype],
2151
+ params: {
2152
+ guid,
2153
+ },
2154
+ },
2155
+ );
2156
+ }
2157
+
2158
+ if (!onlyTokens) {
2159
+ this.queryRun(
2160
+ `INSERT INTO ${SQLite3Driver.escape(
2161
+ `${this.prefix}entities_${etype}`,
2162
+ )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`,
2163
+ {
2164
+ etypes: [etype],
2165
+ params: {
2166
+ guid,
2167
+ tags: ',' + tags.join(',') + ',',
2168
+ cdate,
2169
+ mdate,
2170
+ },
2171
+ },
2172
+ );
2173
+
2174
+ for (const name in sdata) {
2175
+ const value = sdata[name];
2176
+ const uvalue = JSON.parse(value);
2177
+ if (value === undefined) {
2178
+ continue;
2179
+ }
2180
+ const storageValue =
2181
+ typeof uvalue === 'number'
2182
+ ? 'N'
2183
+ : typeof uvalue === 'string'
2184
+ ? 'S'
2185
+ : 'J';
2186
+ const jsonValue = storageValue === 'J' ? value : null;
2187
+
1689
2188
  this.queryRun(
1690
2189
  `INSERT INTO ${SQLite3Driver.escape(
1691
- `${this.prefix}entities_${etype}`
1692
- )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`,
2190
+ `${this.prefix}data_${etype}`,
2191
+ )} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (@guid, @name, @storageValue, jsonb(@jsonValue), @string, @number, @truthy);`,
1693
2192
  {
1694
2193
  etypes: [etype],
1695
2194
  params: {
1696
2195
  guid,
1697
- tags: ',' + tags.join(',') + ',',
1698
- cdate: Number(JSON.parse(sdata.cdate)),
1699
- mdate: Number(JSON.parse(sdata.mdate)),
2196
+ name,
2197
+ storageValue,
2198
+ jsonValue,
2199
+ string: storageValue === 'J' ? null : `${uvalue}`,
2200
+ number: Number(uvalue),
2201
+ truthy: uvalue ? 1 : 0,
1700
2202
  },
1701
- }
2203
+ },
1702
2204
  );
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;
2205
+
2206
+ const references = this.findReferences(value);
2207
+ for (const reference of references) {
1717
2208
  this.queryRun(
1718
2209
  `INSERT INTO ${SQLite3Driver.escape(
1719
- `${this.prefix}data_${etype}`
1720
- )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
2210
+ `${this.prefix}references_${etype}`,
2211
+ )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1721
2212
  {
1722
2213
  etypes: [etype],
1723
2214
  params: {
1724
2215
  guid,
1725
2216
  name,
1726
- storageValue,
2217
+ reference,
1727
2218
  },
1728
- }
2219
+ },
2220
+ );
2221
+ }
2222
+ }
2223
+ }
2224
+
2225
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
2226
+
2227
+ for (let name in sdata) {
2228
+ let tokenString: string | null = null;
2229
+ try {
2230
+ tokenString = EntityClass.getFTSText(name, JSON.parse(sdata[name]));
2231
+ } catch (e: any) {
2232
+ // Ignore error.
2233
+ }
2234
+
2235
+ if (tokenString != null) {
2236
+ const tokens = this.tokenizer.tokenize(tokenString);
2237
+ while (tokens.length) {
2238
+ const currentTokens = tokens.splice(0, 100);
2239
+ const params: { [k: string]: any } = {
2240
+ guid,
2241
+ name,
2242
+ };
2243
+ const values: string[] = [];
2244
+
2245
+ for (let i = 0; i < currentTokens.length; i++) {
2246
+ const token = currentTokens[i];
2247
+ params['token' + i] = token.token;
2248
+ params['position' + i] = token.position;
2249
+ params['stem' + i] = token.stem ? 1 : 0;
2250
+ values.push(
2251
+ '(@guid, @name, @token' +
2252
+ i +
2253
+ ', @position' +
2254
+ i +
2255
+ ', @stem' +
2256
+ i +
2257
+ ')',
2258
+ );
2259
+ }
2260
+
2261
+ this.queryRun(
2262
+ `INSERT INTO ${SQLite3Driver.escape(
2263
+ `${this.prefix}tokens_${etype}`,
2264
+ )} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`,
2265
+ {
2266
+ etypes: [etype],
2267
+ params,
2268
+ },
1729
2269
  );
2270
+ }
2271
+ }
2272
+ }
2273
+
2274
+ if (!onlyTokens) {
2275
+ const uniques = await EntityClass.getUniques({
2276
+ guid,
2277
+ cdate,
2278
+ mdate,
2279
+ tags,
2280
+ data: {},
2281
+ sdata,
2282
+ });
2283
+ for (const unique of uniques) {
2284
+ try {
1730
2285
  this.queryRun(
1731
2286
  `INSERT INTO ${SQLite3Driver.escape(
1732
- `${this.prefix}comparisons_${etype}`
1733
- )} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`,
2287
+ `${this.prefix}uniques_${etype}`,
2288
+ )} ("guid", "unique") VALUES (@guid, @unique);`,
1734
2289
  {
1735
2290
  etypes: [etype],
1736
2291
  params: {
1737
2292
  guid,
1738
- name,
1739
- truthy: uvalue ? 1 : 0,
1740
- string: `${uvalue}`,
1741
- number: Number(uvalue),
2293
+ unique,
1742
2294
  },
1743
- }
2295
+ },
1744
2296
  );
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
- }
2297
+ } catch (e: any) {
2298
+ if (e instanceof EntityUniqueConstraintError) {
2299
+ this.nymph.config.debugError(
2300
+ 'sqlite3',
2301
+ `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
1759
2302
  );
1760
2303
  }
2304
+ throw e;
1761
2305
  }
2306
+ }
2307
+ }
2308
+ } catch (e: any) {
2309
+ this.nymph.config.debugError('sqlite3', `Import entity error: "${e}"`);
2310
+ throw e;
2311
+ }
2312
+ }
2313
+
2314
+ public async importUID({ name, value }: { name: string; value: number }) {
2315
+ try {
2316
+ await this.startTransaction(`nymph-import-uid-${name}`);
2317
+ this.queryRun(
2318
+ `DELETE FROM ${SQLite3Driver.escape(
2319
+ `${this.prefix}uids`,
2320
+ )} WHERE "name"=@name;`,
2321
+ {
2322
+ params: {
2323
+ name,
2324
+ },
1762
2325
  },
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');
2326
+ );
2327
+ this.queryRun(
2328
+ `INSERT INTO ${SQLite3Driver.escape(
2329
+ `${this.prefix}uids`,
2330
+ )} ("name", "cur_uid") VALUES (@name, @value);`,
2331
+ {
2332
+ params: {
2333
+ name,
2334
+ value,
2335
+ },
1788
2336
  },
1789
- async () => {
1790
- await this.commit('nymph-import');
1791
- }
1792
2337
  );
2338
+ await this.commit(`nymph-import-uid-${name}`);
1793
2339
  } catch (e: any) {
1794
- await this.rollback('nymph-import');
2340
+ this.nymph.config.debugError('sqlite3', `Import UID error: "${e}"`);
2341
+ await this.rollback(`nymph-import-uid-${name}`);
1795
2342
  throw e;
1796
2343
  }
1797
2344
  }
@@ -1800,78 +2347,83 @@ export default class SQLite3Driver extends NymphDriver {
1800
2347
  if (name == null) {
1801
2348
  throw new InvalidParametersError('Name not given for UID.');
1802
2349
  }
1803
- this.checkReadOnlyMode();
1804
2350
  await this.startTransaction('nymph-newuid');
2351
+ let curUid: number | undefined = undefined;
1805
2352
  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,
2353
+ curUid =
2354
+ (
2355
+ this.queryGet(
2356
+ `SELECT "cur_uid" FROM ${SQLite3Driver.escape(
2357
+ `${this.prefix}uids`,
2358
+ )} WHERE "name"=@name;`,
2359
+ {
2360
+ params: {
2361
+ name,
2362
+ },
1814
2363
  },
1815
- }
2364
+ ) as any
1816
2365
  )?.cur_uid ?? null;
1817
2366
  if (curUid == null) {
1818
2367
  curUid = 1;
1819
2368
  this.queryRun(
1820
2369
  `INSERT INTO ${SQLite3Driver.escape(
1821
- `${this.prefix}uids`
2370
+ `${this.prefix}uids`,
1822
2371
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
1823
2372
  {
1824
2373
  params: {
1825
2374
  name,
1826
2375
  curUid,
1827
2376
  },
1828
- }
2377
+ },
1829
2378
  );
1830
2379
  } else {
1831
2380
  curUid++;
1832
2381
  this.queryRun(
1833
2382
  `UPDATE ${SQLite3Driver.escape(
1834
- `${this.prefix}uids`
2383
+ `${this.prefix}uids`,
1835
2384
  )} SET "cur_uid"=@curUid WHERE "name"=@name;`,
1836
2385
  {
1837
2386
  params: {
1838
2387
  curUid,
1839
2388
  name,
1840
2389
  },
1841
- }
2390
+ },
1842
2391
  );
1843
2392
  }
1844
- await this.commit('nymph-newuid');
1845
- return curUid as number;
1846
2393
  } catch (e: any) {
2394
+ this.nymph.config.debugError('sqlite3', `New UID error: "${e}"`);
1847
2395
  await this.rollback('nymph-newuid');
1848
2396
  throw e;
1849
2397
  }
2398
+
2399
+ await this.commit('nymph-newuid');
2400
+ return curUid as number;
1850
2401
  }
1851
2402
 
1852
2403
  public async renameUID(oldName: string, newName: string) {
1853
2404
  if (oldName == null || newName == null) {
1854
2405
  throw new InvalidParametersError('Name not given for UID.');
1855
2406
  }
1856
- this.checkReadOnlyMode();
2407
+ await this.startTransaction('nymph-rename-uid');
1857
2408
  this.queryRun(
1858
2409
  `UPDATE ${SQLite3Driver.escape(
1859
- `${this.prefix}uids`
2410
+ `${this.prefix}uids`,
1860
2411
  )} SET "name"=@newName WHERE "name"=@oldName;`,
1861
2412
  {
1862
2413
  params: {
1863
2414
  newName,
1864
2415
  oldName,
1865
2416
  },
1866
- }
2417
+ },
1867
2418
  );
2419
+ await this.commit('nymph-rename-uid');
1868
2420
  return true;
1869
2421
  }
1870
2422
 
1871
2423
  public async rollback(name: string) {
1872
2424
  if (name == null || typeof name !== 'string' || name.length === 0) {
1873
2425
  throw new InvalidParametersError(
1874
- 'Transaction rollback attempted without a name.'
2426
+ 'Transaction rollback attempted without a name.',
1875
2427
  );
1876
2428
  }
1877
2429
  if (this.store.transactionsStarted === 0) {
@@ -1879,17 +2431,30 @@ export default class SQLite3Driver extends NymphDriver {
1879
2431
  }
1880
2432
  this.queryRun(`ROLLBACK TO SAVEPOINT ${SQLite3Driver.escape(name)};`);
1881
2433
  this.store.transactionsStarted--;
2434
+
2435
+ if (
2436
+ this.store.transactionsStarted === 0 &&
2437
+ this.store.linkWrite &&
2438
+ !this.config.explicitWrite
2439
+ ) {
2440
+ this.store.linkWrite.exec('PRAGMA optimize;');
2441
+ this.store.linkWrite.close();
2442
+ this.store.linkWrite = undefined;
2443
+ }
2444
+
1882
2445
  return true;
1883
2446
  }
1884
2447
 
1885
2448
  public async saveEntity(entity: EntityInterface) {
1886
- this.checkReadOnlyMode();
1887
2449
  const insertData = (
1888
2450
  guid: string,
1889
2451
  data: EntityData,
1890
2452
  sdata: SerializedEntityData,
1891
- etype: string
2453
+ uniques: string[],
2454
+ etype: string,
1892
2455
  ) => {
2456
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
2457
+
1893
2458
  const runInsertQuery = (name: string, value: any, svalue: string) => {
1894
2459
  if (value === undefined) {
1895
2460
  return;
@@ -1898,41 +2463,33 @@ export default class SQLite3Driver extends NymphDriver {
1898
2463
  typeof value === 'number'
1899
2464
  ? 'N'
1900
2465
  : typeof value === 'string'
1901
- ? 'S'
1902
- : svalue;
2466
+ ? 'S'
2467
+ : 'J';
2468
+ const jsonValue = storageValue === 'J' ? svalue : null;
2469
+
1903
2470
  this.queryRun(
1904
2471
  `INSERT INTO ${SQLite3Driver.escape(
1905
- `${this.prefix}data_${etype}`
1906
- )} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`,
2472
+ `${this.prefix}data_${etype}`,
2473
+ )} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (@guid, @name, @storageValue, jsonb(@jsonValue), @string, @number, @truthy);`,
1907
2474
  {
1908
2475
  etypes: [etype],
1909
2476
  params: {
1910
2477
  guid,
1911
2478
  name,
1912
2479
  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}`,
2480
+ jsonValue,
2481
+ string: storageValue === 'J' ? null : `${value}`,
1927
2482
  number: Number(value),
2483
+ truthy: value ? 1 : 0,
1928
2484
  },
1929
- }
2485
+ },
1930
2486
  );
2487
+
1931
2488
  const references = this.findReferences(svalue);
1932
2489
  for (const reference of references) {
1933
2490
  this.queryRun(
1934
2491
  `INSERT INTO ${SQLite3Driver.escape(
1935
- `${this.prefix}references_${etype}`
2492
+ `${this.prefix}references_${etype}`,
1936
2493
  )} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`,
1937
2494
  {
1938
2495
  etypes: [etype],
@@ -1941,10 +2498,80 @@ export default class SQLite3Driver extends NymphDriver {
1941
2498
  name,
1942
2499
  reference,
1943
2500
  },
1944
- }
2501
+ },
1945
2502
  );
1946
2503
  }
2504
+
2505
+ let tokenString: string | null = null;
2506
+ try {
2507
+ tokenString = EntityClass.getFTSText(name, value);
2508
+ } catch (e: any) {
2509
+ // Ignore error.
2510
+ }
2511
+
2512
+ if (tokenString != null) {
2513
+ const tokens = this.tokenizer.tokenize(tokenString);
2514
+ while (tokens.length) {
2515
+ const currentTokens = tokens.splice(0, 100);
2516
+ const params: { [k: string]: any } = {
2517
+ guid,
2518
+ name,
2519
+ };
2520
+ const values: string[] = [];
2521
+
2522
+ for (let i = 0; i < currentTokens.length; i++) {
2523
+ const token = currentTokens[i];
2524
+ params['token' + i] = token.token;
2525
+ params['position' + i] = token.position;
2526
+ params['stem' + i] = token.stem ? 1 : 0;
2527
+ values.push(
2528
+ '(@guid, @name, @token' +
2529
+ i +
2530
+ ', @position' +
2531
+ i +
2532
+ ', @stem' +
2533
+ i +
2534
+ ')',
2535
+ );
2536
+ }
2537
+
2538
+ this.queryRun(
2539
+ `INSERT INTO ${SQLite3Driver.escape(
2540
+ `${this.prefix}tokens_${etype}`,
2541
+ )} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`,
2542
+ {
2543
+ etypes: [etype],
2544
+ params,
2545
+ },
2546
+ );
2547
+ }
2548
+ }
1947
2549
  };
2550
+
2551
+ for (const unique of uniques) {
2552
+ try {
2553
+ this.queryRun(
2554
+ `INSERT INTO ${SQLite3Driver.escape(
2555
+ `${this.prefix}uniques_${etype}`,
2556
+ )} ("guid", "unique") VALUES (@guid, @unique);`,
2557
+ {
2558
+ etypes: [etype],
2559
+ params: {
2560
+ guid,
2561
+ unique,
2562
+ },
2563
+ },
2564
+ );
2565
+ } catch (e: any) {
2566
+ if (e instanceof EntityUniqueConstraintError) {
2567
+ this.nymph.config.debugError(
2568
+ 'sqlite3',
2569
+ `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`,
2570
+ );
2571
+ }
2572
+ throw e;
2573
+ }
2574
+ }
1948
2575
  for (const name in data) {
1949
2576
  runInsertQuery(name, data[name], JSON.stringify(data[name]));
1950
2577
  }
@@ -1952,13 +2579,20 @@ export default class SQLite3Driver extends NymphDriver {
1952
2579
  runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1953
2580
  }
1954
2581
  };
2582
+ let inTransaction = false;
1955
2583
  try {
1956
2584
  return this.saveEntityRowLike(
1957
2585
  entity,
1958
- async (_entity, guid, tags, data, sdata, cdate, etype) => {
2586
+ async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
2587
+ if (
2588
+ Object.keys(data).length === 0 &&
2589
+ Object.keys(sdata).length === 0
2590
+ ) {
2591
+ return false;
2592
+ }
1959
2593
  this.queryRun(
1960
2594
  `INSERT INTO ${SQLite3Driver.escape(
1961
- `${this.prefix}entities_${etype}`
2595
+ `${this.prefix}entities_${etype}`,
1962
2596
  )} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @cdate);`,
1963
2597
  {
1964
2598
  etypes: [etype],
@@ -1967,15 +2601,21 @@ export default class SQLite3Driver extends NymphDriver {
1967
2601
  tags: ',' + tags.join(',') + ',',
1968
2602
  cdate,
1969
2603
  },
1970
- }
2604
+ },
1971
2605
  );
1972
- insertData(guid, data, sdata, etype);
2606
+ insertData(guid, data, sdata, uniques, etype);
1973
2607
  return true;
1974
2608
  },
1975
- async (entity, guid, tags, data, sdata, mdate, etype) => {
2609
+ async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
2610
+ if (
2611
+ Object.keys(data).length === 0 &&
2612
+ Object.keys(sdata).length === 0
2613
+ ) {
2614
+ return false;
2615
+ }
1976
2616
  const info = this.queryRun(
1977
2617
  `UPDATE ${SQLite3Driver.escape(
1978
- `${this.prefix}entities_${etype}`
2618
+ `${this.prefix}entities_${etype}`,
1979
2619
  )} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=@guid AND "mdate" <= @emdate;`,
1980
2620
  {
1981
2621
  etypes: [etype],
@@ -1985,62 +2625,80 @@ export default class SQLite3Driver extends NymphDriver {
1985
2625
  guid,
1986
2626
  emdate: Number(entity.mdate),
1987
2627
  },
1988
- }
2628
+ },
1989
2629
  );
1990
2630
  let success = false;
1991
2631
  if (info.changes === 1) {
1992
2632
  this.queryRun(
1993
2633
  `DELETE FROM ${SQLite3Driver.escape(
1994
- `${this.prefix}data_${etype}`
2634
+ `${this.prefix}data_${etype}`,
1995
2635
  )} WHERE "guid"=@guid;`,
1996
2636
  {
1997
2637
  etypes: [etype],
1998
2638
  params: {
1999
2639
  guid,
2000
2640
  },
2001
- }
2641
+ },
2002
2642
  );
2003
2643
  this.queryRun(
2004
2644
  `DELETE FROM ${SQLite3Driver.escape(
2005
- `${this.prefix}comparisons_${etype}`
2645
+ `${this.prefix}references_${etype}`,
2006
2646
  )} WHERE "guid"=@guid;`,
2007
2647
  {
2008
2648
  etypes: [etype],
2009
2649
  params: {
2010
2650
  guid,
2011
2651
  },
2012
- }
2652
+ },
2013
2653
  );
2014
2654
  this.queryRun(
2015
2655
  `DELETE FROM ${SQLite3Driver.escape(
2016
- `${this.prefix}references_${etype}`
2656
+ `${this.prefix}tokens_${etype}`,
2017
2657
  )} WHERE "guid"=@guid;`,
2018
2658
  {
2019
2659
  etypes: [etype],
2020
2660
  params: {
2021
2661
  guid,
2022
2662
  },
2023
- }
2663
+ },
2664
+ );
2665
+ this.queryRun(
2666
+ `DELETE FROM ${SQLite3Driver.escape(
2667
+ `${this.prefix}uniques_${etype}`,
2668
+ )} WHERE "guid"=@guid;`,
2669
+ {
2670
+ etypes: [etype],
2671
+ params: {
2672
+ guid,
2673
+ },
2674
+ },
2024
2675
  );
2025
- insertData(guid, data, sdata, etype);
2676
+ insertData(guid, data, sdata, uniques, etype);
2026
2677
  success = true;
2027
2678
  }
2028
2679
  return success;
2029
2680
  },
2030
2681
  async () => {
2031
2682
  await this.startTransaction('nymph-save');
2683
+ inTransaction = true;
2032
2684
  },
2033
2685
  async (success) => {
2034
- if (success) {
2035
- await this.commit('nymph-save');
2036
- } else {
2037
- await this.rollback('nymph-save');
2686
+ if (inTransaction) {
2687
+ inTransaction = false;
2688
+ if (success) {
2689
+ await this.commit('nymph-save');
2690
+ } else {
2691
+ await this.rollback('nymph-save');
2692
+ }
2038
2693
  }
2039
2694
  return success;
2040
- }
2695
+ },
2041
2696
  );
2042
2697
  } catch (e: any) {
2043
- await this.rollback('nymph-save');
2698
+ this.nymph.config.debugError('sqlite3', `Save entity error: "${e}"`);
2699
+ if (inTransaction) {
2700
+ await this.rollback('nymph-save');
2701
+ }
2044
2702
  throw e;
2045
2703
  }
2046
2704
  }
@@ -2049,39 +2707,91 @@ export default class SQLite3Driver extends NymphDriver {
2049
2707
  if (name == null) {
2050
2708
  throw new InvalidParametersError('Name not given for UID.');
2051
2709
  }
2052
- this.checkReadOnlyMode();
2710
+ await this.startTransaction('nymph-set-uid');
2053
2711
  this.queryRun(
2054
2712
  `DELETE FROM ${SQLite3Driver.escape(
2055
- `${this.prefix}uids`
2713
+ `${this.prefix}uids`,
2056
2714
  )} WHERE "name"=@name;`,
2057
2715
  {
2058
2716
  params: {
2059
2717
  name,
2060
2718
  },
2061
- }
2719
+ },
2062
2720
  );
2063
2721
  this.queryRun(
2064
2722
  `INSERT INTO ${SQLite3Driver.escape(
2065
- `${this.prefix}uids`
2723
+ `${this.prefix}uids`,
2066
2724
  )} ("name", "cur_uid") VALUES (@name, @curUid);`,
2067
2725
  {
2068
2726
  params: {
2069
2727
  name,
2070
2728
  curUid,
2071
2729
  },
2072
- }
2730
+ },
2073
2731
  );
2732
+ await this.commit('nymph-set-uid');
2074
2733
  return true;
2075
2734
  }
2076
2735
 
2736
+ public async internalTransaction(name: string) {
2737
+ await this.startTransaction(name);
2738
+ }
2739
+
2077
2740
  public async startTransaction(name: string) {
2078
2741
  if (name == null || typeof name !== 'string' || name.length === 0) {
2079
2742
  throw new InvalidParametersError(
2080
- 'Transaction start attempted without a name.'
2743
+ 'Transaction start attempted without a name.',
2081
2744
  );
2082
2745
  }
2746
+ if (!this.config.explicitWrite && !this.store.linkWrite) {
2747
+ this._connect(true);
2748
+ }
2083
2749
  this.queryRun(`SAVEPOINT ${SQLite3Driver.escape(name)};`);
2084
2750
  this.store.transactionsStarted++;
2085
2751
  return this.nymph;
2086
2752
  }
2753
+
2754
+ public async needsMigration(): Promise<'json' | 'tokens' | false> {
2755
+ const table: any = this.queryGet(
2756
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @prefix LIMIT 1;",
2757
+ {
2758
+ params: {
2759
+ prefix: this.prefix + 'data_' + '%',
2760
+ },
2761
+ },
2762
+ );
2763
+ if (table?.name) {
2764
+ const result: any = this.queryGet(
2765
+ "SELECT 1 AS `exists` FROM pragma_table_info(@table) WHERE `name`='json';",
2766
+ {
2767
+ params: {
2768
+ table: table.name,
2769
+ },
2770
+ },
2771
+ );
2772
+ if (!result?.exists) {
2773
+ return 'json';
2774
+ }
2775
+ }
2776
+ const table2: any = this.queryGet(
2777
+ "SELECT `name` FROM `sqlite_master` WHERE `type`='table' AND `name` LIKE @tokenTable LIMIT 1;",
2778
+ {
2779
+ params: {
2780
+ tokenTable: this.prefix + 'tokens_' + '%',
2781
+ },
2782
+ },
2783
+ );
2784
+ if (!table2 || !table2.name) {
2785
+ return 'tokens';
2786
+ }
2787
+ return false;
2788
+ }
2789
+
2790
+ public async liveMigration(_migrationType: 'tokenTables') {
2791
+ const etypes = await this.getEtypes();
2792
+
2793
+ for (let etype of etypes) {
2794
+ this.createTokensTable(etype);
2795
+ }
2796
+ }
2087
2797
  }