@nymphjs/driver-postgresql 1.0.0-beta.8 → 1.0.0-beta.80

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.
@@ -3,23 +3,58 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const child_process_1 = __importDefault(require("child_process"));
7
6
  const pg_1 = require("pg");
8
7
  const pg_format_1 = __importDefault(require("pg-format"));
9
8
  const nymph_1 = require("@nymphjs/nymph");
10
9
  const guid_1 = require("@nymphjs/guid");
11
10
  const conf_1 = require("./conf");
11
+ /**
12
+ * The PostgreSQL Nymph database driver.
13
+ */
12
14
  class PostgreSQLDriver extends nymph_1.NymphDriver {
15
+ config;
16
+ postgresqlConfig;
17
+ prefix;
18
+ connected = false;
19
+ // @ts-ignore: this is assigned in connect(), which is called by the constructor.
20
+ link;
21
+ transaction = null;
13
22
  static escape(input) {
14
23
  return pg_format_1.default.ident(input);
15
24
  }
16
25
  static escapeValue(input) {
17
26
  return pg_format_1.default.literal(input);
18
27
  }
28
+ static escapeNullSequences(input) {
29
+ // Postgres doesn't support null bytes in `text`, and it converts strings
30
+ // in JSON to `text`, so we need to escape the escape sequences for null
31
+ // bytes.
32
+ return (input
33
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
34
+ // n so that if there's already an escape, it turns into \n
35
+ // - so that it won't match a \uFFFD that got turned into \uFFFD\uFFFD
36
+ .replace(/\\u0000/g, () => 'nu\uFFFD-')
37
+ .replace(/\\x00/g, () => 'nx\uFFFD-'));
38
+ }
39
+ static unescapeNullSequences(input) {
40
+ return input
41
+ .replace(/nu\uFFFD-/g, () => '\\u0000')
42
+ .replace(/nx\uFFFD-/g, () => '\\x00')
43
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
44
+ }
45
+ static escapeNulls(input) {
46
+ // Postgres doesn't support null bytes in `text`.
47
+ return input
48
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
49
+ .replace(/\x00/g, () => '-\uFFFD-');
50
+ }
51
+ static unescapeNulls(input) {
52
+ return input
53
+ .replace(/-\uFFFD-/g, () => '\x00')
54
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
55
+ }
19
56
  constructor(config, link, transaction) {
20
57
  super();
21
- this.connected = false;
22
- this.transaction = null;
23
58
  this.config = { ...conf_1.PostgreSQLDriverConfigDefaults, ...config };
24
59
  const { host, user, password, database, port, customPoolConfig } = this.config;
25
60
  this.postgresqlConfig = customPoolConfig ?? {
@@ -41,19 +76,40 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
41
76
  this.connect();
42
77
  }
43
78
  }
79
+ /**
80
+ * This is used internally by Nymph. Don't call it yourself.
81
+ *
82
+ * @returns A clone of this instance.
83
+ */
44
84
  clone() {
45
85
  return new PostgreSQLDriver(this.config, this.link, this.transaction ?? undefined);
46
86
  }
47
- getConnection() {
48
- if (this.transaction != null && this.transaction.connection != null) {
87
+ getConnection(outsideTransaction = false) {
88
+ if (this.transaction != null &&
89
+ this.transaction.connection != null &&
90
+ !outsideTransaction) {
49
91
  return Promise.resolve(this.transaction.connection);
50
92
  }
51
- return new Promise((resolve, reject) => this.link.connect((err, client, done) => err ? reject(err) : resolve({ client, done })));
93
+ return new Promise((resolve, reject) => this.link.connect((err, client, done) => err
94
+ ? reject(err)
95
+ : client
96
+ ? resolve({ client, done })
97
+ : reject('No client returned from connect.')));
52
98
  }
99
+ /**
100
+ * Connect to the PostgreSQL database.
101
+ *
102
+ * @returns Whether this instance is connected to a PostgreSQL database.
103
+ */
53
104
  async connect() {
105
+ // If we think we're connected, try pinging the server.
54
106
  try {
55
107
  if (this.connected) {
56
- const connection = await new Promise((resolve, reject) => this.link.connect((err, client, done) => err ? reject(err) : resolve({ client, done })));
108
+ const connection = await new Promise((resolve, reject) => this.link.connect((err, client, done) => err
109
+ ? reject(err)
110
+ : client
111
+ ? resolve({ client, done })
112
+ : reject('No client returned from connect.')));
57
113
  await new Promise((resolve, reject) => connection.client.query('SELECT 1;', [], (err, res) => {
58
114
  if (err) {
59
115
  reject(err);
@@ -66,6 +122,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
66
122
  catch (e) {
67
123
  this.connected = false;
68
124
  }
125
+ // Connecting, selecting database
69
126
  if (!this.connected) {
70
127
  try {
71
128
  this.link = new pg_1.Pool(this.postgresqlConfig);
@@ -85,6 +142,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
85
142
  }
86
143
  return this.connected;
87
144
  }
145
+ /**
146
+ * Disconnect from the PostgreSQL database.
147
+ *
148
+ * @returns Whether this instance is connected to a PostgreSQL database.
149
+ */
88
150
  async disconnect() {
89
151
  if (this.connected) {
90
152
  await new Promise((resolve) => this.link.end(() => resolve(0)));
@@ -95,85 +157,116 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
95
157
  async inTransaction() {
96
158
  return !!this.transaction;
97
159
  }
160
+ /**
161
+ * Check connection status.
162
+ *
163
+ * @returns Whether this instance is connected to a PostgreSQL database.
164
+ */
98
165
  isConnected() {
99
166
  return this.connected;
100
167
  }
101
- createTables(etype = null) {
168
+ /**
169
+ * Create entity tables in the database.
170
+ *
171
+ * @param etype The entity type to create a table for. If this is blank, the default tables are created.
172
+ * @returns True on success, false on failure.
173
+ */
174
+ async createTables(etype = null) {
175
+ const connection = await this.getConnection(true);
102
176
  if (etype != null) {
103
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} (
177
+ // Create the entity table.
178
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} (
104
179
  "guid" BYTEA NOT NULL,
105
180
  "tags" TEXT[],
106
181
  "cdate" DOUBLE PRECISION NOT NULL,
107
182
  "mdate" DOUBLE PRECISION NOT NULL,
108
183
  PRIMARY KEY ("guid")
109
- ) WITH ( OIDS=FALSE );`);
110
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
111
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)};`);
112
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("cdate");`);
113
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)};`);
114
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("mdate");`);
115
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)};`);
116
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("tags");`);
117
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} (
118
- "guid" BYTEA NOT NULL,
119
- "name" TEXT NOT NULL,
120
- "value" TEXT NOT NULL,
121
- PRIMARY KEY ("guid", "name"),
122
- FOREIGN KEY ("guid")
123
- REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
124
- ) WITH ( OIDS=FALSE );`);
125
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
126
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)};`);
127
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid");`);
128
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)};`);
129
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name");`);
130
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)};`);
131
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name" = 'user'::text;`);
132
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)};`);
133
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name" = 'group'::text;`);
134
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} (
184
+ ) WITH ( OIDS=FALSE );`, { connection });
185
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
186
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)};`, { connection });
187
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("cdate");`, { connection });
188
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)};`, { connection });
189
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("mdate");`, { connection });
190
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)};`, { connection });
191
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("tags");`, { connection });
192
+ // Create the data table.
193
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} (
135
194
  "guid" BYTEA NOT NULL,
136
195
  "name" TEXT NOT NULL,
137
- "truthy" BOOLEAN,
196
+ "value" CHARACTER(1) NOT NULL,
197
+ "json" JSONB,
138
198
  "string" TEXT,
139
199
  "number" DOUBLE PRECISION,
200
+ "truthy" BOOLEAN,
140
201
  PRIMARY KEY ("guid", "name"),
141
202
  FOREIGN KEY ("guid")
142
203
  REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
143
- ) WITH ( OIDS=FALSE );`);
144
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
145
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid`)};`);
146
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid");`);
147
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_name`)};`);
148
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("name");`);
149
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_truthy`)};`);
150
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid", "name") WHERE "truthy" = TRUE;`);
151
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_falsy`)};`);
152
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_falsy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid", "name") WHERE "truthy" <> TRUE;`);
153
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} (
204
+ ) WITH ( OIDS=FALSE );`, { connection });
205
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
206
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)};`, { connection });
207
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid");`, { connection });
208
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name`)};`, { connection });
209
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid", "name");`, { connection });
210
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)};`, { connection });
211
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name" = 'user'::text;`, { connection });
212
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)};`, { connection });
213
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name" = 'group'::text;`, { connection });
214
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)};`, { connection });
215
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name");`, { connection });
216
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_string`)};`, { connection });
217
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_string`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name", LEFT("string", 512));`, { connection });
218
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_number`)};`, { connection });
219
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_number`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name", "number");`, { connection });
220
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_truthy`)};`, { connection });
221
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name") WHERE "truthy" = TRUE;`, { connection });
222
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_falsy`)};`, { connection });
223
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_falsy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name") WHERE "truthy" <> TRUE;`, { connection });
224
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_string`)};`, { connection });
225
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_string`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("string" gin_trgm_ops);`, { connection });
226
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_json`)};`, { connection });
227
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_json`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("json");`, { connection });
228
+ // Create the references table.
229
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} (
154
230
  "guid" BYTEA NOT NULL,
155
231
  "name" TEXT NOT NULL,
156
232
  "reference" BYTEA NOT NULL,
157
233
  PRIMARY KEY ("guid", "name", "reference"),
158
234
  FOREIGN KEY ("guid")
159
235
  REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
160
- ) WITH ( OIDS=FALSE );`);
161
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
162
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)};`);
163
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid");`);
164
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)};`);
165
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name");`);
166
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)};`);
167
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference");`);
236
+ ) WITH ( OIDS=FALSE );`, { connection });
237
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
238
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)};`, { connection });
239
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid");`, { connection });
240
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)};`, { connection });
241
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name");`, { connection });
242
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name_reference`)};`, { connection });
243
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name", "reference");`, { connection });
244
+ // Create the unique strings table.
245
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} (
246
+ "guid" BYTEA NOT NULL,
247
+ "unique" TEXT NOT NULL UNIQUE,
248
+ PRIMARY KEY ("guid", "unique"),
249
+ FOREIGN KEY ("guid")
250
+ REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
251
+ ) WITH ( OIDS=FALSE );`, { connection });
168
252
  }
169
253
  else {
170
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uids`)} (
254
+ // Add trigram extensions.
255
+ try {
256
+ await this.queryRun(`CREATE EXTENSION pg_trgm;`, { connection });
257
+ }
258
+ catch (e) {
259
+ // Ignore errors.
260
+ }
261
+ // Create the UID table.
262
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uids`)} (
171
263
  "name" TEXT NOT NULL,
172
264
  "cur_uid" BIGINT NOT NULL,
173
265
  PRIMARY KEY ("name")
174
- ) WITH ( OIDS = FALSE );`);
175
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
266
+ ) WITH ( OIDS = FALSE );`, { connection });
267
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
176
268
  }
269
+ connection.done();
177
270
  return true;
178
271
  }
179
272
  translateQuery(origQuery, origParams) {
@@ -191,13 +284,15 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
191
284
  }
192
285
  async query(runQuery, query, etypes = []) {
193
286
  try {
287
+ this.nymph.config.debugInfo('postgresql:query', query);
194
288
  return await runQuery();
195
289
  }
196
290
  catch (e) {
197
291
  const errorCode = e?.code;
198
- if (errorCode === '42P01' && this.createTables()) {
292
+ if (errorCode === '42P01' && (await this.createTables())) {
293
+ // If the tables don't exist yet, create them.
199
294
  for (let etype of etypes) {
200
- this.createTables(etype);
295
+ await this.createTables(etype);
201
296
  }
202
297
  try {
203
298
  return await runQuery();
@@ -206,30 +301,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
206
301
  throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
207
302
  }
208
303
  }
209
- else {
210
- throw e;
211
- }
212
- }
213
- }
214
- querySync(runQuery, query, etypes = []) {
215
- try {
216
- return runQuery();
217
- }
218
- catch (e) {
219
- const errorCode = e?.code;
220
- if (errorCode === '42P01' && this.createTables()) {
221
- for (let etype of etypes) {
222
- this.createTables(etype);
223
- }
224
- try {
225
- return runQuery();
226
- }
227
- catch (e2) {
228
- throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
229
- }
304
+ else if (errorCode === '23505') {
305
+ throw new nymph_1.EntityUniqueConstraintError(`Unique constraint violation.`);
230
306
  }
231
307
  else {
232
- throw e;
308
+ throw new nymph_1.QueryFailedError('Query failed: ' + e?.code + ' - ' + e?.message, query);
233
309
  }
234
310
  }
235
311
  }
@@ -249,35 +325,6 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
249
325
  return results.rows;
250
326
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
251
327
  }
252
- queryIterSync(query, { etypes = [], params = {}, } = {}) {
253
- const { query: newQuery, params: newParams } = this.translateQuery(query, params);
254
- return this.querySync(() => {
255
- const output = child_process_1.default.spawnSync(process.argv0, [__dirname + '/runPostgresqlSync.js'], {
256
- input: JSON.stringify({
257
- postgresqlConfig: this.postgresqlConfig,
258
- query: newQuery,
259
- params: newParams,
260
- }),
261
- timeout: 30000,
262
- maxBuffer: 100 * 1024 * 1024,
263
- encoding: 'utf8',
264
- windowsHide: true,
265
- });
266
- try {
267
- return JSON.parse(output.stdout).rows;
268
- }
269
- catch (e) {
270
- }
271
- if (output.status === 0) {
272
- throw new Error('Unknown parse error.');
273
- }
274
- const err = JSON.parse(output.stderr);
275
- const e = new Error(err.name);
276
- for (const name in err) {
277
- e[name] = err[name];
278
- }
279
- }, `${query} -- ${JSON.stringify(params)}`, etypes);
280
- }
281
328
  queryGet(query, { etypes = [], params = {}, } = {}) {
282
329
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
283
330
  return this.query(async () => {
@@ -294,12 +341,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
294
341
  return results.rows[0];
295
342
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
296
343
  }
297
- queryRun(query, { etypes = [], params = {}, } = {}) {
344
+ queryRun(query, { etypes = [], params = {}, connection, } = {}) {
298
345
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
299
346
  return this.query(async () => {
300
347
  const results = await new Promise((resolve, reject) => {
301
348
  try {
302
- (this.transaction?.connection?.client ?? this.link)
349
+ ((connection ?? this.transaction?.connection)?.client ??
350
+ this.link)
303
351
  .query(newQuery, newParams)
304
352
  .then((results) => resolve(results), (error) => reject(error));
305
353
  }
@@ -310,36 +358,6 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
310
358
  return { rowCount: results.rowCount ?? 0 };
311
359
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
312
360
  }
313
- queryRunSync(query, { etypes = [], params = {}, } = {}) {
314
- const { query: newQuery, params: newParams } = this.translateQuery(query, params);
315
- return this.querySync(() => {
316
- const output = child_process_1.default.spawnSync(process.argv0, [__dirname + '/runPostgresqlSync.js'], {
317
- input: JSON.stringify({
318
- postgresqlConfig: this.postgresqlConfig,
319
- query: newQuery,
320
- params: newParams,
321
- }),
322
- timeout: 30000,
323
- maxBuffer: 100 * 1024 * 1024,
324
- encoding: 'utf8',
325
- windowsHide: true,
326
- });
327
- try {
328
- const results = JSON.parse(output.stdout);
329
- return { rowCount: results.rowCount ?? 0 };
330
- }
331
- catch (e) {
332
- }
333
- if (output.status === 0) {
334
- throw new Error('Unknown parse error.');
335
- }
336
- const err = JSON.parse(output.stderr);
337
- const e = new Error(err.name);
338
- for (const name in err) {
339
- e[name] = err[name];
340
- }
341
- }, `${query} -- ${JSON.stringify(params)}`, etypes);
342
- }
343
361
  async commit(name) {
344
362
  if (name == null || typeof name !== 'string' || name.length === 0) {
345
363
  throw new nymph_1.InvalidParametersError('Transaction commit attempted without a name.');
@@ -382,28 +400,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
382
400
  guid,
383
401
  },
384
402
  });
385
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
403
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
386
404
  etypes: [etype],
387
405
  params: {
388
406
  guid,
389
407
  },
390
408
  });
391
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
409
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
392
410
  etypes: [etype],
393
411
  params: {
394
412
  guid,
395
413
  },
396
414
  });
397
- await this.commit('nymph-delete');
398
- if (this.nymph.config.cache) {
399
- this.cleanCache(guid);
400
- }
401
- return true;
402
415
  }
403
416
  catch (e) {
417
+ this.nymph.config.debugError('postgresql', `Delete entity error: "${e}"`);
404
418
  await this.rollback('nymph-delete');
405
419
  throw e;
406
420
  }
421
+ await this.commit('nymph-delete');
422
+ // Remove any cached versions of this entity.
423
+ if (this.nymph.config.cache) {
424
+ this.cleanCache(guid);
425
+ }
426
+ return true;
407
427
  }
408
428
  async deleteUID(name) {
409
429
  if (!name) {
@@ -416,79 +436,124 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
416
436
  });
417
437
  return true;
418
438
  }
419
- async exportEntities(writeLine) {
420
- writeLine('#nex2');
421
- writeLine('# Nymph Entity Exchange v2');
422
- writeLine('# http://nymph.io');
423
- writeLine('#');
424
- writeLine('# Generation Time: ' + new Date().toLocaleString());
425
- writeLine('');
426
- writeLine('#');
427
- writeLine('# UIDs');
428
- writeLine('#');
429
- writeLine('');
439
+ async *exportDataIterator() {
440
+ if (yield {
441
+ type: 'comment',
442
+ content: `#nex2
443
+ # Nymph Entity Exchange v2
444
+ # http://nymph.io
445
+ #
446
+ # Generation Time: ${new Date().toLocaleString()}
447
+ `,
448
+ }) {
449
+ return;
450
+ }
451
+ if (yield {
452
+ type: 'comment',
453
+ content: `
454
+
455
+ #
456
+ # UIDs
457
+ #
458
+
459
+ `,
460
+ }) {
461
+ return;
462
+ }
463
+ // Export UIDs.
430
464
  let uids = await this.queryIter(`SELECT * FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
431
465
  for (const uid of uids) {
432
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
466
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
467
+ return;
468
+ }
433
469
  }
434
- writeLine('');
435
- writeLine('#');
436
- writeLine('# Entities');
437
- writeLine('#');
438
- writeLine('');
439
- const tables = await this.queryIter('SELECT relname FROM pg_stat_user_tables ORDER BY relname;');
470
+ if (yield {
471
+ type: 'comment',
472
+ content: `
473
+
474
+ #
475
+ # Entities
476
+ #
477
+
478
+ `,
479
+ }) {
480
+ return;
481
+ }
482
+ // Get the etypes.
483
+ const tables = await this.queryIter('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix;', {
484
+ params: {
485
+ db: this.config.database,
486
+ prefix: this.prefix + 'entities_' + '%',
487
+ },
488
+ });
440
489
  const etypes = [];
441
- for (const tableRow of tables) {
442
- const table = tableRow.relname;
443
- if (table.startsWith(this.prefix + 'entities_')) {
444
- etypes.push(table.substr((this.prefix + 'entities_').length));
445
- }
490
+ for (const table of tables) {
491
+ etypes.push(table.table_name.substr((this.prefix + 'entities_').length));
446
492
  }
447
493
  for (const etype of etypes) {
448
- const dataIterator = (await this.queryIter(`SELECT encode(e."guid", 'hex') AS "guid", e."tags", e."cdate", e."mdate", d."name" AS "dname", d."value" AS "dvalue", c."string", c."number"
494
+ // Export entities.
495
+ const dataIterator = (await this.queryIter(`SELECT encode(e."guid", 'hex') AS "guid", e."tags", e."cdate", e."mdate", d."name", d."value", d."json", d."string", d."number"
449
496
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} e
450
497
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} d ON e."guid"=d."guid"
451
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} c ON d."guid"=c."guid" AND d."name"=c."name"
452
498
  ORDER BY e."guid";`))[Symbol.iterator]();
453
499
  let datum = dataIterator.next();
454
500
  while (!datum.done) {
455
501
  const guid = datum.value.guid;
456
- const tags = datum.value.tags.join(',');
502
+ const tags = datum.value.tags.filter((tag) => tag).join(',');
457
503
  const cdate = datum.value.cdate;
458
504
  const mdate = datum.value.mdate;
459
- writeLine(`{${guid}}<${etype}>[${tags}]`);
460
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
461
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
462
- if (datum.value.dname != null) {
505
+ let currentEntityExport = [];
506
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
507
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
508
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
509
+ if (datum.value.name != null) {
510
+ // This do will keep going and adding the data until the
511
+ // next entity is reached. $row will end on the next entity.
463
512
  do {
464
- const value = datum.value.dvalue === 'N'
513
+ const value = datum.value.value === 'N'
465
514
  ? JSON.stringify(Number(datum.value.number))
466
- : datum.value.dvalue === 'S'
467
- ? JSON.stringify(datum.value.string)
468
- : datum.value.dvalue;
469
- writeLine(`\t${datum.value.dname}=${value}`);
515
+ : datum.value.value === 'S'
516
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(datum.value.string))
517
+ : datum.value.value === 'J'
518
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(datum.value.json))
519
+ : datum.value.value;
520
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
470
521
  datum = dataIterator.next();
471
522
  } while (!datum.done && datum.value.guid === guid);
472
523
  }
473
524
  else {
525
+ // Make sure that datum is incremented :)
474
526
  datum = dataIterator.next();
475
527
  }
528
+ currentEntityExport.push('');
529
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
530
+ return;
531
+ }
476
532
  }
477
533
  }
478
- return;
479
534
  }
480
- makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = []) {
535
+ /**
536
+ * Generate the PostgreSQL query.
537
+ * @param options The options array.
538
+ * @param formattedSelectors The formatted selector array.
539
+ * @param etype
540
+ * @param count Used to track internal params.
541
+ * @param params Used to store internal params.
542
+ * @param subquery Whether only a subquery should be returned.
543
+ * @returns The SQL query.
544
+ */
545
+ makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = [], guidSelector = undefined) {
481
546
  if (typeof options.class?.alterOptions === 'function') {
482
547
  options = options.class.alterOptions(options);
483
548
  }
484
549
  const eTable = `e${tableSuffix}`;
485
550
  const dTable = `d${tableSuffix}`;
486
- const cTable = `c${tableSuffix}`;
487
551
  const fTable = `f${tableSuffix}`;
488
552
  const ieTable = `ie${tableSuffix}`;
489
553
  const countTable = `count${tableSuffix}`;
554
+ const sTable = `s${tableSuffix}`;
490
555
  const sort = options.sort ?? 'cdate';
491
- const queryParts = this.iterateSelectorsForQuery(formattedSelectors, (key, value, typeIsOr, typeIsNot) => {
556
+ const queryParts = this.iterateSelectorsForQuery(formattedSelectors, ({ key, value, typeIsOr, typeIsNot }) => {
492
557
  const clauseNot = key.startsWith('!');
493
558
  let curQuery = '';
494
559
  for (const curValue of value) {
@@ -532,12 +597,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
532
597
  }
533
598
  const name = `param${++count.i}`;
534
599
  curQuery +=
535
- ieTable +
536
- '."guid" ' +
537
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
538
- 'IN (SELECT "guid" FROM ' +
600
+ ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
601
+ 'EXISTS (SELECT "guid" FROM ' +
539
602
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
540
- ' WHERE "name"=@' +
603
+ ' WHERE "guid"=' +
604
+ ieTable +
605
+ '."guid" AND "name"=@' +
541
606
  name +
542
607
  ')';
543
608
  params[name] = curVar;
@@ -569,10 +634,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
569
634
  const name = `param${++count.i}`;
570
635
  curQuery +=
571
636
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
637
+ 'EXISTS (SELECT "guid" FROM ' +
638
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
639
+ ' WHERE "guid"=' +
572
640
  ieTable +
573
- '."guid" IN (SELECT "guid" FROM ' +
574
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
575
- ' WHERE "name"=@' +
641
+ '."guid" AND "name"=@' +
576
642
  name +
577
643
  ' AND "truthy"=TRUE)';
578
644
  params[name] = curVar;
@@ -619,10 +685,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
619
685
  const value = `param${++count.i}`;
620
686
  curQuery +=
621
687
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
688
+ 'EXISTS (SELECT "guid" FROM ' +
689
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
690
+ ' WHERE "guid"=' +
622
691
  ieTable +
623
- '."guid" IN (SELECT "guid" FROM ' +
624
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
625
- ' WHERE "name"=@' +
692
+ '."guid" AND "name"=@' +
626
693
  name +
627
694
  ' AND "number"=@' +
628
695
  value +
@@ -638,16 +705,19 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
638
705
  const value = `param${++count.i}`;
639
706
  curQuery +=
640
707
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
708
+ 'EXISTS (SELECT "guid" FROM ' +
709
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
710
+ ' WHERE "guid"=' +
641
711
  ieTable +
642
- '."guid" IN (SELECT "guid" FROM ' +
643
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
644
- ' WHERE "name"=@' +
712
+ '."guid" AND "name"=@' +
645
713
  name +
646
- ' AND "string"=@' +
647
- value +
714
+ ' AND "string"=' +
715
+ (curValue[1].length < 512
716
+ ? 'LEFT(@' + value + ', 512)'
717
+ : '@' + value) +
648
718
  ')';
649
719
  params[name] = curValue[0];
650
- params[value] = curValue[1];
720
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
651
721
  }
652
722
  else {
653
723
  if (curQuery) {
@@ -665,16 +735,17 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
665
735
  const value = `param${++count.i}`;
666
736
  curQuery +=
667
737
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
668
- ieTable +
669
- '."guid" IN (SELECT "guid" FROM ' +
738
+ 'EXISTS (SELECT "guid" FROM ' +
670
739
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
671
- ' WHERE "name"=@' +
740
+ ' WHERE "guid"=' +
741
+ ieTable +
742
+ '."guid" AND "name"=@' +
672
743
  name +
673
- ' AND "value"=@' +
744
+ ' AND "json"=@' +
674
745
  value +
675
746
  ')';
676
747
  params[name] = curValue[0];
677
- params[value] = svalue;
748
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
678
749
  }
679
750
  break;
680
751
  case 'contain':
@@ -687,7 +758,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
687
758
  curQuery +=
688
759
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
689
760
  ieTable +
690
- '."cdate"=' +
761
+ '."cdate"=@' +
691
762
  cdate;
692
763
  params[cdate] = isNaN(Number(curValue[1]))
693
764
  ? null
@@ -702,7 +773,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
702
773
  curQuery +=
703
774
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
704
775
  ieTable +
705
- '."mdate"=' +
776
+ '."mdate"=@' +
706
777
  mdate;
707
778
  params[mdate] = isNaN(Number(curValue[1]))
708
779
  ? null
@@ -714,55 +785,32 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
714
785
  curQuery += typeIsOr ? ' OR ' : ' AND ';
715
786
  }
716
787
  let svalue;
717
- let stringValue;
718
788
  if (curValue[1] instanceof Object &&
719
789
  typeof curValue[1].toReference === 'function') {
720
790
  svalue = JSON.stringify(curValue[1].toReference());
721
- stringValue = `${curValue[1].toReference()}`;
722
791
  }
723
- else {
792
+ else if (typeof curValue[1] === 'string' ||
793
+ typeof curValue[1] === 'number') {
724
794
  svalue = JSON.stringify(curValue[1]);
725
- stringValue = `${curValue[1]}`;
726
- }
727
- const name = `param${++count.i}`;
728
- const value = `param${++count.i}`;
729
- if (typeof curValue[1] === 'string') {
730
- const stringParam = `param${++count.i}`;
731
- curQuery +=
732
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
733
- '(' +
734
- ieTable +
735
- '."guid" IN (SELECT "guid" FROM ' +
736
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
737
- ' WHERE "name"=@' +
738
- name +
739
- ' AND position(@' +
740
- value +
741
- ' IN "value")>0) OR ' +
742
- ieTable +
743
- '."guid" IN (SELECT "guid" FROM ' +
744
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
745
- ' WHERE "name"=@' +
746
- name +
747
- ' AND "string"=@' +
748
- stringParam +
749
- '))';
750
- params[stringParam] = stringValue;
751
795
  }
752
796
  else {
753
- curQuery +=
754
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
755
- ieTable +
756
- '."guid" IN (SELECT "guid" FROM ' +
757
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
758
- ' WHERE "name"=@' +
759
- name +
760
- ' AND position(@' +
761
- value +
762
- ' IN "value")>0)';
797
+ svalue = JSON.stringify([curValue[1]]);
763
798
  }
799
+ const name = `param${++count.i}`;
800
+ const value = `param${++count.i}`;
801
+ curQuery +=
802
+ ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
803
+ 'EXISTS (SELECT "guid" FROM ' +
804
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
805
+ ' WHERE "guid"=' +
806
+ ieTable +
807
+ '."guid" AND "name"=@' +
808
+ name +
809
+ ' AND "json" @> @' +
810
+ value +
811
+ ')';
764
812
  params[name] = curValue[0];
765
- params[value] = svalue;
813
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
766
814
  }
767
815
  break;
768
816
  case 'match':
@@ -805,16 +853,17 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
805
853
  const value = `param${++count.i}`;
806
854
  curQuery +=
807
855
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
856
+ 'EXISTS (SELECT "guid" FROM ' +
857
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
858
+ ' WHERE "guid"=' +
808
859
  ieTable +
809
- '."guid" IN (SELECT "guid" FROM ' +
810
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
811
- ' WHERE "name"=@' +
860
+ '."guid" AND "name"=@' +
812
861
  name +
813
862
  ' AND "string" ~ @' +
814
863
  value +
815
864
  ')';
816
865
  params[name] = curValue[0];
817
- params[value] = curValue[1];
866
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
818
867
  }
819
868
  break;
820
869
  case 'imatch':
@@ -857,16 +906,17 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
857
906
  const value = `param${++count.i}`;
858
907
  curQuery +=
859
908
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
909
+ 'EXISTS (SELECT "guid" FROM ' +
910
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
911
+ ' WHERE "guid"=' +
860
912
  ieTable +
861
- '."guid" IN (SELECT "guid" FROM ' +
862
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
863
- ' WHERE "name"=@' +
913
+ '."guid" AND "name"=@' +
864
914
  name +
865
915
  ' AND "string" ~* @' +
866
916
  value +
867
917
  ')';
868
918
  params[name] = curValue[0];
869
- params[value] = curValue[1];
919
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
870
920
  }
871
921
  break;
872
922
  case 'like':
@@ -909,16 +959,17 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
909
959
  const value = `param${++count.i}`;
910
960
  curQuery +=
911
961
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
962
+ 'EXISTS (SELECT "guid" FROM ' +
963
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
964
+ ' WHERE "guid"=' +
912
965
  ieTable +
913
- '."guid" IN (SELECT "guid" FROM ' +
914
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
915
- ' WHERE "name"=@' +
966
+ '."guid" AND "name"=@' +
916
967
  name +
917
968
  ' AND "string" LIKE @' +
918
969
  value +
919
970
  ')';
920
971
  params[name] = curValue[0];
921
- params[value] = curValue[1];
972
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
922
973
  }
923
974
  break;
924
975
  case 'ilike':
@@ -961,16 +1012,17 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
961
1012
  const value = `param${++count.i}`;
962
1013
  curQuery +=
963
1014
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1015
+ 'EXISTS (SELECT "guid" FROM ' +
1016
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1017
+ ' WHERE "guid"=' +
964
1018
  ieTable +
965
- '."guid" IN (SELECT "guid" FROM ' +
966
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
967
- ' WHERE "name"=@' +
1019
+ '."guid" AND "name"=@' +
968
1020
  name +
969
1021
  ' AND "string" ILIKE @' +
970
1022
  value +
971
1023
  ')';
972
1024
  params[name] = curValue[0];
973
- params[value] = curValue[1];
1025
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
974
1026
  }
975
1027
  break;
976
1028
  case 'gt':
@@ -1013,10 +1065,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1013
1065
  const value = `param${++count.i}`;
1014
1066
  curQuery +=
1015
1067
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1068
+ 'EXISTS (SELECT "guid" FROM ' +
1069
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1070
+ ' WHERE "guid"=' +
1016
1071
  ieTable +
1017
- '."guid" IN (SELECT "guid" FROM ' +
1018
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1019
- ' WHERE "name"=@' +
1072
+ '."guid" AND "name"=@' +
1020
1073
  name +
1021
1074
  ' AND "number">@' +
1022
1075
  value +
@@ -1067,10 +1120,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1067
1120
  const value = `param${++count.i}`;
1068
1121
  curQuery +=
1069
1122
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1123
+ 'EXISTS (SELECT "guid" FROM ' +
1124
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1125
+ ' WHERE "guid"=' +
1070
1126
  ieTable +
1071
- '."guid" IN (SELECT "guid" FROM ' +
1072
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1073
- ' WHERE "name"=@' +
1127
+ '."guid" AND "name"=@' +
1074
1128
  name +
1075
1129
  ' AND "number">=@' +
1076
1130
  value +
@@ -1121,10 +1175,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1121
1175
  const value = `param${++count.i}`;
1122
1176
  curQuery +=
1123
1177
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1178
+ 'EXISTS (SELECT "guid" FROM ' +
1179
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1180
+ ' WHERE "guid"=' +
1124
1181
  ieTable +
1125
- '."guid" IN (SELECT "guid" FROM ' +
1126
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1127
- ' WHERE "name"=@' +
1182
+ '."guid" AND "name"=@' +
1128
1183
  name +
1129
1184
  ' AND "number"<@' +
1130
1185
  value +
@@ -1175,10 +1230,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1175
1230
  const value = `param${++count.i}`;
1176
1231
  curQuery +=
1177
1232
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1233
+ 'EXISTS (SELECT "guid" FROM ' +
1234
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1235
+ ' WHERE "guid"=' +
1178
1236
  ieTable +
1179
- '."guid" IN (SELECT "guid" FROM ' +
1180
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1181
- ' WHERE "name"=@' +
1237
+ '."guid" AND "name"=@' +
1182
1238
  name +
1183
1239
  ' AND "number"<=@' +
1184
1240
  value +
@@ -1208,10 +1264,11 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1208
1264
  const guid = `param${++count.i}`;
1209
1265
  curQuery +=
1210
1266
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1211
- ieTable +
1212
- '."guid" IN (SELECT "guid" FROM ' +
1267
+ 'EXISTS (SELECT "guid" FROM ' +
1213
1268
  PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1214
- ' WHERE "name"=@' +
1269
+ ' WHERE "guid"=' +
1270
+ ieTable +
1271
+ '."guid" AND "name"=@' +
1215
1272
  name +
1216
1273
  ' AND "reference"=decode(@' +
1217
1274
  guid +
@@ -1233,22 +1290,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1233
1290
  break;
1234
1291
  case 'qref':
1235
1292
  case '!qref':
1293
+ const referenceTableSuffix = (0, guid_1.makeTableSuffix)();
1236
1294
  const [qrefOptions, ...qrefSelectors] = curValue[1];
1237
1295
  const QrefEntityClass = qrefOptions.class;
1238
1296
  etypes.push(QrefEntityClass.ETYPE);
1239
- const qrefQuery = this.makeEntityQuery({ ...qrefOptions, return: 'guid', class: QrefEntityClass }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, (0, guid_1.makeTableSuffix)(), etypes);
1297
+ const qrefQuery = this.makeEntityQuery({ ...qrefOptions, return: 'guid', class: QrefEntityClass }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, (0, guid_1.makeTableSuffix)(), etypes, 'r' + referenceTableSuffix + '."reference"');
1240
1298
  if (curQuery) {
1241
1299
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1242
1300
  }
1243
1301
  const qrefName = `param${++count.i}`;
1244
1302
  curQuery +=
1245
1303
  ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1246
- ieTable +
1247
- '."guid" IN (SELECT "guid" FROM ' +
1304
+ 'EXISTS (SELECT "guid" FROM ' +
1248
1305
  PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1249
- ' WHERE "name"=@' +
1306
+ ' r' +
1307
+ referenceTableSuffix +
1308
+ ' WHERE r' +
1309
+ referenceTableSuffix +
1310
+ '."guid"=' +
1311
+ ieTable +
1312
+ '."guid" AND r' +
1313
+ referenceTableSuffix +
1314
+ '."name"=@' +
1250
1315
  qrefName +
1251
- ' AND "reference" IN (' +
1316
+ ' AND EXISTS (' +
1252
1317
  qrefQuery.query +
1253
1318
  '))';
1254
1319
  params[qrefName] = curValue[0];
@@ -1258,18 +1323,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1258
1323
  return curQuery;
1259
1324
  });
1260
1325
  let sortBy;
1326
+ let sortByInner;
1327
+ let sortJoin = '';
1328
+ let sortJoinInner = '';
1329
+ const order = options.reverse ? ' DESC' : '';
1261
1330
  switch (sort) {
1262
1331
  case 'mdate':
1263
- sortBy = '"mdate"';
1332
+ sortBy = `${eTable}."mdate"${order}`;
1333
+ sortByInner = `${ieTable}."mdate"${order}`;
1264
1334
  break;
1265
1335
  case 'cdate':
1336
+ sortBy = `${eTable}."cdate"${order}`;
1337
+ sortByInner = `${ieTable}."cdate"${order}`;
1338
+ break;
1266
1339
  default:
1267
- sortBy = '"cdate"';
1340
+ const name = `param${++count.i}`;
1341
+ sortJoin = `LEFT JOIN (
1342
+ SELECT "guid", "string", "number"
1343
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1344
+ WHERE "name"=@${name}
1345
+ ORDER BY "number"${order}, "string"${order}
1346
+ ) ${sTable} ON ${eTable}."guid"=${sTable}."guid"`;
1347
+ sortJoinInner = `LEFT JOIN (
1348
+ SELECT "guid", "string", "number"
1349
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1350
+ WHERE "name"=@${name}
1351
+ ORDER BY "number"${order}, "string"${order}
1352
+ ) ${sTable} ON ${ieTable}."guid"=${sTable}."guid"`;
1353
+ sortBy = `${sTable}."number"${order}, ${sTable}."string"${order}`;
1354
+ sortByInner = sortBy;
1355
+ params[name] = sort;
1268
1356
  break;
1269
1357
  }
1270
- if (options.reverse) {
1271
- sortBy += ' DESC';
1272
- }
1273
1358
  let query;
1274
1359
  if (queryParts.length) {
1275
1360
  if (subquery) {
@@ -1285,18 +1370,21 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1285
1370
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1286
1371
  }
1287
1372
  const whereClause = queryParts.join(') AND (');
1373
+ const guidClause = guidSelector
1374
+ ? `${ieTable}."guid"=${guidSelector} AND `
1375
+ : '';
1288
1376
  if (options.return === 'count') {
1289
1377
  if (limit || offset) {
1290
1378
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1291
- SELECT COUNT(${ieTable}."guid") AS "guid"
1379
+ SELECT ${ieTable}."guid" AS "guid"
1292
1380
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1293
- WHERE (${whereClause})${limit}${offset}
1381
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1294
1382
  ) ${countTable}`;
1295
1383
  }
1296
1384
  else {
1297
1385
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1298
1386
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1299
- WHERE (${whereClause})`;
1387
+ WHERE ${guidClause}(${whereClause})`;
1300
1388
  }
1301
1389
  }
1302
1390
  else if (options.return === 'guid') {
@@ -1305,8 +1393,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1305
1393
  : `${ieTable}."guid"`;
1306
1394
  query = `SELECT ${guidColumn} AS "guid"
1307
1395
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1308
- WHERE (${whereClause})
1309
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1396
+ ${sortJoinInner}
1397
+ WHERE ${guidClause}(${whereClause})
1398
+ ORDER BY ${sortByInner}, ${ieTable}."guid"${limit}${offset}`;
1310
1399
  }
1311
1400
  else {
1312
1401
  query = `SELECT
@@ -1316,18 +1405,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1316
1405
  ${eTable}."mdate",
1317
1406
  ${dTable}."name",
1318
1407
  ${dTable}."value",
1319
- ${cTable}."string",
1320
- ${cTable}."number"
1408
+ ${dTable}."json",
1409
+ ${dTable}."string",
1410
+ ${dTable}."number"
1321
1411
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1322
1412
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1323
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1413
+ ${sortJoin}
1324
1414
  INNER JOIN (
1325
1415
  SELECT ${ieTable}."guid"
1326
1416
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1327
- WHERE (${whereClause})
1328
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1417
+ ${sortJoinInner}
1418
+ WHERE ${guidClause}(${whereClause})
1419
+ ORDER BY ${sortByInner}${limit}${offset}
1329
1420
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1330
- ORDER BY ${eTable}.${sortBy}`;
1421
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1331
1422
  }
1332
1423
  }
1333
1424
  }
@@ -1344,16 +1435,19 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1344
1435
  if ('offset' in options) {
1345
1436
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1346
1437
  }
1438
+ const guidClause = guidSelector
1439
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
1440
+ : '';
1347
1441
  if (options.return === 'count') {
1348
1442
  if (limit || offset) {
1349
1443
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1350
- SELECT COUNT(${ieTable}."guid") AS "guid"
1351
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${limit}${offset}
1444
+ SELECT ${ieTable}."guid" AS "guid"
1445
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}${limit}${offset}
1352
1446
  ) ${countTable}`;
1353
1447
  }
1354
1448
  else {
1355
1449
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1356
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}`;
1450
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}`;
1357
1451
  }
1358
1452
  }
1359
1453
  else if (options.return === 'guid') {
@@ -1362,7 +1456,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1362
1456
  : `${ieTable}."guid"`;
1363
1457
  query = `SELECT ${guidColumn} AS "guid"
1364
1458
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1365
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1459
+ ${sortJoinInner}
1460
+ ${guidClause}
1461
+ ORDER BY ${sortByInner}, ${ieTable}."guid"${limit}${offset}`;
1366
1462
  }
1367
1463
  else {
1368
1464
  if (limit || offset) {
@@ -1373,17 +1469,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1373
1469
  ${eTable}."mdate",
1374
1470
  ${dTable}."name",
1375
1471
  ${dTable}."value",
1376
- ${cTable}."string",
1377
- ${cTable}."number"
1472
+ ${dTable}."json",
1473
+ ${dTable}."string",
1474
+ ${dTable}."number"
1378
1475
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1379
1476
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1380
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1477
+ ${sortJoin}
1381
1478
  INNER JOIN (
1382
1479
  SELECT ${ieTable}."guid"
1383
1480
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1384
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1481
+ ${sortJoinInner}
1482
+ ${guidClause}
1483
+ ORDER BY ${sortByInner}${limit}${offset}
1385
1484
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1386
- ORDER BY ${eTable}.${sortBy}`;
1485
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1387
1486
  }
1388
1487
  else {
1389
1488
  query = `SELECT
@@ -1393,12 +1492,14 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1393
1492
  ${eTable}."mdate",
1394
1493
  ${dTable}."name",
1395
1494
  ${dTable}."value",
1396
- ${cTable}."string",
1397
- ${cTable}."number"
1495
+ ${dTable}."json",
1496
+ ${dTable}."string",
1497
+ ${dTable}."number"
1398
1498
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1399
1499
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1400
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1401
- ORDER BY ${eTable}.${sortBy}`;
1500
+ ${sortJoin}
1501
+ ${guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
1502
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1402
1503
  }
1403
1504
  }
1404
1505
  }
@@ -1419,19 +1520,14 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1419
1520
  result,
1420
1521
  };
1421
1522
  }
1422
- performQuerySync(options, formattedSelectors, etype) {
1423
- const { query, params, etypes } = this.makeEntityQuery(options, formattedSelectors, etype);
1424
- const result = (this.queryIterSync(query, { etypes, params }) || [])[Symbol.iterator]();
1425
- return {
1426
- result,
1427
- };
1428
- }
1429
1523
  async getEntities(options = {}, ...selectors) {
1430
- const { result: resultPromise, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuery(options, formattedSelectors, etype), () => {
1524
+ const { result: resultPromise, process } = this.getEntitiesRowLike(
1525
+ // @ts-ignore: options is correct here.
1526
+ options, selectors, ({ options, selectors, etype }) => this.performQuery(options, selectors, etype), () => {
1431
1527
  const next = result.next();
1432
1528
  return next.done ? null : next.value;
1433
1529
  }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
1434
- tags: row.tags,
1530
+ tags: row.tags.filter((tag) => tag),
1435
1531
  cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1436
1532
  mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1437
1533
  }), (row) => ({
@@ -1439,8 +1535,10 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1439
1535
  svalue: row.value === 'N'
1440
1536
  ? JSON.stringify(Number(row.number))
1441
1537
  : row.value === 'S'
1442
- ? JSON.stringify(row.string)
1443
- : row.value,
1538
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(row.string))
1539
+ : row.value === 'J'
1540
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(row.json))
1541
+ : row.value,
1444
1542
  }));
1445
1543
  const result = await resultPromise;
1446
1544
  const value = process();
@@ -1449,28 +1547,6 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1449
1547
  }
1450
1548
  return value;
1451
1549
  }
1452
- getEntitiesSync(options = {}, ...selectors) {
1453
- const { result, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuerySync(options, formattedSelectors, etype), () => {
1454
- const next = result.next();
1455
- return next.done ? null : next.value;
1456
- }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
1457
- tags: row.tags,
1458
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1459
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1460
- }), (row) => ({
1461
- name: row.name,
1462
- svalue: row.value === 'N'
1463
- ? JSON.stringify(Number(row.number))
1464
- : row.value === 'S'
1465
- ? JSON.stringify(row.string)
1466
- : row.value,
1467
- }));
1468
- const value = process();
1469
- if (value instanceof Error) {
1470
- throw value;
1471
- }
1472
- return value;
1473
- }
1474
1550
  async getUID(name) {
1475
1551
  if (name == null) {
1476
1552
  throw new nymph_1.InvalidParametersError('Name not given for UID.');
@@ -1482,115 +1558,128 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1482
1558
  });
1483
1559
  return result?.cur_uid == null ? null : Number(result.cur_uid);
1484
1560
  }
1485
- async import(filename) {
1561
+ async importEntity({ guid, cdate, mdate, tags, sdata, etype, }) {
1486
1562
  try {
1487
- const result = await this.importFromFile(filename, async (guid, tags, sdata, etype) => {
1488
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1489
- etypes: [etype],
1490
- params: {
1491
- guid,
1492
- },
1493
- });
1494
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`, {
1563
+ let promises = [];
1564
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1565
+ etypes: [etype],
1566
+ params: {
1567
+ guid,
1568
+ },
1569
+ }));
1570
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1571
+ etypes: [etype],
1572
+ params: {
1573
+ guid,
1574
+ },
1575
+ }));
1576
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1577
+ etypes: [etype],
1578
+ params: {
1579
+ guid,
1580
+ },
1581
+ }));
1582
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1583
+ etypes: [etype],
1584
+ params: {
1585
+ guid,
1586
+ },
1587
+ }));
1588
+ await Promise.all(promises);
1589
+ promises = [];
1590
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`, {
1591
+ etypes: [etype],
1592
+ params: {
1593
+ guid,
1594
+ tags,
1595
+ cdate: isNaN(cdate) ? null : cdate,
1596
+ mdate: isNaN(mdate) ? null : mdate,
1597
+ },
1598
+ });
1599
+ for (const name in sdata) {
1600
+ const value = sdata[name];
1601
+ const uvalue = JSON.parse(value);
1602
+ if (value === undefined) {
1603
+ continue;
1604
+ }
1605
+ const storageValue = typeof uvalue === 'number'
1606
+ ? 'N'
1607
+ : typeof uvalue === 'string'
1608
+ ? 'S'
1609
+ : 'J';
1610
+ const jsonValue = storageValue === 'J'
1611
+ ? PostgreSQLDriver.escapeNullSequences(value)
1612
+ : null;
1613
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (decode(@guid, 'hex'), @name, @storageValue, @jsonValue, @string, @number, @truthy);`, {
1495
1614
  etypes: [etype],
1496
1615
  params: {
1497
1616
  guid,
1498
- tags,
1499
- cdate: isNaN(Number(JSON.parse(sdata.cdate)))
1500
- ? null
1501
- : Number(JSON.parse(sdata.cdate)),
1502
- mdate: isNaN(Number(JSON.parse(sdata.mdate)))
1617
+ name,
1618
+ storageValue,
1619
+ jsonValue,
1620
+ string: storageValue === 'J'
1503
1621
  ? null
1504
- : Number(JSON.parse(sdata.mdate)),
1505
- },
1506
- });
1507
- const promises = [];
1508
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1509
- etypes: [etype],
1510
- params: {
1511
- guid,
1512
- },
1513
- }));
1514
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1515
- etypes: [etype],
1516
- params: {
1517
- guid,
1518
- },
1519
- }));
1520
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1521
- etypes: [etype],
1522
- params: {
1523
- guid,
1622
+ : PostgreSQLDriver.escapeNulls(`${uvalue}`),
1623
+ number: isNaN(Number(uvalue)) ? null : Number(uvalue),
1624
+ truthy: !!uvalue,
1524
1625
  },
1525
1626
  }));
1526
- await Promise.all(promises);
1527
- delete sdata.cdate;
1528
- delete sdata.mdate;
1529
- for (const name in sdata) {
1530
- const value = sdata[name];
1531
- const uvalue = JSON.parse(value);
1532
- if (value === undefined) {
1533
- continue;
1534
- }
1535
- const storageValue = typeof uvalue === 'number'
1536
- ? 'N'
1537
- : typeof uvalue === 'string'
1538
- ? 'S'
1539
- : value;
1540
- const promises = [];
1541
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
1542
- etypes: [etype],
1543
- params: {
1544
- guid,
1545
- name,
1546
- storageValue,
1547
- },
1548
- }));
1549
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`, {
1627
+ const references = this.findReferences(value);
1628
+ for (const reference of references) {
1629
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`, {
1550
1630
  etypes: [etype],
1551
1631
  params: {
1552
1632
  guid,
1553
1633
  name,
1554
- truthy: !!uvalue,
1555
- string: `${uvalue}`,
1556
- number: isNaN(Number(uvalue)) ? null : Number(uvalue),
1634
+ reference,
1557
1635
  },
1558
1636
  }));
1559
- const references = this.findReferences(value);
1560
- for (const reference of references) {
1561
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} ("guid", "name", "reference") VALUES (decode(@guid, 'hex'), @name, decode(@reference, 'hex'));`, {
1562
- etypes: [etype],
1563
- params: {
1564
- guid,
1565
- name,
1566
- reference,
1567
- },
1568
- }));
1569
- }
1570
1637
  }
1571
- await Promise.all(promises);
1572
- }, async (name, curUid) => {
1573
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1574
- params: {
1575
- name,
1576
- },
1577
- });
1578
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
1638
+ }
1639
+ const uniques = await this.nymph
1640
+ .getEntityClassByEtype(etype)
1641
+ .getUniques({ guid, cdate, mdate, tags, data: {}, sdata });
1642
+ for (const unique of uniques) {
1643
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
1644
+ etypes: [etype],
1579
1645
  params: {
1580
- name,
1581
- curUid,
1646
+ guid,
1647
+ unique,
1582
1648
  },
1583
- });
1584
- }, async () => {
1585
- await this.internalTransaction('nymph-import');
1586
- }, async () => {
1587
- await this.commit('nymph-import');
1649
+ }).catch((e) => {
1650
+ if (e instanceof nymph_1.EntityUniqueConstraintError) {
1651
+ this.nymph.config.debugError('postgresql', `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
1652
+ }
1653
+ return e;
1654
+ }));
1655
+ }
1656
+ await Promise.all(promises);
1657
+ }
1658
+ catch (e) {
1659
+ this.nymph.config.debugError('postgresql', `Import entity error: "${e}"`);
1660
+ throw e;
1661
+ }
1662
+ }
1663
+ async importUID({ name, value }) {
1664
+ try {
1665
+ await this.internalTransaction(`nymph-import-uid-${name}`);
1666
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1667
+ params: {
1668
+ name,
1669
+ },
1588
1670
  });
1589
- return result;
1671
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @value);`, {
1672
+ params: {
1673
+ name,
1674
+ value,
1675
+ },
1676
+ });
1677
+ await this.commit(`nymph-import-uid-${name}`);
1590
1678
  }
1591
1679
  catch (e) {
1592
- await this.rollback('nymph-import');
1593
- return false;
1680
+ this.nymph.config.debugError('postgresql', `Import UID error: "${e}"`);
1681
+ await this.rollback(`nymph-import-uid-${name}`);
1682
+ throw e;
1594
1683
  }
1595
1684
  }
1596
1685
  async newUID(name) {
@@ -1598,13 +1687,14 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1598
1687
  throw new nymph_1.InvalidParametersError('Name not given for UID.');
1599
1688
  }
1600
1689
  await this.internalTransaction('nymph-newuid');
1690
+ let curUid = undefined;
1601
1691
  try {
1602
1692
  const lock = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name FOR UPDATE;`, {
1603
1693
  params: {
1604
1694
  name,
1605
1695
  },
1606
1696
  });
1607
- let curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
1697
+ curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
1608
1698
  if (curUid == null) {
1609
1699
  curUid = 1;
1610
1700
  await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
@@ -1623,13 +1713,14 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1623
1713
  },
1624
1714
  });
1625
1715
  }
1626
- await this.commit('nymph-newuid');
1627
- return curUid;
1628
1716
  }
1629
1717
  catch (e) {
1718
+ this.nymph.config.debugError('postgresql', `New UID error: "${e}"`);
1630
1719
  await this.rollback('nymph-newuid');
1631
1720
  throw e;
1632
1721
  }
1722
+ await this.commit('nymph-newuid');
1723
+ return curUid;
1633
1724
  }
1634
1725
  async renameUID(oldName, newName) {
1635
1726
  if (oldName == null || newName == null) {
@@ -1662,7 +1753,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1662
1753
  return true;
1663
1754
  }
1664
1755
  async saveEntity(entity) {
1665
- const insertData = async (guid, data, sdata, etype) => {
1756
+ const insertData = async (guid, data, sdata, uniques, etype) => {
1666
1757
  const runInsertQuery = async (name, value, svalue) => {
1667
1758
  if (value === undefined) {
1668
1759
  return;
@@ -1671,24 +1762,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1671
1762
  ? 'N'
1672
1763
  : typeof value === 'string'
1673
1764
  ? 'S'
1674
- : svalue;
1765
+ : 'J';
1766
+ const jsonValue = storageValue === 'J'
1767
+ ? PostgreSQLDriver.escapeNullSequences(svalue)
1768
+ : null;
1675
1769
  const promises = [];
1676
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
1770
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value", "json", "string", "number", "truthy") VALUES (decode(@guid, 'hex'), @name, @storageValue, @jsonValue, @string, @number, @truthy);`, {
1677
1771
  etypes: [etype],
1678
1772
  params: {
1679
1773
  guid,
1680
1774
  name,
1681
1775
  storageValue,
1682
- },
1683
- }));
1684
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ("guid", "name", "truthy", "string", "number") VALUES (decode(@guid, 'hex'), @name, @truthy, @string, @number);`, {
1685
- etypes: [etype],
1686
- params: {
1687
- guid,
1688
- name,
1689
- truthy: !!value,
1690
- string: `${value}`,
1776
+ jsonValue,
1777
+ string: storageValue === 'J'
1778
+ ? null
1779
+ : PostgreSQLDriver.escapeNulls(`${value}`),
1691
1780
  number: isNaN(Number(value)) ? null : Number(value),
1781
+ truthy: !!value,
1692
1782
  },
1693
1783
  }));
1694
1784
  const references = this.findReferences(svalue);
@@ -1704,6 +1794,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1704
1794
  }
1705
1795
  await Promise.all(promises);
1706
1796
  };
1797
+ for (const unique of uniques) {
1798
+ try {
1799
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
1800
+ etypes: [etype],
1801
+ params: {
1802
+ guid,
1803
+ unique,
1804
+ },
1805
+ });
1806
+ }
1807
+ catch (e) {
1808
+ if (e instanceof nymph_1.EntityUniqueConstraintError) {
1809
+ this.nymph.config.debugError('postgresql', `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
1810
+ }
1811
+ throw e;
1812
+ }
1813
+ }
1707
1814
  for (const name in data) {
1708
1815
  await runInsertQuery(name, data[name], JSON.stringify(data[name]));
1709
1816
  }
@@ -1711,8 +1818,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1711
1818
  await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1712
1819
  }
1713
1820
  };
1821
+ let inTransaction = false;
1714
1822
  try {
1715
- const result = await this.saveEntityRowLike(entity, async (_entity, guid, tags, data, sdata, cdate, etype) => {
1823
+ const result = await this.saveEntityRowLike(entity, async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
1824
+ if (Object.keys(data).length === 0 &&
1825
+ Object.keys(sdata).length === 0) {
1826
+ return false;
1827
+ }
1716
1828
  await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @cdate);`, {
1717
1829
  etypes: [etype],
1718
1830
  params: {
@@ -1721,9 +1833,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1721
1833
  cdate,
1722
1834
  },
1723
1835
  });
1724
- await insertData(guid, data, sdata, etype);
1836
+ await insertData(guid, data, sdata, uniques, etype);
1725
1837
  return true;
1726
- }, async (entity, guid, tags, data, sdata, mdate, etype) => {
1838
+ }, async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
1839
+ if (Object.keys(data).length === 0 &&
1840
+ Object.keys(sdata).length === 0) {
1841
+ return false;
1842
+ }
1727
1843
  const promises = [];
1728
1844
  promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1729
1845
  etypes: [etype],
@@ -1737,13 +1853,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1737
1853
  guid,
1738
1854
  },
1739
1855
  }));
1740
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1856
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1741
1857
  etypes: [etype],
1742
1858
  params: {
1743
1859
  guid,
1744
1860
  },
1745
1861
  }));
1746
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1862
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1747
1863
  etypes: [etype],
1748
1864
  params: {
1749
1865
  guid,
@@ -1768,38 +1884,45 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1768
1884
  guid,
1769
1885
  },
1770
1886
  }));
1771
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1887
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1772
1888
  etypes: [etype],
1773
1889
  params: {
1774
1890
  guid,
1775
1891
  },
1776
1892
  }));
1777
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1893
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1778
1894
  etypes: [etype],
1779
1895
  params: {
1780
1896
  guid,
1781
1897
  },
1782
1898
  }));
1783
1899
  await Promise.all(promises);
1784
- await insertData(guid, data, sdata, etype);
1900
+ await insertData(guid, data, sdata, uniques, etype);
1785
1901
  success = true;
1786
1902
  }
1787
1903
  return success;
1788
1904
  }, async () => {
1789
1905
  await this.internalTransaction('nymph-save');
1906
+ inTransaction = true;
1790
1907
  }, async (success) => {
1791
- if (success) {
1792
- await this.commit('nymph-save');
1793
- }
1794
- else {
1795
- await this.rollback('nymph-save');
1908
+ if (inTransaction) {
1909
+ inTransaction = false;
1910
+ if (success) {
1911
+ await this.commit('nymph-save');
1912
+ }
1913
+ else {
1914
+ await this.rollback('nymph-save');
1915
+ }
1796
1916
  }
1797
1917
  return success;
1798
1918
  });
1799
1919
  return result;
1800
1920
  }
1801
1921
  catch (e) {
1802
- await this.rollback('nymph-save');
1922
+ this.nymph.config.debugError('postgresql', `Save entity error: "${e}"`);
1923
+ if (inTransaction) {
1924
+ await this.rollback('nymph-save');
1925
+ }
1803
1926
  throw e;
1804
1927
  }
1805
1928
  }
@@ -1821,23 +1944,25 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1821
1944
  curUid,
1822
1945
  },
1823
1946
  });
1824
- await this.commit('nymph-setuid');
1825
- return true;
1826
1947
  }
1827
1948
  catch (e) {
1828
1949
  await this.rollback('nymph-setuid');
1829
1950
  throw e;
1830
1951
  }
1952
+ await this.commit('nymph-setuid');
1953
+ return true;
1831
1954
  }
1832
1955
  async internalTransaction(name) {
1833
1956
  if (name == null || typeof name !== 'string' || name.length === 0) {
1834
1957
  throw new nymph_1.InvalidParametersError('Transaction start attempted without a name.');
1835
1958
  }
1836
1959
  if (!this.transaction || this.transaction.count === 0) {
1960
+ // Lock to one connection.
1837
1961
  this.transaction = {
1838
1962
  count: 0,
1839
1963
  connection: await this.getConnection(),
1840
1964
  };
1965
+ // We're not in a transaction yet, so start one.
1841
1966
  await this.queryRun('BEGIN;');
1842
1967
  }
1843
1968
  await this.queryRun(`SAVEPOINT ${PostgreSQLDriver.escape(name)};`);
@@ -1845,7 +1970,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1845
1970
  return this.transaction;
1846
1971
  }
1847
1972
  async startTransaction(name) {
1848
- const inTransaction = this.inTransaction();
1973
+ const inTransaction = await this.inTransaction();
1849
1974
  const transaction = await this.internalTransaction(name);
1850
1975
  if (!inTransaction) {
1851
1976
  this.transaction = null;
@@ -1854,6 +1979,24 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1854
1979
  nymph.driver.transaction = transaction;
1855
1980
  return nymph;
1856
1981
  }
1982
+ async needsMigration() {
1983
+ const table = await this.queryGet('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix LIMIT 1;', {
1984
+ params: {
1985
+ db: this.config.database,
1986
+ prefix: this.prefix + 'data_' + '%',
1987
+ },
1988
+ });
1989
+ if (table?.table_name) {
1990
+ const result = await this.queryGet('SELECT 1 AS "exists" FROM "information_schema"."columns" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name"=@table AND "column_name"=\'json\';', {
1991
+ params: {
1992
+ db: this.config.database,
1993
+ table: table.table_name,
1994
+ },
1995
+ });
1996
+ return !result?.exists;
1997
+ }
1998
+ return false;
1999
+ }
1857
2000
  }
1858
2001
  exports.default = PostgreSQLDriver;
1859
2002
  //# sourceMappingURL=PostgreSQLDriver.js.map