@nymphjs/driver-sqlite3 1.0.0-beta.6 → 1.0.0-beta.61

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.
@@ -7,6 +7,16 @@ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
7
7
  const nymph_1 = require("@nymphjs/nymph");
8
8
  const guid_1 = require("@nymphjs/guid");
9
9
  const conf_1 = require("./conf");
10
+ class InternalStore {
11
+ constructor(link) {
12
+ this.connected = false;
13
+ this.transactionsStarted = 0;
14
+ this.link = link;
15
+ }
16
+ }
17
+ /**
18
+ * The SQLite3 Nymph database driver.
19
+ */
10
20
  class SQLite3Driver extends nymph_1.NymphDriver {
11
21
  static escape(input) {
12
22
  if (input.indexOf('\x00') !== -1) {
@@ -14,95 +24,201 @@ class SQLite3Driver extends nymph_1.NymphDriver {
14
24
  }
15
25
  return '"' + input.replace(/"/g, () => '""') + '"';
16
26
  }
17
- constructor(config) {
27
+ constructor(config, store) {
18
28
  super();
19
- this.connected = false;
20
- this.transactionsStarted = 0;
21
29
  this.config = { ...conf_1.SQLite3DriverConfigDefaults, ...config };
30
+ if (this.config.filename === ':memory:') {
31
+ this.config.explicitWrite = true;
32
+ }
22
33
  this.prefix = this.config.prefix;
23
- this.connect();
34
+ if (store) {
35
+ this.store = store;
36
+ }
37
+ else {
38
+ this.connect();
39
+ }
40
+ }
41
+ /**
42
+ * This is used internally by Nymph. Don't call it yourself.
43
+ *
44
+ * @returns A clone of this instance.
45
+ */
46
+ clone() {
47
+ return new SQLite3Driver(this.config, this.store);
24
48
  }
25
- async connect() {
26
- const { filename, fileMustExist, timeout, readonly, verbose } = this.config;
27
- if (!this.connected) {
49
+ /**
50
+ * Connect to the SQLite3 database.
51
+ *
52
+ * @returns Whether this instance is connected to a SQLite3 database.
53
+ */
54
+ connect() {
55
+ if (this.store && this.store.connected) {
56
+ return Promise.resolve(true);
57
+ }
58
+ // Connecting
59
+ this._connect(false);
60
+ return Promise.resolve(this.store.connected);
61
+ }
62
+ _connect(write) {
63
+ const { filename, fileMustExist, timeout, explicitWrite, wal, verbose } = this.config;
64
+ try {
65
+ const setOptions = (link) => {
66
+ // Set database and connection options.
67
+ if (wal) {
68
+ link.pragma('journal_mode = WAL;');
69
+ }
70
+ link.pragma('encoding = "UTF-8";');
71
+ link.pragma('foreign_keys = 1;');
72
+ link.pragma('case_sensitive_like = 1;');
73
+ for (let pragma of this.config.pragmas) {
74
+ link.pragma(pragma);
75
+ }
76
+ // Create the preg_match and regexp functions.
77
+ link.function('regexp', { deterministic: true }, ((pattern, subject) => (this.posixRegexMatch(pattern, subject) ? 1 : 0)));
78
+ };
79
+ let link;
28
80
  try {
29
- this.link = new better_sqlite3_1.default(filename, {
30
- readonly,
81
+ link = new better_sqlite3_1.default(filename, {
82
+ readonly: !explicitWrite && !write,
31
83
  fileMustExist,
32
84
  timeout,
33
85
  verbose,
34
86
  });
35
- this.connected = true;
36
- this.link.pragma('encoding = "UTF-8";');
37
- this.link.pragma('foreign_keys = 1;');
38
- this.link.pragma('case_sensitive_like = 1;');
39
- this.link.function('regexp', { deterministic: true }, (pattern, subject) => this.posixRegexMatch(pattern, subject) ? 1 : 0);
40
87
  }
41
88
  catch (e) {
42
- this.connected = false;
43
- if (filename === ':memory:') {
44
- throw new nymph_1.NotConfiguredError("It seems the config hasn't been set up correctly.");
89
+ if (e.code === 'SQLITE_CANTOPEN' &&
90
+ !explicitWrite &&
91
+ !write &&
92
+ !this.config.fileMustExist) {
93
+ // This happens when the file doesn't exist and we attempt to open it
94
+ // readonly.
95
+ // First open it in write mode.
96
+ const writeLink = new better_sqlite3_1.default(filename, {
97
+ readonly: false,
98
+ fileMustExist,
99
+ timeout,
100
+ verbose,
101
+ });
102
+ setOptions(writeLink);
103
+ writeLink.close();
104
+ // Now open in readonly.
105
+ link = new better_sqlite3_1.default(filename, {
106
+ readonly: true,
107
+ fileMustExist,
108
+ timeout,
109
+ verbose,
110
+ });
45
111
  }
46
112
  else {
47
- throw new nymph_1.UnableToConnectError('Could not connect: ' + e?.message);
113
+ throw e;
48
114
  }
49
115
  }
116
+ if (!this.store) {
117
+ if (write) {
118
+ throw new Error('Tried to open in write without opening in read first.');
119
+ }
120
+ this.store = new InternalStore(link);
121
+ }
122
+ else if (write) {
123
+ this.store.linkWrite = link;
124
+ }
125
+ else {
126
+ this.store.link = link;
127
+ }
128
+ this.store.connected = true;
129
+ setOptions(link);
130
+ }
131
+ catch (e) {
132
+ if (this.store) {
133
+ this.store.connected = false;
134
+ }
135
+ if (filename === ':memory:') {
136
+ throw new nymph_1.NotConfiguredError("It seems the config hasn't been set up correctly. Could not connect: " +
137
+ e?.message);
138
+ }
139
+ else {
140
+ throw new nymph_1.UnableToConnectError('Could not connect: ' + e?.message);
141
+ }
50
142
  }
51
- return this.connected;
52
143
  }
144
+ /**
145
+ * Disconnect from the SQLite3 database.
146
+ *
147
+ * @returns Whether this instance is connected to a SQLite3 database.
148
+ */
53
149
  async disconnect() {
54
- if (this.connected) {
55
- this.link.exec('PRAGMA optimize;');
56
- this.link.close();
57
- this.connected = false;
150
+ if (this.store.connected) {
151
+ if (this.store.linkWrite && !this.config.explicitWrite) {
152
+ this.store.linkWrite.exec('PRAGMA optimize;');
153
+ this.store.linkWrite.close();
154
+ this.store.linkWrite = undefined;
155
+ }
156
+ if (this.config.explicitWrite) {
157
+ this.store.link.exec('PRAGMA optimize;');
158
+ }
159
+ this.store.link.close();
160
+ this.store.transactionsStarted = 0;
161
+ this.store.connected = false;
58
162
  }
59
- return this.connected;
163
+ return this.store.connected;
60
164
  }
61
165
  async inTransaction() {
62
- return this.transactionsStarted > 0;
166
+ return this.store.transactionsStarted > 0;
63
167
  }
168
+ /**
169
+ * Check connection status.
170
+ *
171
+ * @returns Whether this instance is connected to a SQLite3 database.
172
+ */
64
173
  isConnected() {
65
- return this.connected;
66
- }
67
- checkReadOnlyMode() {
68
- if (this.config.readonly) {
69
- throw new nymph_1.InvalidParametersError('Attempt to write to SQLite3 DB in read only mode.');
70
- }
174
+ return this.store.connected;
71
175
  }
176
+ /**
177
+ * Create entity tables in the database.
178
+ *
179
+ * @param etype The entity type to create a table for. If this is blank, the default tables are created.
180
+ */
72
181
  createTables(etype = null) {
73
- this.checkReadOnlyMode();
74
182
  this.startTransaction('nymph-tablecreation');
75
183
  try {
76
184
  if (etype != null) {
185
+ // Create the entity table.
77
186
  this.queryRun(`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid" CHARACTER(24) PRIMARY KEY, "tags" TEXT, "cdate" REAL NOT NULL, "mdate" REAL NOT NULL);`);
78
187
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("cdate");`);
79
188
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("mdate");`);
80
189
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("tags");`);
190
+ // Create the data table.
81
191
  this.queryRun(`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY("guid", "name"));`);
82
192
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid");`);
83
193
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("name");`);
84
194
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}data_${etype}_id_value`)} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("value");`);
85
195
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}data_${etype}_id_guid__name_user`)} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid") WHERE "name" = \'user\';`);
86
196
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}data_${etype}_id_guid__name_group`)} ON ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid") WHERE "name" = \'group\';`);
197
+ // Create the comparisons table.
87
198
  this.queryRun(`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "truthy" INTEGER, "string" TEXT, "number" REAL, PRIMARY KEY("guid", "name"));`);
88
199
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}_id_guid`)} ON ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("guid");`);
89
200
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}_id_name`)} ON ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("name");`);
90
201
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}_id_name__truthy`)} ON ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("name") WHERE "truthy" = 1;`);
202
+ this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}_id_string`)} ON ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("string");`);
203
+ // Create the references table.
91
204
  this.queryRun(`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid") ON DELETE CASCADE, "name" TEXT NOT NULL, "reference" CHARACTER(24) NOT NULL, PRIMARY KEY("guid", "name", "reference"));`);
92
205
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} ("guid");`);
93
206
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} ("name");`);
94
207
  this.queryRun(`CREATE INDEX IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}references_${etype}_id_reference`)} ON ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} ("reference");`);
208
+ // Create the unique strings table.
209
+ this.queryRun(`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}uniques_${etype}`)} ("guid" CHARACTER(24) NOT NULL REFERENCES ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid") ON DELETE CASCADE, "unique" TEXT NOT NULL UNIQUE, PRIMARY KEY("guid", "unique"));`);
95
210
  }
96
211
  else {
212
+ // Create the UID table.
97
213
  this.queryRun(`CREATE TABLE IF NOT EXISTS ${SQLite3Driver.escape(`${this.prefix}uids`)} ("name" TEXT PRIMARY KEY NOT NULL, "cur_uid" INTEGER NOT NULL);`);
98
214
  }
99
- this.commit('nymph-tablecreation');
100
- return true;
101
215
  }
102
216
  catch (e) {
103
217
  this.rollback('nymph-tablecreation');
104
218
  throw e;
105
219
  }
220
+ this.commit('nymph-tablecreation');
221
+ return true;
106
222
  }
107
223
  query(runQuery, query, etypes = []) {
108
224
  try {
@@ -124,29 +240,42 @@ class SQLite3Driver extends nymph_1.NymphDriver {
124
240
  throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
125
241
  }
126
242
  }
243
+ else if (errorCode === 'SQLITE_CONSTRAINT_UNIQUE' &&
244
+ errorMsg.match(/^UNIQUE constraint failed: /)) {
245
+ throw new nymph_1.EntityUniqueConstraintError(`Unique constraint violation.`);
246
+ }
127
247
  else {
128
248
  throw new nymph_1.QueryFailedError('Query failed: ' + e?.code + ' - ' + e?.message, query);
129
249
  }
130
250
  }
131
251
  }
132
252
  queryIter(query, { etypes = [], params = {}, } = {}) {
133
- return this.query(() => this.link.prepare(query).iterate(params), `${query} -- ${JSON.stringify(params)}`, etypes);
253
+ return this.query(() => (this.store.linkWrite || this.store.link)
254
+ .prepare(query)
255
+ .iterate(params), `${query} -- ${JSON.stringify(params)}`, etypes);
134
256
  }
135
257
  queryGet(query, { etypes = [], params = {}, } = {}) {
136
- return this.query(() => this.link.prepare(query).get(params), `${query} -- ${JSON.stringify(params)}`, etypes);
258
+ return this.query(() => (this.store.linkWrite || this.store.link).prepare(query).get(params), `${query} -- ${JSON.stringify(params)}`, etypes);
137
259
  }
138
260
  queryRun(query, { etypes = [], params = {}, } = {}) {
139
- return this.query(() => this.link.prepare(query).run(params), `${query} -- ${JSON.stringify(params)}`, etypes);
261
+ return this.query(() => (this.store.linkWrite || this.store.link).prepare(query).run(params), `${query} -- ${JSON.stringify(params)}`, etypes);
140
262
  }
141
263
  async commit(name) {
142
264
  if (name == null || typeof name !== 'string' || name.length === 0) {
143
265
  throw new nymph_1.InvalidParametersError('Transaction commit attempted without a name.');
144
266
  }
145
- if (this.transactionsStarted === 0) {
267
+ if (this.store.transactionsStarted === 0) {
146
268
  return true;
147
269
  }
148
270
  this.queryRun(`RELEASE SAVEPOINT ${SQLite3Driver.escape(name)};`);
149
- this.transactionsStarted--;
271
+ this.store.transactionsStarted--;
272
+ if (this.store.transactionsStarted === 0 &&
273
+ this.store.linkWrite &&
274
+ !this.config.explicitWrite) {
275
+ this.store.linkWrite.exec('PRAGMA optimize;');
276
+ this.store.linkWrite.close();
277
+ this.store.linkWrite = undefined;
278
+ }
150
279
  return true;
151
280
  }
152
281
  async deleteEntityByID(guid, className) {
@@ -159,7 +288,6 @@ class SQLite3Driver extends nymph_1.NymphDriver {
159
288
  EntityClass = className;
160
289
  }
161
290
  const etype = EntityClass.ETYPE;
162
- this.checkReadOnlyMode();
163
291
  await this.startTransaction('nymph-delete');
164
292
  try {
165
293
  this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=@guid;`, {
@@ -186,27 +314,36 @@ class SQLite3Driver extends nymph_1.NymphDriver {
186
314
  guid,
187
315
  },
188
316
  });
189
- await this.commit('nymph-delete');
190
- if (this.nymph.config.cache) {
191
- this.cleanCache(guid);
192
- }
193
- return true;
317
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=@guid;`, {
318
+ etypes: [etype],
319
+ params: {
320
+ guid,
321
+ },
322
+ });
194
323
  }
195
324
  catch (e) {
325
+ this.nymph.config.debugError('sqlite3', `Delete entity error: "${e}"`);
196
326
  await this.rollback('nymph-delete');
197
327
  throw e;
198
328
  }
329
+ await this.commit('nymph-delete');
330
+ // Remove any cached versions of this entity.
331
+ if (this.nymph.config.cache) {
332
+ this.cleanCache(guid);
333
+ }
334
+ return true;
199
335
  }
200
336
  async deleteUID(name) {
201
337
  if (!name) {
202
338
  throw new nymph_1.InvalidParametersError('Name not given for UID');
203
339
  }
204
- this.checkReadOnlyMode();
340
+ await this.startTransaction('nymph-delete-uid');
205
341
  this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
206
342
  params: {
207
343
  name,
208
344
  },
209
345
  });
346
+ await this.commit('nymph-delete-uid');
210
347
  return true;
211
348
  }
212
349
  async exportEntities(writeLine) {
@@ -220,6 +357,7 @@ class SQLite3Driver extends nymph_1.NymphDriver {
220
357
  writeLine('# UIDs');
221
358
  writeLine('#');
222
359
  writeLine('');
360
+ // Export UIDs.
223
361
  let uids = this.queryIter(`SELECT * FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
224
362
  for (const uid of uids) {
225
363
  writeLine(`<${uid.name}>[${uid.cur_uid}]`);
@@ -229,6 +367,7 @@ class SQLite3Driver extends nymph_1.NymphDriver {
229
367
  writeLine('# Entities');
230
368
  writeLine('#');
231
369
  writeLine('');
370
+ // Get the etypes.
232
371
  const tables = this.queryIter("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;");
233
372
  const etypes = [];
234
373
  for (const table of tables) {
@@ -237,6 +376,7 @@ class SQLite3Driver extends nymph_1.NymphDriver {
237
376
  }
238
377
  }
239
378
  for (const etype of etypes) {
379
+ // Export entities.
240
380
  const dataIterator = this.queryIter(`SELECT e.*, d."name" AS "dname", d."value" AS "dvalue", c."string", c."number" FROM ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} e LEFT JOIN ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} d USING ("guid") INNER JOIN ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} c USING ("guid", "name") ORDER BY e."guid";`)[Symbol.iterator]();
241
381
  let datum = dataIterator.next();
242
382
  while (!datum.done) {
@@ -248,6 +388,8 @@ class SQLite3Driver extends nymph_1.NymphDriver {
248
388
  writeLine(`\tcdate=${JSON.stringify(cdate)}`);
249
389
  writeLine(`\tmdate=${JSON.stringify(mdate)}`);
250
390
  if (datum.value.dname != null) {
391
+ // This do will keep going and adding the data until the
392
+ // next entity is reached. datum will end on the next entity.
251
393
  do {
252
394
  const value = datum.value.dvalue === 'N'
253
395
  ? JSON.stringify(datum.value.number)
@@ -259,12 +401,23 @@ class SQLite3Driver extends nymph_1.NymphDriver {
259
401
  } while (!datum.done && datum.value.guid === guid);
260
402
  }
261
403
  else {
404
+ // Make sure that datum is incremented :)
262
405
  datum = dataIterator.next();
263
406
  }
264
407
  }
265
408
  }
266
409
  return;
267
410
  }
411
+ /**
412
+ * Generate the SQLite3 query.
413
+ * @param options The options array.
414
+ * @param formattedSelectors The formatted selector array.
415
+ * @param etype
416
+ * @param count Used to track internal params.
417
+ * @param params Used to store internal params.
418
+ * @param subquery Whether only a subquery should be returned.
419
+ * @returns The SQL query.
420
+ */
268
421
  makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = []) {
269
422
  if (typeof options.class?.alterOptions === 'function') {
270
423
  options = options.class.alterOptions(options);
@@ -274,8 +427,9 @@ class SQLite3Driver extends nymph_1.NymphDriver {
274
427
  const cTable = `c${tableSuffix}`;
275
428
  const fTable = `f${tableSuffix}`;
276
429
  const ieTable = `ie${tableSuffix}`;
430
+ const sTable = `s${tableSuffix}`;
277
431
  const sort = options.sort ?? 'cdate';
278
- const queryParts = this.iterateSelectorsForQuery(formattedSelectors, (key, value, typeIsOr, typeIsNot) => {
432
+ const queryParts = this.iterateSelectorsForQuery(formattedSelectors, ({ key, value, typeIsOr, typeIsNot }) => {
279
433
  const clauseNot = key.startsWith('!');
280
434
  let curQuery = '';
281
435
  for (const curValue of value) {
@@ -1019,18 +1173,31 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1019
1173
  return curQuery;
1020
1174
  });
1021
1175
  let sortBy;
1176
+ let sortByInner;
1177
+ let sortJoin = '';
1178
+ const order = options.reverse ? ' DESC' : '';
1022
1179
  switch (sort) {
1023
1180
  case 'mdate':
1024
- sortBy = '"mdate"';
1181
+ sortBy = `${eTable}."mdate"${order}`;
1182
+ sortByInner = `${ieTable}."mdate"${order}`;
1025
1183
  break;
1026
1184
  case 'cdate':
1185
+ sortBy = `${eTable}."cdate"${order}`;
1186
+ sortByInner = `${ieTable}."cdate"${order}`;
1187
+ break;
1027
1188
  default:
1028
- sortBy = '"cdate"';
1189
+ const name = `param${++count.i}`;
1190
+ sortJoin = `LEFT JOIN (
1191
+ SELECT "guid", "string", "number"
1192
+ FROM ${SQLite3Driver.escape(this.prefix + 'comparisons_' + etype)}
1193
+ WHERE "name"=@${name}
1194
+ ORDER BY "number"${order}, "string"${order}
1195
+ ) ${sTable} USING ("guid")`;
1196
+ sortBy = `${sTable}."number"${order}, ${sTable}."string"${order}`;
1197
+ sortByInner = sortBy;
1198
+ params[name] = sort;
1029
1199
  break;
1030
1200
  }
1031
- if (options.reverse) {
1032
- sortBy += ' DESC';
1033
- }
1034
1201
  let query;
1035
1202
  if (queryParts.length) {
1036
1203
  if (subquery) {
@@ -1063,8 +1230,9 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1063
1230
  else if (options.return === 'guid') {
1064
1231
  query = `SELECT "guid"
1065
1232
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${ieTable}
1233
+ ${sortJoin}
1066
1234
  WHERE (${whereClause})
1067
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1235
+ ORDER BY ${sortByInner}, "guid"${limit}${offset}`;
1068
1236
  }
1069
1237
  else {
1070
1238
  query = `SELECT
@@ -1079,13 +1247,15 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1079
1247
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${eTable}
1080
1248
  LEFT JOIN ${SQLite3Driver.escape(this.prefix + 'data_' + etype)} ${dTable} USING ("guid")
1081
1249
  INNER JOIN ${SQLite3Driver.escape(this.prefix + 'comparisons_' + etype)} ${cTable} USING ("guid", "name")
1250
+ ${sortJoin}
1082
1251
  INNER JOIN (
1083
1252
  SELECT "guid"
1084
1253
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${ieTable}
1254
+ ${sortJoin}
1085
1255
  WHERE (${whereClause})
1086
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1256
+ ORDER BY ${sortByInner}${limit}${offset}
1087
1257
  ) ${fTable} USING ("guid")
1088
- ORDER BY ${eTable}.${sortBy}`;
1258
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1089
1259
  }
1090
1260
  }
1091
1261
  }
@@ -1117,7 +1287,8 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1117
1287
  else if (options.return === 'guid') {
1118
1288
  query = `SELECT "guid"
1119
1289
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${ieTable}
1120
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1290
+ ${sortJoin}
1291
+ ORDER BY ${sortByInner}, "guid"${limit}${offset}`;
1121
1292
  }
1122
1293
  else {
1123
1294
  if (limit || offset) {
@@ -1133,12 +1304,14 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1133
1304
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${eTable}
1134
1305
  LEFT JOIN ${SQLite3Driver.escape(this.prefix + 'data_' + etype)} ${dTable} USING ("guid")
1135
1306
  INNER JOIN ${SQLite3Driver.escape(this.prefix + 'comparisons_' + etype)} c USING ("guid", "name")
1307
+ ${sortJoin}
1136
1308
  INNER JOIN (
1137
1309
  SELECT "guid"
1138
1310
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${ieTable}
1139
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1311
+ ${sortJoin}
1312
+ ORDER BY ${sortByInner}${limit}${offset}
1140
1313
  ) ${fTable} USING ("guid")
1141
- ORDER BY ${eTable}.${sortBy}`;
1314
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1142
1315
  }
1143
1316
  else {
1144
1317
  query = `SELECT
@@ -1153,7 +1326,8 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1153
1326
  FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)} ${eTable}
1154
1327
  LEFT JOIN ${SQLite3Driver.escape(this.prefix + 'data_' + etype)} ${dTable} USING ("guid")
1155
1328
  INNER JOIN ${SQLite3Driver.escape(this.prefix + 'comparisons_' + etype)} ${cTable} USING ("guid", "name")
1156
- ORDER BY ${eTable}.${sortBy}`;
1329
+ ${sortJoin}
1330
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1157
1331
  }
1158
1332
  }
1159
1333
  }
@@ -1175,10 +1349,7 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1175
1349
  };
1176
1350
  }
1177
1351
  async getEntities(options = {}, ...selectors) {
1178
- return this.getEntitiesSync(options, ...selectors);
1179
- }
1180
- getEntitiesSync(options = {}, ...selectors) {
1181
- const { result, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuery(options, formattedSelectors, etype), () => {
1352
+ const { result, process } = this.getEntitiesRowLike(options, selectors, ({ options, selectors, etype }) => this.performQuery(options, selectors, etype), () => {
1182
1353
  const next = result.next();
1183
1354
  return next.done ? null : next.value;
1184
1355
  }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
@@ -1210,106 +1381,158 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1210
1381
  });
1211
1382
  return result?.cur_uid ?? null;
1212
1383
  }
1213
- async import(filename) {
1214
- this.checkReadOnlyMode();
1384
+ async import(filename, transaction) {
1215
1385
  try {
1216
1386
  return this.importFromFile(filename, async (guid, tags, sdata, etype) => {
1217
- this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=@guid;`, {
1218
- etypes: [etype],
1219
- params: {
1220
- guid,
1221
- },
1222
- });
1223
- this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=@guid;`, {
1224
- etypes: [etype],
1225
- params: {
1226
- guid,
1227
- },
1228
- });
1229
- this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=@guid;`, {
1230
- etypes: [etype],
1231
- params: {
1232
- guid,
1233
- },
1234
- });
1235
- this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=@guid;`, {
1236
- etypes: [etype],
1237
- params: {
1238
- guid,
1239
- },
1240
- });
1241
- this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`, {
1242
- etypes: [etype],
1243
- params: {
1244
- guid,
1245
- tags: ',' + tags.join(',') + ',',
1246
- cdate: Number(JSON.parse(sdata.cdate)),
1247
- mdate: Number(JSON.parse(sdata.mdate)),
1248
- },
1249
- });
1250
- delete sdata.cdate;
1251
- delete sdata.mdate;
1252
- for (const name in sdata) {
1253
- const value = sdata[name];
1254
- const uvalue = JSON.parse(value);
1255
- if (value === undefined) {
1256
- continue;
1257
- }
1258
- const storageValue = typeof uvalue === 'number'
1259
- ? 'N'
1260
- : typeof uvalue === 'string'
1261
- ? 'S'
1262
- : value;
1263
- this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`, {
1387
+ try {
1388
+ await this.startTransaction(`nymph-import-entity-${guid}`);
1389
+ const cdate = Number(JSON.parse(sdata.cdate));
1390
+ delete sdata.cdate;
1391
+ const mdate = Number(JSON.parse(sdata.mdate));
1392
+ delete sdata.mdate;
1393
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=@guid;`, {
1264
1394
  etypes: [etype],
1265
1395
  params: {
1266
1396
  guid,
1267
- name,
1268
- storageValue,
1269
1397
  },
1270
1398
  });
1271
- this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`, {
1399
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=@guid;`, {
1400
+ etypes: [etype],
1401
+ params: {
1402
+ guid,
1403
+ },
1404
+ });
1405
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=@guid;`, {
1406
+ etypes: [etype],
1407
+ params: {
1408
+ guid,
1409
+ },
1410
+ });
1411
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=@guid;`, {
1412
+ etypes: [etype],
1413
+ params: {
1414
+ guid,
1415
+ },
1416
+ });
1417
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=@guid;`, {
1272
1418
  etypes: [etype],
1273
1419
  params: {
1274
1420
  guid,
1275
- name,
1276
- truthy: uvalue ? 1 : 0,
1277
- string: `${uvalue}`,
1278
- number: Number(uvalue),
1279
1421
  },
1280
1422
  });
1281
- const references = this.findReferences(value);
1282
- for (const reference of references) {
1283
- this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`, {
1423
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @mdate);`, {
1424
+ etypes: [etype],
1425
+ params: {
1426
+ guid,
1427
+ tags: ',' + tags.join(',') + ',',
1428
+ cdate,
1429
+ mdate,
1430
+ },
1431
+ });
1432
+ for (const name in sdata) {
1433
+ const value = sdata[name];
1434
+ const uvalue = JSON.parse(value);
1435
+ if (value === undefined) {
1436
+ continue;
1437
+ }
1438
+ const storageValue = typeof uvalue === 'number'
1439
+ ? 'N'
1440
+ : typeof uvalue === 'string'
1441
+ ? 'S'
1442
+ : value;
1443
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (@guid, @name, @storageValue);`, {
1284
1444
  etypes: [etype],
1285
1445
  params: {
1286
1446
  guid,
1287
1447
  name,
1288
- reference,
1448
+ storageValue,
1289
1449
  },
1290
1450
  });
1451
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}comparisons_${etype}`)} ("guid", "name", "truthy", "string", "number") VALUES (@guid, @name, @truthy, @string, @number);`, {
1452
+ etypes: [etype],
1453
+ params: {
1454
+ guid,
1455
+ name,
1456
+ truthy: uvalue ? 1 : 0,
1457
+ string: `${uvalue}`,
1458
+ number: Number(uvalue),
1459
+ },
1460
+ });
1461
+ const references = this.findReferences(value);
1462
+ for (const reference of references) {
1463
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}references_${etype}`)} ("guid", "name", "reference") VALUES (@guid, @name, @reference);`, {
1464
+ etypes: [etype],
1465
+ params: {
1466
+ guid,
1467
+ name,
1468
+ reference,
1469
+ },
1470
+ });
1471
+ }
1291
1472
  }
1473
+ const uniques = await this.nymph
1474
+ .getEntityClassByEtype(etype)
1475
+ .getUniques({ guid, cdate, mdate, tags, data: {}, sdata });
1476
+ for (const unique of uniques) {
1477
+ try {
1478
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (@guid, @unique);`, {
1479
+ etypes: [etype],
1480
+ params: {
1481
+ guid,
1482
+ unique,
1483
+ },
1484
+ });
1485
+ }
1486
+ catch (e) {
1487
+ if (e instanceof nymph_1.EntityUniqueConstraintError) {
1488
+ this.nymph.config.debugError('sqlite3', `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
1489
+ }
1490
+ throw e;
1491
+ }
1492
+ }
1493
+ await this.commit(`nymph-import-entity-${guid}`);
1494
+ }
1495
+ catch (e) {
1496
+ this.nymph.config.debugError('sqlite3', `Import entity error: "${e}"`);
1497
+ await this.rollback(`nymph-import-entity-${guid}`);
1498
+ throw e;
1292
1499
  }
1293
1500
  }, async (name, curUid) => {
1294
- this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1295
- params: {
1296
- name,
1297
- },
1298
- });
1299
- this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
1300
- params: {
1301
- name,
1302
- curUid,
1303
- },
1304
- });
1501
+ try {
1502
+ await this.startTransaction(`nymph-import-uid-${name}`);
1503
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1504
+ params: {
1505
+ name,
1506
+ },
1507
+ });
1508
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
1509
+ params: {
1510
+ name,
1511
+ curUid,
1512
+ },
1513
+ });
1514
+ await this.commit(`nymph-import-uid-${name}`);
1515
+ }
1516
+ catch (e) {
1517
+ this.nymph.config.debugError('sqlite3', `Import UID error: "${e}"`);
1518
+ await this.rollback(`nymph-import-uid-${name}`);
1519
+ throw e;
1520
+ }
1305
1521
  }, async () => {
1306
- await this.startTransaction('nymph-import');
1522
+ if (transaction) {
1523
+ await this.startTransaction('nymph-import');
1524
+ }
1307
1525
  }, async () => {
1308
- await this.commit('nymph-import');
1526
+ if (transaction) {
1527
+ await this.commit('nymph-import');
1528
+ }
1309
1529
  });
1310
1530
  }
1311
1531
  catch (e) {
1312
- await this.rollback('nymph-import');
1532
+ this.nymph.config.debugError('sqlite3', `Import error: "${e}"`);
1533
+ if (transaction) {
1534
+ await this.rollback('nymph-import');
1535
+ }
1313
1536
  throw e;
1314
1537
  }
1315
1538
  }
@@ -1317,14 +1540,15 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1317
1540
  if (name == null) {
1318
1541
  throw new nymph_1.InvalidParametersError('Name not given for UID.');
1319
1542
  }
1320
- this.checkReadOnlyMode();
1321
1543
  await this.startTransaction('nymph-newuid');
1544
+ let curUid = undefined;
1322
1545
  try {
1323
- let curUid = this.queryGet(`SELECT "cur_uid" FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1324
- params: {
1325
- name,
1326
- },
1327
- })?.cur_uid ?? null;
1546
+ curUid =
1547
+ this.queryGet(`SELECT "cur_uid" FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1548
+ params: {
1549
+ name,
1550
+ },
1551
+ })?.cur_uid ?? null;
1328
1552
  if (curUid == null) {
1329
1553
  curUid = 1;
1330
1554
  this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
@@ -1343,41 +1567,49 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1343
1567
  },
1344
1568
  });
1345
1569
  }
1346
- await this.commit('nymph-newuid');
1347
- return curUid;
1348
1570
  }
1349
1571
  catch (e) {
1572
+ this.nymph.config.debugError('sqlite3', `New UID error: "${e}"`);
1350
1573
  await this.rollback('nymph-newuid');
1351
1574
  throw e;
1352
1575
  }
1576
+ await this.commit('nymph-newuid');
1577
+ return curUid;
1353
1578
  }
1354
1579
  async renameUID(oldName, newName) {
1355
1580
  if (oldName == null || newName == null) {
1356
1581
  throw new nymph_1.InvalidParametersError('Name not given for UID.');
1357
1582
  }
1358
- this.checkReadOnlyMode();
1583
+ await this.startTransaction('nymph-rename-uid');
1359
1584
  this.queryRun(`UPDATE ${SQLite3Driver.escape(`${this.prefix}uids`)} SET "name"=@newName WHERE "name"=@oldName;`, {
1360
1585
  params: {
1361
1586
  newName,
1362
1587
  oldName,
1363
1588
  },
1364
1589
  });
1590
+ await this.commit('nymph-rename-uid');
1365
1591
  return true;
1366
1592
  }
1367
1593
  async rollback(name) {
1368
1594
  if (name == null || typeof name !== 'string' || name.length === 0) {
1369
1595
  throw new nymph_1.InvalidParametersError('Transaction rollback attempted without a name.');
1370
1596
  }
1371
- if (this.transactionsStarted === 0) {
1597
+ if (this.store.transactionsStarted === 0) {
1372
1598
  return true;
1373
1599
  }
1374
1600
  this.queryRun(`ROLLBACK TO SAVEPOINT ${SQLite3Driver.escape(name)};`);
1375
- this.transactionsStarted--;
1601
+ this.store.transactionsStarted--;
1602
+ if (this.store.transactionsStarted === 0 &&
1603
+ this.store.linkWrite &&
1604
+ !this.config.explicitWrite) {
1605
+ this.store.linkWrite.exec('PRAGMA optimize;');
1606
+ this.store.linkWrite.close();
1607
+ this.store.linkWrite = undefined;
1608
+ }
1376
1609
  return true;
1377
1610
  }
1378
1611
  async saveEntity(entity) {
1379
- this.checkReadOnlyMode();
1380
- const insertData = (guid, data, sdata, etype) => {
1612
+ const insertData = (guid, data, sdata, uniques, etype) => {
1381
1613
  const runInsertQuery = (name, value, svalue) => {
1382
1614
  if (value === undefined) {
1383
1615
  return;
@@ -1417,6 +1649,23 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1417
1649
  });
1418
1650
  }
1419
1651
  };
1652
+ for (const unique of uniques) {
1653
+ try {
1654
+ this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (@guid, @unique);`, {
1655
+ etypes: [etype],
1656
+ params: {
1657
+ guid,
1658
+ unique,
1659
+ },
1660
+ });
1661
+ }
1662
+ catch (e) {
1663
+ if (e instanceof nymph_1.EntityUniqueConstraintError) {
1664
+ this.nymph.config.debugError('sqlite3', `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
1665
+ }
1666
+ throw e;
1667
+ }
1668
+ }
1420
1669
  for (const name in data) {
1421
1670
  runInsertQuery(name, data[name], JSON.stringify(data[name]));
1422
1671
  }
@@ -1424,8 +1673,13 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1424
1673
  runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1425
1674
  }
1426
1675
  };
1676
+ let inTransaction = false;
1427
1677
  try {
1428
- return this.saveEntityRowLike(entity, async (_entity, guid, tags, data, sdata, cdate, etype) => {
1678
+ return this.saveEntityRowLike(entity, async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
1679
+ if (Object.keys(data).length === 0 &&
1680
+ Object.keys(sdata).length === 0) {
1681
+ return false;
1682
+ }
1429
1683
  this.queryRun(`INSERT INTO ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (@guid, @tags, @cdate, @cdate);`, {
1430
1684
  etypes: [etype],
1431
1685
  params: {
@@ -1434,9 +1688,13 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1434
1688
  cdate,
1435
1689
  },
1436
1690
  });
1437
- insertData(guid, data, sdata, etype);
1691
+ insertData(guid, data, sdata, uniques, etype);
1438
1692
  return true;
1439
- }, async (entity, guid, tags, data, sdata, mdate, etype) => {
1693
+ }, async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
1694
+ if (Object.keys(data).length === 0 &&
1695
+ Object.keys(sdata).length === 0) {
1696
+ return false;
1697
+ }
1440
1698
  const info = this.queryRun(`UPDATE ${SQLite3Driver.escape(`${this.prefix}entities_${etype}`)} SET "tags"=@tags, "mdate"=@mdate WHERE "guid"=@guid AND "mdate" <= @emdate;`, {
1441
1699
  etypes: [etype],
1442
1700
  params: {
@@ -1466,24 +1724,37 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1466
1724
  guid,
1467
1725
  },
1468
1726
  });
1469
- insertData(guid, data, sdata, etype);
1727
+ this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=@guid;`, {
1728
+ etypes: [etype],
1729
+ params: {
1730
+ guid,
1731
+ },
1732
+ });
1733
+ insertData(guid, data, sdata, uniques, etype);
1470
1734
  success = true;
1471
1735
  }
1472
1736
  return success;
1473
1737
  }, async () => {
1474
1738
  await this.startTransaction('nymph-save');
1739
+ inTransaction = true;
1475
1740
  }, async (success) => {
1476
- if (success) {
1477
- await this.commit('nymph-save');
1478
- }
1479
- else {
1480
- await this.rollback('nymph-save');
1741
+ if (inTransaction) {
1742
+ inTransaction = false;
1743
+ if (success) {
1744
+ await this.commit('nymph-save');
1745
+ }
1746
+ else {
1747
+ await this.rollback('nymph-save');
1748
+ }
1481
1749
  }
1482
1750
  return success;
1483
1751
  });
1484
1752
  }
1485
1753
  catch (e) {
1486
- await this.rollback('nymph-save');
1754
+ this.nymph.config.debugError('sqlite3', `Save entity error: "${e}"`);
1755
+ if (inTransaction) {
1756
+ await this.rollback('nymph-save');
1757
+ }
1487
1758
  throw e;
1488
1759
  }
1489
1760
  }
@@ -1491,7 +1762,7 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1491
1762
  if (name == null) {
1492
1763
  throw new nymph_1.InvalidParametersError('Name not given for UID.');
1493
1764
  }
1494
- this.checkReadOnlyMode();
1765
+ await this.startTransaction('nymph-set-uid');
1495
1766
  this.queryRun(`DELETE FROM ${SQLite3Driver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1496
1767
  params: {
1497
1768
  name,
@@ -1503,14 +1774,18 @@ class SQLite3Driver extends nymph_1.NymphDriver {
1503
1774
  curUid,
1504
1775
  },
1505
1776
  });
1777
+ await this.commit('nymph-set-uid');
1506
1778
  return true;
1507
1779
  }
1508
1780
  async startTransaction(name) {
1509
1781
  if (name == null || typeof name !== 'string' || name.length === 0) {
1510
1782
  throw new nymph_1.InvalidParametersError('Transaction start attempted without a name.');
1511
1783
  }
1784
+ if (!this.config.explicitWrite && !this.store.linkWrite) {
1785
+ this._connect(true);
1786
+ }
1512
1787
  this.queryRun(`SAVEPOINT ${SQLite3Driver.escape(name)};`);
1513
- this.transactionsStarted++;
1788
+ this.store.transactionsStarted++;
1514
1789
  return this.nymph;
1515
1790
  }
1516
1791
  }