@nymphjs/driver-postgresql 1.0.0-beta.1 → 1.0.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,57 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const child_process_1 = __importDefault(require("child_process"));
7
- const pg_1 = require("pg");
8
- const pg_format_1 = __importDefault(require("pg-format"));
9
- const nymph_1 = require("@nymphjs/nymph");
10
- const guid_1 = require("@nymphjs/guid");
11
- const conf_1 = require("./conf");
12
- class PostgreSQLDriver extends nymph_1.NymphDriver {
1
+ import pg from 'pg';
2
+ import format from 'pg-format';
3
+ import Cursor from 'pg-cursor';
4
+ import { NymphDriver, EntityUniqueConstraintError, InvalidParametersError, NotConfiguredError, QueryFailedError, UnableToConnectError, xor, } from '@nymphjs/nymph';
5
+ import { makeTableSuffix } from '@nymphjs/guid';
6
+ import { PostgreSQLDriverConfigDefaults as defaults, } from './conf/index.js';
7
+ /**
8
+ * The PostgreSQL Nymph database driver.
9
+ */
10
+ export default class PostgreSQLDriver extends NymphDriver {
11
+ config;
12
+ postgresqlConfig;
13
+ prefix;
14
+ connected = false;
15
+ // @ts-ignore: this is assigned in connect(), which is called by the constructor.
16
+ link;
17
+ transaction = null;
13
18
  static escape(input) {
14
- return pg_format_1.default.ident(input);
19
+ return format.ident(input);
15
20
  }
16
21
  static escapeValue(input) {
17
- return pg_format_1.default.literal(input);
22
+ return format.literal(input);
23
+ }
24
+ static escapeNullSequences(input) {
25
+ // Postgres doesn't support null bytes in `text`, and it converts strings
26
+ // in JSON to `text`, so we need to escape the escape sequences for null
27
+ // bytes.
28
+ return (input
29
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
30
+ // n so that if there's already an escape, it turns into \n
31
+ // - so that it won't match a \uFFFD that got turned into \uFFFD\uFFFD
32
+ .replace(/\\u0000/g, () => 'nu\uFFFD-')
33
+ .replace(/\\x00/g, () => 'nx\uFFFD-'));
34
+ }
35
+ static unescapeNullSequences(input) {
36
+ return input
37
+ .replace(/nu\uFFFD-/g, () => '\\u0000')
38
+ .replace(/nx\uFFFD-/g, () => '\\x00')
39
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
40
+ }
41
+ static escapeNulls(input) {
42
+ // Postgres doesn't support null bytes in `text`.
43
+ return input
44
+ .replace(/\uFFFD/g, () => '\uFFFD\uFFFD')
45
+ .replace(/\x00/g, () => '-\uFFFD-');
46
+ }
47
+ static unescapeNulls(input) {
48
+ return input
49
+ .replace(/-\uFFFD-/g, () => '\x00')
50
+ .replace(/\uFFFD\uFFFD/g, () => '\uFFFD');
18
51
  }
19
52
  constructor(config, link, transaction) {
20
53
  super();
21
- this.connected = false;
22
- this.transaction = null;
23
- this.config = { ...conf_1.PostgreSQLDriverConfigDefaults, ...config };
54
+ this.config = { ...defaults, ...config };
24
55
  const { host, user, password, database, port, customPoolConfig } = this.config;
25
56
  this.postgresqlConfig = customPoolConfig ?? {
26
57
  host,
@@ -41,16 +72,40 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
41
72
  this.connect();
42
73
  }
43
74
  }
44
- getConnection() {
45
- if (this.transaction != null && this.transaction.connection != null) {
75
+ /**
76
+ * This is used internally by Nymph. Don't call it yourself.
77
+ *
78
+ * @returns A clone of this instance.
79
+ */
80
+ clone() {
81
+ return new PostgreSQLDriver(this.config, this.link, this.transaction ?? undefined);
82
+ }
83
+ getConnection(outsideTransaction = false) {
84
+ if (this.transaction != null &&
85
+ this.transaction.connection != null &&
86
+ !outsideTransaction) {
46
87
  return Promise.resolve(this.transaction.connection);
47
88
  }
48
- return new Promise((resolve, reject) => this.link.connect((err, client, done) => err ? reject(err) : resolve({ client, done })));
89
+ return new Promise((resolve, reject) => this.link.connect((err, client, done) => err
90
+ ? reject(err)
91
+ : client
92
+ ? resolve({ client, done })
93
+ : reject('No client returned from connect.')));
49
94
  }
95
+ /**
96
+ * Connect to the PostgreSQL database.
97
+ *
98
+ * @returns Whether this instance is connected to a PostgreSQL database.
99
+ */
50
100
  async connect() {
101
+ // If we think we're connected, try pinging the server.
51
102
  try {
52
103
  if (this.connected) {
53
- const connection = await new Promise((resolve, reject) => this.link.connect((err, client, done) => err ? reject(err) : resolve({ client, done })));
104
+ const connection = await new Promise((resolve, reject) => this.link.connect((err, client, done) => err
105
+ ? reject(err)
106
+ : client
107
+ ? resolve({ client, done })
108
+ : reject('No client returned from connect.')));
54
109
  await new Promise((resolve, reject) => connection.client.query('SELECT 1;', [], (err, res) => {
55
110
  if (err) {
56
111
  reject(err);
@@ -63,9 +118,10 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
63
118
  catch (e) {
64
119
  this.connected = false;
65
120
  }
121
+ // Connecting, selecting database
66
122
  if (!this.connected) {
67
123
  try {
68
- this.link = new pg_1.Pool(this.postgresqlConfig);
124
+ this.link = new pg.Pool(this.postgresqlConfig);
69
125
  this.connected = true;
70
126
  }
71
127
  catch (e) {
@@ -73,15 +129,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
73
129
  this.postgresqlConfig.user === 'nymph' &&
74
130
  this.postgresqlConfig.password === 'password' &&
75
131
  this.postgresqlConfig.database === 'nymph') {
76
- throw new nymph_1.NotConfiguredError("It seems the config hasn't been set up correctly.");
132
+ throw new NotConfiguredError("It seems the config hasn't been set up correctly.");
77
133
  }
78
134
  else {
79
- throw new nymph_1.UnableToConnectError('Could not connect: ' + e?.message);
135
+ throw new UnableToConnectError('Could not connect: ' + e?.message);
80
136
  }
81
137
  }
82
138
  }
83
139
  return this.connected;
84
140
  }
141
+ /**
142
+ * Disconnect from the PostgreSQL database.
143
+ *
144
+ * @returns Whether this instance is connected to a PostgreSQL database.
145
+ */
85
146
  async disconnect() {
86
147
  if (this.connected) {
87
148
  await new Promise((resolve) => this.link.end(() => resolve(0)));
@@ -90,87 +151,179 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
90
151
  return this.connected;
91
152
  }
92
153
  async inTransaction() {
154
+ if (this.transaction && this.transaction.count === 0) {
155
+ this.transaction = null;
156
+ }
93
157
  return !!this.transaction;
94
158
  }
159
+ /**
160
+ * Check connection status.
161
+ *
162
+ * @returns Whether this instance is connected to a PostgreSQL database.
163
+ */
95
164
  isConnected() {
96
165
  return this.connected;
97
166
  }
98
- createTables(etype = null) {
99
- if (etype != null) {
100
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} (
167
+ async createEntitiesTable(etype, connection) {
168
+ // Create the entity table.
169
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} (
101
170
  "guid" BYTEA NOT NULL,
102
171
  "tags" TEXT[],
103
172
  "cdate" DOUBLE PRECISION NOT NULL,
104
173
  "mdate" DOUBLE PRECISION NOT NULL,
105
174
  PRIMARY KEY ("guid")
106
- ) WITH ( OIDS=FALSE );`);
107
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
108
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)};`);
109
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("cdate");`);
110
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)};`);
111
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("mdate");`);
112
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)};`);
113
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("tags");`);
114
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} (
115
- "guid" BYTEA NOT NULL,
116
- "name" TEXT NOT NULL,
117
- "value" TEXT NOT NULL,
118
- PRIMARY KEY ("guid", "name"),
119
- FOREIGN KEY ("guid")
120
- REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
121
- ) WITH ( OIDS=FALSE );`);
122
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
123
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)};`);
124
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid");`);
125
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)};`);
126
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name");`);
127
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)};`);
128
- 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;`);
129
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)};`);
130
- 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;`);
131
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} (
175
+ ) WITH ( OIDS=FALSE );`, { connection });
176
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
177
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)};`, { connection });
178
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_cdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("cdate");`, { connection });
179
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)};`, { connection });
180
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_mdate`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING btree ("mdate");`, { connection });
181
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)};`, { connection });
182
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}_id_tags`)} ON ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} USING gin ("tags");`, { connection });
183
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
184
+ }
185
+ async createDataTable(etype, connection) {
186
+ // Create the data table.
187
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} (
132
188
  "guid" BYTEA NOT NULL,
133
189
  "name" TEXT NOT NULL,
134
- "truthy" BOOLEAN,
190
+ "value" CHARACTER(1) NOT NULL,
191
+ "json" JSONB,
135
192
  "string" TEXT,
136
193
  "number" DOUBLE PRECISION,
194
+ "truthy" BOOLEAN,
137
195
  PRIMARY KEY ("guid", "name"),
138
196
  FOREIGN KEY ("guid")
139
197
  REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
140
- ) WITH ( OIDS=FALSE );`);
141
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
142
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid`)};`);
143
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("guid");`);
144
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_name`)};`);
145
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} USING btree ("name");`);
146
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_truthy`)};`);
147
- 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;`);
148
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}_id_guid_name_falsy`)};`);
149
- 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;`);
150
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} (
198
+ ) WITH ( OIDS=FALSE );`, { connection });
199
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
200
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)};`, { connection });
201
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid");`, { connection });
202
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name`)};`, { connection });
203
+ 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 });
204
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__user`)};`, { connection });
205
+ 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 });
206
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name__group`)};`, { connection });
207
+ 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 });
208
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)};`, { connection });
209
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name");`, { connection });
210
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_string`)};`, { connection });
211
+ 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 });
212
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_number`)};`, { connection });
213
+ 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 });
214
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_number`)};`, { connection });
215
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_number`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid", "name", "number");`, { connection });
216
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_truthy`)};`, { connection });
217
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("name", "truthy");`, { connection });
218
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_truthy`)};`, { connection });
219
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_guid_name_truthy`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid", "name", "truthy");`, { connection });
220
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_string`)};`, { connection });
221
+ 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 });
222
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_json`)};`, { connection });
223
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_json`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING gin ("json");`, { connection });
224
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acuserread`)};`, { connection });
225
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acuserread`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name"='acUser' AND "number" >= 1;`, { connection });
226
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acgroupread`)};`, { connection });
227
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acgroupread`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name"='acGroup' AND "number" >= 1;`, { connection });
228
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acotherread`)};`, { connection });
229
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acotherread`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name"='acOther' AND "number" >= 1;`, { connection });
230
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acuser`)};`, { connection });
231
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acuser`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name"='user';`, { connection });
232
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acgroup`)};`, { connection });
233
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}_id_acgroup`)} ON ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} USING btree ("guid") WHERE "name"='group';`, { connection });
234
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
235
+ }
236
+ async createReferencesTable(etype, connection) {
237
+ // Create the references table.
238
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} (
151
239
  "guid" BYTEA NOT NULL,
152
240
  "name" TEXT NOT NULL,
153
241
  "reference" BYTEA NOT NULL,
154
242
  PRIMARY KEY ("guid", "name", "reference"),
155
243
  FOREIGN KEY ("guid")
156
244
  REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
157
- ) WITH ( OIDS=FALSE );`);
158
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
159
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)};`);
160
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid");`);
161
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)};`);
162
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name");`);
163
- this.queryRunSync(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)};`);
164
- this.queryRunSync(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference");`);
245
+ ) WITH ( OIDS=FALSE );`, { connection });
246
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
247
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)};`, { connection });
248
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid");`, { connection });
249
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)};`, { connection });
250
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("name");`, { connection });
251
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_name_reference`)};`, { connection });
252
+ 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 });
253
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)};`, { connection });
254
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference");`, { connection });
255
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_name_reference`)};`, { connection });
256
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_name_reference`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid", "name", "reference");`, { connection });
257
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_name_guid`)};`, { connection });
258
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_name_guid`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "name", "guid");`, { connection });
259
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_name`)};`, { connection });
260
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_reference_guid_name`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("reference", "guid", "name");`, { connection });
261
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_nameuser`)};`, { connection });
262
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_nameuser`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid", "reference") WHERE "name"='user';`, { connection });
263
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_namegroup`)};`, { connection });
264
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}_id_guid_reference_namegroup`)} ON ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} USING btree ("guid", "reference") WHERE "name"='group';`, { connection });
265
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
266
+ }
267
+ async createTokensTable(etype, connection) {
268
+ // Create the tokens table.
269
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} (
270
+ "guid" BYTEA NOT NULL,
271
+ "name" TEXT NOT NULL,
272
+ "token" INTEGER NOT NULL,
273
+ "position" INTEGER NOT NULL,
274
+ "stem" BOOLEAN NOT NULL,
275
+ PRIMARY KEY ("guid", "name", "token", "position"),
276
+ FOREIGN KEY ("guid")
277
+ REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
278
+ ) WITH ( OIDS=FALSE );`, { connection });
279
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
280
+ await this.queryRun(`DROP INDEX IF EXISTS ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_name_token`)};`, { connection });
281
+ await this.queryRun(`CREATE INDEX ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}_id_name_token`)} ON ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} USING btree ("name", "token");`, { connection });
282
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} SET ( autovacuum_vacuum_scale_factor = 0.05, autovacuum_analyze_scale_factor = 0.05 );`, { connection });
283
+ }
284
+ async createUniquesTable(etype, connection) {
285
+ // Create the unique strings table.
286
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} (
287
+ "guid" BYTEA NOT NULL,
288
+ "unique" TEXT NOT NULL UNIQUE,
289
+ PRIMARY KEY ("guid", "unique"),
290
+ FOREIGN KEY ("guid")
291
+ REFERENCES ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE CASCADE
292
+ ) WITH ( OIDS=FALSE );`, { connection });
293
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
294
+ }
295
+ /**
296
+ * Create entity tables in the database.
297
+ *
298
+ * @param etype The entity type to create a table for. If this is blank, the default tables are created.
299
+ * @returns True on success, false on failure.
300
+ */
301
+ async createTables(etype = null) {
302
+ const connection = await this.getConnection(true);
303
+ if (etype != null) {
304
+ await this.createEntitiesTable(etype, connection);
305
+ await this.createDataTable(etype, connection);
306
+ await this.createReferencesTable(etype, connection);
307
+ await this.createTokensTable(etype, connection);
308
+ await this.createUniquesTable(etype, connection);
165
309
  }
166
310
  else {
167
- this.queryRunSync(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uids`)} (
311
+ // Add trigram extensions.
312
+ try {
313
+ await this.queryRun(`CREATE EXTENSION pg_trgm;`, { connection });
314
+ }
315
+ catch (e) {
316
+ // Ignore errors.
317
+ }
318
+ // Create the UID table.
319
+ await this.queryRun(`CREATE TABLE IF NOT EXISTS ${PostgreSQLDriver.escape(`${this.prefix}uids`)} (
168
320
  "name" TEXT NOT NULL,
169
321
  "cur_uid" BIGINT NOT NULL,
170
322
  PRIMARY KEY ("name")
171
- ) WITH ( OIDS = FALSE );`);
172
- this.queryRunSync(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`);
323
+ ) WITH ( OIDS = FALSE );`, { connection });
324
+ await this.queryRun(`ALTER TABLE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} OWNER TO ${PostgreSQLDriver.escape(this.config.user)};`, { connection });
173
325
  }
326
+ connection.done();
174
327
  return true;
175
328
  }
176
329
  translateQuery(origQuery, origParams) {
@@ -188,49 +341,32 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
188
341
  }
189
342
  async query(runQuery, query, etypes = []) {
190
343
  try {
344
+ this.nymph.config.debugInfo('postgresql:query', query);
191
345
  return await runQuery();
192
346
  }
193
347
  catch (e) {
194
348
  const errorCode = e?.code;
195
- if (errorCode === '42P01' && this.createTables()) {
349
+ if (errorCode === '42P01' && (await this.createTables())) {
350
+ // If the tables don't exist yet, create them.
196
351
  for (let etype of etypes) {
197
- this.createTables(etype);
352
+ await this.createTables(etype);
198
353
  }
199
354
  try {
200
355
  return await runQuery();
201
356
  }
202
357
  catch (e2) {
203
- throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
358
+ throw new QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
204
359
  }
205
360
  }
206
- else {
207
- throw e;
208
- }
209
- }
210
- }
211
- querySync(runQuery, query, etypes = []) {
212
- try {
213
- return runQuery();
214
- }
215
- catch (e) {
216
- const errorCode = e?.code;
217
- if (errorCode === '42P01' && this.createTables()) {
218
- for (let etype of etypes) {
219
- this.createTables(etype);
220
- }
221
- try {
222
- return runQuery();
223
- }
224
- catch (e2) {
225
- throw new nymph_1.QueryFailedError('Query failed: ' + e2?.code + ' - ' + e2?.message, query);
226
- }
361
+ else if (errorCode === '23505') {
362
+ throw new EntityUniqueConstraintError(`Unique constraint violation.`);
227
363
  }
228
364
  else {
229
- throw e;
365
+ throw new QueryFailedError('Query failed: ' + e?.code + ' - ' + e?.message, query);
230
366
  }
231
367
  }
232
368
  }
233
- queryIter(query, { etypes = [], params = {}, } = {}) {
369
+ queryArray(query, { etypes = [], params = {}, } = {}) {
234
370
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
235
371
  return this.query(async () => {
236
372
  const results = await new Promise((resolve, reject) => {
@@ -246,32 +382,30 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
246
382
  return results.rows;
247
383
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
248
384
  }
249
- queryIterSync(query, { etypes = [], params = {}, } = {}) {
385
+ async queryIter(query, { etypes = [], params = {}, } = {}) {
250
386
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
251
- return this.querySync(() => {
252
- const output = child_process_1.default.spawnSync(process.argv0, [__dirname + '/runPostgresqlSync.js'], {
253
- input: JSON.stringify({
254
- postgresqlConfig: this.postgresqlConfig,
255
- query: newQuery,
256
- params: newParams,
257
- }),
258
- timeout: 30000,
259
- maxBuffer: 100 * 1024 * 1024,
260
- encoding: 'utf8',
261
- windowsHide: true,
262
- });
263
- try {
264
- return JSON.parse(output.stdout).rows;
265
- }
266
- catch (e) {
267
- }
268
- if (output.status === 0) {
269
- throw new Error('Unknown parse error.');
270
- }
271
- const err = JSON.parse(output.stderr);
272
- const e = new Error(err.name);
273
- for (const name in err) {
274
- e[name] = err[name];
387
+ const that = this;
388
+ return this.query(async function* () {
389
+ const transaction = !!that.transaction?.connection;
390
+ const connection = await that.getConnection();
391
+ const cursor = new Cursor(newQuery, newParams);
392
+ const iter = connection.client.query(cursor);
393
+ while (true) {
394
+ const rows = await iter.read(100);
395
+ if (!rows.length) {
396
+ await new Promise((resolve) => {
397
+ iter.close(() => {
398
+ if (!transaction) {
399
+ connection.done();
400
+ }
401
+ resolve();
402
+ });
403
+ });
404
+ return;
405
+ }
406
+ for (let row of rows) {
407
+ yield row;
408
+ }
275
409
  }
276
410
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
277
411
  }
@@ -291,12 +425,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
291
425
  return results.rows[0];
292
426
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
293
427
  }
294
- queryRun(query, { etypes = [], params = {}, } = {}) {
428
+ queryRun(query, { etypes = [], params = {}, connection, } = {}) {
295
429
  const { query: newQuery, params: newParams } = this.translateQuery(query, params);
296
430
  return this.query(async () => {
297
431
  const results = await new Promise((resolve, reject) => {
298
432
  try {
299
- (this.transaction?.connection?.client ?? this.link)
433
+ ((connection ?? this.transaction?.connection)?.client ??
434
+ this.link)
300
435
  .query(newQuery, newParams)
301
436
  .then((results) => resolve(results), (error) => reject(error));
302
437
  }
@@ -307,39 +442,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
307
442
  return { rowCount: results.rowCount ?? 0 };
308
443
  }, `${query} -- ${JSON.stringify(params)}`, etypes);
309
444
  }
310
- queryRunSync(query, { etypes = [], params = {}, } = {}) {
311
- const { query: newQuery, params: newParams } = this.translateQuery(query, params);
312
- return this.querySync(() => {
313
- const output = child_process_1.default.spawnSync(process.argv0, [__dirname + '/runPostgresqlSync.js'], {
314
- input: JSON.stringify({
315
- postgresqlConfig: this.postgresqlConfig,
316
- query: newQuery,
317
- params: newParams,
318
- }),
319
- timeout: 30000,
320
- maxBuffer: 100 * 1024 * 1024,
321
- encoding: 'utf8',
322
- windowsHide: true,
323
- });
324
- try {
325
- const results = JSON.parse(output.stdout);
326
- return { rowCount: results.rowCount ?? 0 };
327
- }
328
- catch (e) {
329
- }
330
- if (output.status === 0) {
331
- throw new Error('Unknown parse error.');
332
- }
333
- const err = JSON.parse(output.stderr);
334
- const e = new Error(err.name);
335
- for (const name in err) {
336
- e[name] = err[name];
337
- }
338
- }, `${query} -- ${JSON.stringify(params)}`, etypes);
339
- }
340
445
  async commit(name) {
341
446
  if (name == null || typeof name !== 'string' || name.length === 0) {
342
- throw new nymph_1.InvalidParametersError('Transaction commit attempted without a name.');
447
+ throw new InvalidParametersError('Transaction commit attempted without a name.');
343
448
  }
344
449
  if (!this.transaction || this.transaction.count === 0) {
345
450
  this.transaction = null;
@@ -379,32 +484,40 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
379
484
  guid,
380
485
  },
381
486
  });
382
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
487
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
383
488
  etypes: [etype],
384
489
  params: {
385
490
  guid,
386
491
  },
387
492
  });
388
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
493
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
494
+ etypes: [etype],
495
+ params: {
496
+ guid,
497
+ },
498
+ });
499
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
389
500
  etypes: [etype],
390
501
  params: {
391
502
  guid,
392
503
  },
393
504
  });
394
- await this.commit('nymph-delete');
395
- if (this.nymph.config.cache) {
396
- this.cleanCache(guid);
397
- }
398
- return true;
399
505
  }
400
506
  catch (e) {
507
+ this.nymph.config.debugError('postgresql', `Delete entity error: "${e}"`);
401
508
  await this.rollback('nymph-delete');
402
509
  throw e;
403
510
  }
511
+ await this.commit('nymph-delete');
512
+ // Remove any cached versions of this entity.
513
+ if (this.nymph.config.cache) {
514
+ this.cleanCache(guid);
515
+ }
516
+ return true;
404
517
  }
405
518
  async deleteUID(name) {
406
519
  if (!name) {
407
- throw new nymph_1.InvalidParametersError('Name not given for UID');
520
+ throw new InvalidParametersError('Name not given for UID');
408
521
  }
409
522
  await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
410
523
  params: {
@@ -413,79 +526,128 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
413
526
  });
414
527
  return true;
415
528
  }
416
- async exportEntities(writeLine) {
417
- writeLine('#nex2');
418
- writeLine('# Nymph Entity Exchange v2');
419
- writeLine('# http://nymph.io');
420
- writeLine('#');
421
- writeLine('# Generation Time: ' + new Date().toLocaleString());
422
- writeLine('');
423
- writeLine('#');
424
- writeLine('# UIDs');
425
- writeLine('#');
426
- writeLine('');
427
- let uids = await this.queryIter(`SELECT * FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
428
- for (const uid of uids) {
429
- writeLine(`<${uid.name}>[${uid.cur_uid}]`);
430
- }
431
- writeLine('');
432
- writeLine('#');
433
- writeLine('# Entities');
434
- writeLine('#');
435
- writeLine('');
436
- const tables = await this.queryIter('SELECT relname FROM pg_stat_user_tables ORDER BY relname;');
529
+ async getEtypes() {
530
+ const tables = await this.queryArray('SELECT "table_name" AS "table_name" FROM "information_schema"."tables" WHERE "table_catalog"=@db AND "table_schema"=\'public\' AND "table_name" LIKE @prefix;', {
531
+ params: {
532
+ db: this.config.database,
533
+ prefix: this.prefix + 'entities_' + '%',
534
+ },
535
+ });
437
536
  const etypes = [];
438
- for (const tableRow of tables) {
439
- const table = tableRow.relname;
440
- if (table.startsWith(this.prefix + 'entities_')) {
441
- etypes.push(table.substr((this.prefix + 'entities_').length));
537
+ for (const table of tables) {
538
+ etypes.push(table.table_name.substr((this.prefix + 'entities_').length));
539
+ }
540
+ return etypes;
541
+ }
542
+ async *exportDataIterator() {
543
+ if (yield {
544
+ type: 'comment',
545
+ content: `#nex2
546
+ # Nymph Entity Exchange v2
547
+ # http://nymph.io
548
+ #
549
+ # Generation Time: ${new Date().toLocaleString()}
550
+ `,
551
+ }) {
552
+ return;
553
+ }
554
+ if (yield {
555
+ type: 'comment',
556
+ content: `
557
+
558
+ #
559
+ # UIDs
560
+ #
561
+
562
+ `,
563
+ }) {
564
+ return;
565
+ }
566
+ // Export UIDs.
567
+ let uids = await this.queryIter(`SELECT * FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ORDER BY "name";`);
568
+ for await (const uid of uids) {
569
+ if (yield { type: 'uid', content: `<${uid.name}>[${uid.cur_uid}]\n` }) {
570
+ return;
442
571
  }
443
572
  }
573
+ if (yield {
574
+ type: 'comment',
575
+ content: `
576
+
577
+ #
578
+ # Entities
579
+ #
580
+
581
+ `,
582
+ }) {
583
+ return;
584
+ }
585
+ // Get the etypes.
586
+ const etypes = await this.getEtypes();
444
587
  for (const etype of etypes) {
445
- 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"
588
+ // Export entities.
589
+ 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"
446
590
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} e
447
591
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} d ON e."guid"=d."guid"
448
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} c ON d."guid"=c."guid" AND d."name"=c."name"
449
- ORDER BY e."guid";`))[Symbol.iterator]();
450
- let datum = dataIterator.next();
592
+ ORDER BY e."guid";`);
593
+ let datum = await dataIterator.next();
451
594
  while (!datum.done) {
452
595
  const guid = datum.value.guid;
453
- const tags = datum.value.tags.join(',');
596
+ const tags = datum.value.tags.filter((tag) => tag).join(',');
454
597
  const cdate = datum.value.cdate;
455
598
  const mdate = datum.value.mdate;
456
- writeLine(`{${guid}}<${etype}>[${tags}]`);
457
- writeLine(`\tcdate=${JSON.stringify(cdate)}`);
458
- writeLine(`\tmdate=${JSON.stringify(mdate)}`);
459
- if (datum.value.dname != null) {
599
+ let currentEntityExport = [];
600
+ currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
601
+ currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
602
+ currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
603
+ if (datum.value.name != null) {
604
+ // This do will keep going and adding the data until the
605
+ // next entity is reached. $row will end on the next entity.
460
606
  do {
461
- const value = datum.value.dvalue === 'N'
607
+ const value = datum.value.value === 'N'
462
608
  ? JSON.stringify(Number(datum.value.number))
463
- : datum.value.dvalue === 'S'
464
- ? JSON.stringify(datum.value.string)
465
- : datum.value.dvalue;
466
- writeLine(`\t${datum.value.dname}=${value}`);
467
- datum = dataIterator.next();
609
+ : datum.value.value === 'S'
610
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(datum.value.string))
611
+ : datum.value.value === 'J'
612
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(datum.value.json))
613
+ : datum.value.value;
614
+ currentEntityExport.push(`\t${datum.value.name}=${value}`);
615
+ datum = await dataIterator.next();
468
616
  } while (!datum.done && datum.value.guid === guid);
469
617
  }
470
618
  else {
471
- datum = dataIterator.next();
619
+ // Make sure that datum is incremented :)
620
+ datum = await dataIterator.next();
621
+ }
622
+ currentEntityExport.push('');
623
+ if (yield { type: 'entity', content: currentEntityExport.join('\n') }) {
624
+ return;
472
625
  }
473
626
  }
474
627
  }
475
- return;
476
628
  }
477
- makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = []) {
629
+ /**
630
+ * Generate the PostgreSQL query.
631
+ * @param options The options array.
632
+ * @param formattedSelectors The formatted selector array.
633
+ * @param etype
634
+ * @param count Used to track internal params.
635
+ * @param params Used to store internal params.
636
+ * @param subquery Whether only a subquery should be returned.
637
+ * @returns The SQL query.
638
+ */
639
+ makeEntityQuery(options, formattedSelectors, etype, count = { i: 0 }, params = {}, subquery = false, tableSuffix = '', etypes = [], guidSelector = undefined) {
478
640
  if (typeof options.class?.alterOptions === 'function') {
479
641
  options = options.class.alterOptions(options);
480
642
  }
481
643
  const eTable = `e${tableSuffix}`;
482
644
  const dTable = `d${tableSuffix}`;
483
- const cTable = `c${tableSuffix}`;
484
645
  const fTable = `f${tableSuffix}`;
485
646
  const ieTable = `ie${tableSuffix}`;
486
647
  const countTable = `count${tableSuffix}`;
648
+ const sTable = `s${tableSuffix}`;
487
649
  const sort = options.sort ?? 'cdate';
488
- const queryParts = this.iterateSelectorsForQuery(formattedSelectors, (key, value, typeIsOr, typeIsNot) => {
650
+ const queryParts = this.iterateSelectorsForQuery(formattedSelectors, ({ key, value, typeIsOr, typeIsNot }) => {
489
651
  const clauseNot = key.startsWith('!');
490
652
  let curQuery = '';
491
653
  for (const curValue of value) {
@@ -498,7 +660,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
498
660
  }
499
661
  const guid = `param${++count.i}`;
500
662
  curQuery +=
501
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
663
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
502
664
  ieTable +
503
665
  '."guid"=decode(@' +
504
666
  guid +
@@ -514,7 +676,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
514
676
  }
515
677
  const tag = `param${++count.i}`;
516
678
  curQuery +=
517
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
679
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
518
680
  '@' +
519
681
  tag +
520
682
  ' <@ ie."tags"';
@@ -529,12 +691,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
529
691
  }
530
692
  const name = `param${++count.i}`;
531
693
  curQuery +=
532
- ieTable +
533
- '."guid" ' +
534
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
535
- 'IN (SELECT "guid" FROM ' +
694
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
695
+ 'EXISTS (SELECT "guid" FROM ' +
536
696
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
537
- ' WHERE "name"=@' +
697
+ ' WHERE "guid"=' +
698
+ ieTable +
699
+ '."guid" AND "name"=@' +
538
700
  name +
539
701
  ')';
540
702
  params[name] = curVar;
@@ -548,7 +710,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
548
710
  }
549
711
  if (curVar === 'cdate') {
550
712
  curQuery +=
551
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
713
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
552
714
  '(' +
553
715
  ieTable +
554
716
  '."cdate" NOT NULL)';
@@ -556,7 +718,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
556
718
  }
557
719
  else if (curVar === 'mdate') {
558
720
  curQuery +=
559
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
721
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
560
722
  '(' +
561
723
  ieTable +
562
724
  '."mdate" NOT NULL)';
@@ -565,11 +727,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
565
727
  else {
566
728
  const name = `param${++count.i}`;
567
729
  curQuery +=
568
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
730
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
731
+ 'EXISTS (SELECT "guid" FROM ' +
732
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
733
+ ' WHERE "guid"=' +
569
734
  ieTable +
570
- '."guid" IN (SELECT "guid" FROM ' +
571
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
572
- ' WHERE "name"=@' +
735
+ '."guid" AND "name"=@' +
573
736
  name +
574
737
  ' AND "truthy"=TRUE)';
575
738
  params[name] = curVar;
@@ -584,7 +747,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
584
747
  }
585
748
  const cdate = `param${++count.i}`;
586
749
  curQuery +=
587
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
750
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
588
751
  ieTable +
589
752
  '."cdate"=@' +
590
753
  cdate;
@@ -599,7 +762,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
599
762
  }
600
763
  const mdate = `param${++count.i}`;
601
764
  curQuery +=
602
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
765
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
603
766
  ieTable +
604
767
  '."mdate"=@' +
605
768
  mdate;
@@ -615,11 +778,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
615
778
  const name = `param${++count.i}`;
616
779
  const value = `param${++count.i}`;
617
780
  curQuery +=
618
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
781
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
782
+ 'EXISTS (SELECT "guid" FROM ' +
783
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
784
+ ' WHERE "guid"=' +
619
785
  ieTable +
620
- '."guid" IN (SELECT "guid" FROM ' +
621
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
622
- ' WHERE "name"=@' +
786
+ '."guid" AND "name"=@' +
623
787
  name +
624
788
  ' AND "number"=@' +
625
789
  value +
@@ -634,17 +798,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
634
798
  const name = `param${++count.i}`;
635
799
  const value = `param${++count.i}`;
636
800
  curQuery +=
637
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
801
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
802
+ 'EXISTS (SELECT "guid" FROM ' +
803
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
804
+ ' WHERE "guid"=' +
638
805
  ieTable +
639
- '."guid" IN (SELECT "guid" FROM ' +
640
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
641
- ' WHERE "name"=@' +
806
+ '."guid" AND "name"=@' +
642
807
  name +
643
- ' AND "string"=@' +
644
- value +
808
+ ' AND "string"=' +
809
+ (curValue[1].length < 512
810
+ ? 'LEFT(@' + value + ', 512)'
811
+ : '@' + value) +
645
812
  ')';
646
813
  params[name] = curValue[0];
647
- params[value] = curValue[1];
814
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
648
815
  }
649
816
  else {
650
817
  if (curQuery) {
@@ -661,17 +828,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
661
828
  const name = `param${++count.i}`;
662
829
  const value = `param${++count.i}`;
663
830
  curQuery +=
664
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
665
- ieTable +
666
- '."guid" IN (SELECT "guid" FROM ' +
831
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
832
+ 'EXISTS (SELECT "guid" FROM ' +
667
833
  PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
668
- ' WHERE "name"=@' +
834
+ ' WHERE "guid"=' +
835
+ ieTable +
836
+ '."guid" AND "name"=@' +
669
837
  name +
670
- ' AND "value"=@' +
838
+ ' AND "json"=@' +
671
839
  value +
672
840
  ')';
673
841
  params[name] = curValue[0];
674
- params[value] = svalue;
842
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
675
843
  }
676
844
  break;
677
845
  case 'contain':
@@ -682,9 +850,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
682
850
  }
683
851
  const cdate = `param${++count.i}`;
684
852
  curQuery +=
685
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
853
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
686
854
  ieTable +
687
- '."cdate"=' +
855
+ '."cdate"=@' +
688
856
  cdate;
689
857
  params[cdate] = isNaN(Number(curValue[1]))
690
858
  ? null
@@ -697,9 +865,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
697
865
  }
698
866
  const mdate = `param${++count.i}`;
699
867
  curQuery +=
700
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
868
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
701
869
  ieTable +
702
- '."mdate"=' +
870
+ '."mdate"=@' +
703
871
  mdate;
704
872
  params[mdate] = isNaN(Number(curValue[1]))
705
873
  ? null
@@ -711,55 +879,151 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
711
879
  curQuery += typeIsOr ? ' OR ' : ' AND ';
712
880
  }
713
881
  let svalue;
714
- let stringValue;
715
882
  if (curValue[1] instanceof Object &&
716
883
  typeof curValue[1].toReference === 'function') {
717
884
  svalue = JSON.stringify(curValue[1].toReference());
718
- stringValue = `${curValue[1].toReference()}`;
719
885
  }
720
- else {
886
+ else if (typeof curValue[1] === 'string' ||
887
+ typeof curValue[1] === 'number') {
721
888
  svalue = JSON.stringify(curValue[1]);
722
- stringValue = `${curValue[1]}`;
889
+ }
890
+ else {
891
+ svalue = JSON.stringify([curValue[1]]);
723
892
  }
724
893
  const name = `param${++count.i}`;
725
894
  const value = `param${++count.i}`;
726
- if (typeof curValue[1] === 'string') {
727
- const stringParam = `param${++count.i}`;
728
- curQuery +=
729
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
730
- '(' +
731
- ieTable +
732
- '."guid" IN (SELECT "guid" FROM ' +
733
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
734
- ' WHERE "name"=@' +
735
- name +
736
- ' AND position(@' +
737
- value +
738
- ' IN "value")>0) OR ' +
739
- ieTable +
740
- '."guid" IN (SELECT "guid" FROM ' +
741
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
742
- ' WHERE "name"=@' +
743
- name +
744
- ' AND "string"=@' +
745
- stringParam +
746
- '))';
747
- params[stringParam] = stringValue;
895
+ curQuery +=
896
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
897
+ 'EXISTS (SELECT "guid" FROM ' +
898
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
899
+ ' WHERE "guid"=' +
900
+ ieTable +
901
+ '."guid" AND "name"=@' +
902
+ name +
903
+ ' AND "json" @> @' +
904
+ value +
905
+ ')';
906
+ params[name] = curValue[0];
907
+ params[value] = PostgreSQLDriver.escapeNullSequences(svalue);
908
+ }
909
+ break;
910
+ case 'search':
911
+ case '!search':
912
+ if (curValue[0] === 'cdate' || curValue[0] === 'mdate') {
913
+ if (curQuery) {
914
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
748
915
  }
749
- else {
750
- curQuery +=
751
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
752
- ieTable +
753
- '."guid" IN (SELECT "guid" FROM ' +
754
- PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
755
- ' WHERE "name"=@' +
756
- name +
757
- ' AND position(@' +
758
- value +
759
- ' IN "value")>0)';
916
+ curQuery +=
917
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(FALSE)';
918
+ break;
919
+ }
920
+ else {
921
+ if (curQuery) {
922
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
760
923
  }
924
+ const name = `param${++count.i}`;
925
+ const queryPartToken = (term) => {
926
+ const value = `param${++count.i}`;
927
+ params[value] = term.token;
928
+ return ('EXISTS (SELECT "guid" FROM ' +
929
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
930
+ ' WHERE "guid"=' +
931
+ ieTable +
932
+ '."guid" AND "name"=@' +
933
+ name +
934
+ ' AND "token"=@' +
935
+ value +
936
+ (term.nostemmed ? ' AND "stem"=FALSE' : '') +
937
+ ')');
938
+ };
939
+ const queryPartSeries = (series) => {
940
+ const tokenTableSuffix = makeTableSuffix();
941
+ const tokenParts = series.tokens.map((token, i) => {
942
+ const value = `param${++count.i}`;
943
+ params[value] = token.token;
944
+ return {
945
+ fromClause: i === 0
946
+ ? 'FROM ' +
947
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
948
+ ' t' +
949
+ tokenTableSuffix +
950
+ '0'
951
+ : 'JOIN ' +
952
+ PostgreSQLDriver.escape(this.prefix + 'tokens_' + etype) +
953
+ ' t' +
954
+ tokenTableSuffix +
955
+ i +
956
+ ' ON t' +
957
+ tokenTableSuffix +
958
+ i +
959
+ '."guid" = t' +
960
+ tokenTableSuffix +
961
+ '0."guid" AND t' +
962
+ tokenTableSuffix +
963
+ i +
964
+ '."name" = t' +
965
+ tokenTableSuffix +
966
+ '0."name" AND t' +
967
+ tokenTableSuffix +
968
+ i +
969
+ '."position" = t' +
970
+ tokenTableSuffix +
971
+ '0."position" + ' +
972
+ i,
973
+ whereClause: 't' +
974
+ tokenTableSuffix +
975
+ i +
976
+ '."token"=@' +
977
+ value +
978
+ (token.nostemmed
979
+ ? ' AND t' + tokenTableSuffix + i + '."stem"=FALSE'
980
+ : ''),
981
+ };
982
+ });
983
+ return ('EXISTS (SELECT t' +
984
+ tokenTableSuffix +
985
+ '0."guid" ' +
986
+ tokenParts.map((part) => part.fromClause).join(' ') +
987
+ ' WHERE t' +
988
+ tokenTableSuffix +
989
+ '0."guid"=' +
990
+ ieTable +
991
+ '."guid" AND t' +
992
+ tokenTableSuffix +
993
+ '0."name"=@' +
994
+ name +
995
+ ' AND ' +
996
+ tokenParts.map((part) => part.whereClause).join(' AND ') +
997
+ ')');
998
+ };
999
+ const queryPartTerm = (term) => {
1000
+ if (term.type === 'series') {
1001
+ return queryPartSeries(term);
1002
+ }
1003
+ else if (term.type === 'not') {
1004
+ return 'NOT ' + queryPartTerm(term.operand);
1005
+ }
1006
+ else if (term.type === 'or') {
1007
+ let queryParts = [];
1008
+ for (let operand of term.operands) {
1009
+ queryParts.push(queryPartTerm(operand));
1010
+ }
1011
+ return '(' + queryParts.join(' OR ') + ')';
1012
+ }
1013
+ return queryPartToken(term);
1014
+ };
1015
+ const parsedFTSQuery = this.tokenizer.parseSearchQuery(curValue[1]);
1016
+ // Run through the query and add terms.
1017
+ let termStrings = [];
1018
+ for (let term of parsedFTSQuery) {
1019
+ termStrings.push(queryPartTerm(term));
1020
+ }
1021
+ curQuery +=
1022
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1023
+ '(' +
1024
+ termStrings.join(' AND ') +
1025
+ ')';
761
1026
  params[name] = curValue[0];
762
- params[value] = svalue;
763
1027
  }
764
1028
  break;
765
1029
  case 'match':
@@ -770,7 +1034,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
770
1034
  }
771
1035
  const cdate = `param${++count.i}`;
772
1036
  curQuery +=
773
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1037
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
774
1038
  '(' +
775
1039
  ieTable +
776
1040
  '."cdate" ~ @' +
@@ -785,7 +1049,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
785
1049
  }
786
1050
  const mdate = `param${++count.i}`;
787
1051
  curQuery +=
788
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1052
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
789
1053
  '(' +
790
1054
  ieTable +
791
1055
  '."mdate" ~ @' +
@@ -801,17 +1065,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
801
1065
  const name = `param${++count.i}`;
802
1066
  const value = `param${++count.i}`;
803
1067
  curQuery +=
804
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1068
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1069
+ 'EXISTS (SELECT "guid" FROM ' +
1070
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1071
+ ' WHERE "guid"=' +
805
1072
  ieTable +
806
- '."guid" IN (SELECT "guid" FROM ' +
807
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
808
- ' WHERE "name"=@' +
1073
+ '."guid" AND "name"=@' +
809
1074
  name +
810
1075
  ' AND "string" ~ @' +
811
1076
  value +
812
1077
  ')';
813
1078
  params[name] = curValue[0];
814
- params[value] = curValue[1];
1079
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
815
1080
  }
816
1081
  break;
817
1082
  case 'imatch':
@@ -822,7 +1087,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
822
1087
  }
823
1088
  const cdate = `param${++count.i}`;
824
1089
  curQuery +=
825
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1090
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
826
1091
  '(' +
827
1092
  ieTable +
828
1093
  '."cdate" ~* @' +
@@ -837,7 +1102,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
837
1102
  }
838
1103
  const mdate = `param${++count.i}`;
839
1104
  curQuery +=
840
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1105
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
841
1106
  '(' +
842
1107
  ieTable +
843
1108
  '."mdate" ~* @' +
@@ -853,17 +1118,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
853
1118
  const name = `param${++count.i}`;
854
1119
  const value = `param${++count.i}`;
855
1120
  curQuery +=
856
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1121
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1122
+ 'EXISTS (SELECT "guid" FROM ' +
1123
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1124
+ ' WHERE "guid"=' +
857
1125
  ieTable +
858
- '."guid" IN (SELECT "guid" FROM ' +
859
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
860
- ' WHERE "name"=@' +
1126
+ '."guid" AND "name"=@' +
861
1127
  name +
862
1128
  ' AND "string" ~* @' +
863
1129
  value +
864
1130
  ')';
865
1131
  params[name] = curValue[0];
866
- params[value] = curValue[1];
1132
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
867
1133
  }
868
1134
  break;
869
1135
  case 'like':
@@ -874,7 +1140,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
874
1140
  }
875
1141
  const cdate = `param${++count.i}`;
876
1142
  curQuery +=
877
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1143
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
878
1144
  '(' +
879
1145
  ieTable +
880
1146
  '."cdate" LIKE @' +
@@ -889,7 +1155,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
889
1155
  }
890
1156
  const mdate = `param${++count.i}`;
891
1157
  curQuery +=
892
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1158
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
893
1159
  '(' +
894
1160
  ieTable +
895
1161
  '."mdate" LIKE @' +
@@ -905,17 +1171,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
905
1171
  const name = `param${++count.i}`;
906
1172
  const value = `param${++count.i}`;
907
1173
  curQuery +=
908
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1174
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1175
+ 'EXISTS (SELECT "guid" FROM ' +
1176
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1177
+ ' WHERE "guid"=' +
909
1178
  ieTable +
910
- '."guid" IN (SELECT "guid" FROM ' +
911
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
912
- ' WHERE "name"=@' +
1179
+ '."guid" AND "name"=@' +
913
1180
  name +
914
1181
  ' AND "string" LIKE @' +
915
1182
  value +
916
1183
  ')';
917
1184
  params[name] = curValue[0];
918
- params[value] = curValue[1];
1185
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
919
1186
  }
920
1187
  break;
921
1188
  case 'ilike':
@@ -926,7 +1193,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
926
1193
  }
927
1194
  const cdate = `param${++count.i}`;
928
1195
  curQuery +=
929
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1196
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
930
1197
  '(' +
931
1198
  ieTable +
932
1199
  '."cdate" ILIKE @' +
@@ -941,7 +1208,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
941
1208
  }
942
1209
  const mdate = `param${++count.i}`;
943
1210
  curQuery +=
944
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1211
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
945
1212
  '(' +
946
1213
  ieTable +
947
1214
  '."mdate" ILIKE @' +
@@ -957,17 +1224,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
957
1224
  const name = `param${++count.i}`;
958
1225
  const value = `param${++count.i}`;
959
1226
  curQuery +=
960
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1227
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1228
+ 'EXISTS (SELECT "guid" FROM ' +
1229
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1230
+ ' WHERE "guid"=' +
961
1231
  ieTable +
962
- '."guid" IN (SELECT "guid" FROM ' +
963
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
964
- ' WHERE "name"=@' +
1232
+ '."guid" AND "name"=@' +
965
1233
  name +
966
1234
  ' AND "string" ILIKE @' +
967
1235
  value +
968
1236
  ')';
969
1237
  params[name] = curValue[0];
970
- params[value] = curValue[1];
1238
+ params[value] = PostgreSQLDriver.escapeNulls(curValue[1]);
971
1239
  }
972
1240
  break;
973
1241
  case 'gt':
@@ -978,7 +1246,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
978
1246
  }
979
1247
  const cdate = `param${++count.i}`;
980
1248
  curQuery +=
981
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1249
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
982
1250
  ieTable +
983
1251
  '."cdate">@' +
984
1252
  cdate;
@@ -993,7 +1261,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
993
1261
  }
994
1262
  const mdate = `param${++count.i}`;
995
1263
  curQuery +=
996
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1264
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
997
1265
  ieTable +
998
1266
  '."mdate">@' +
999
1267
  mdate;
@@ -1009,11 +1277,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1009
1277
  const name = `param${++count.i}`;
1010
1278
  const value = `param${++count.i}`;
1011
1279
  curQuery +=
1012
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1280
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1281
+ 'EXISTS (SELECT "guid" FROM ' +
1282
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1283
+ ' WHERE "guid"=' +
1013
1284
  ieTable +
1014
- '."guid" IN (SELECT "guid" FROM ' +
1015
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1016
- ' WHERE "name"=@' +
1285
+ '."guid" AND "name"=@' +
1017
1286
  name +
1018
1287
  ' AND "number">@' +
1019
1288
  value +
@@ -1032,7 +1301,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1032
1301
  }
1033
1302
  const cdate = `param${++count.i}`;
1034
1303
  curQuery +=
1035
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1304
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1036
1305
  ieTable +
1037
1306
  '."cdate">=@' +
1038
1307
  cdate;
@@ -1047,7 +1316,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1047
1316
  }
1048
1317
  const mdate = `param${++count.i}`;
1049
1318
  curQuery +=
1050
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1319
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1051
1320
  ieTable +
1052
1321
  '."mdate">=@' +
1053
1322
  mdate;
@@ -1063,11 +1332,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1063
1332
  const name = `param${++count.i}`;
1064
1333
  const value = `param${++count.i}`;
1065
1334
  curQuery +=
1066
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1335
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1336
+ 'EXISTS (SELECT "guid" FROM ' +
1337
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1338
+ ' WHERE "guid"=' +
1067
1339
  ieTable +
1068
- '."guid" IN (SELECT "guid" FROM ' +
1069
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1070
- ' WHERE "name"=@' +
1340
+ '."guid" AND "name"=@' +
1071
1341
  name +
1072
1342
  ' AND "number">=@' +
1073
1343
  value +
@@ -1086,7 +1356,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1086
1356
  }
1087
1357
  const cdate = `param${++count.i}`;
1088
1358
  curQuery +=
1089
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1359
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1090
1360
  ieTable +
1091
1361
  '."cdate"<@' +
1092
1362
  cdate;
@@ -1101,7 +1371,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1101
1371
  }
1102
1372
  const mdate = `param${++count.i}`;
1103
1373
  curQuery +=
1104
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1374
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1105
1375
  ieTable +
1106
1376
  '."mdate"<@' +
1107
1377
  mdate;
@@ -1117,11 +1387,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1117
1387
  const name = `param${++count.i}`;
1118
1388
  const value = `param${++count.i}`;
1119
1389
  curQuery +=
1120
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1390
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1391
+ 'EXISTS (SELECT "guid" FROM ' +
1392
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1393
+ ' WHERE "guid"=' +
1121
1394
  ieTable +
1122
- '."guid" IN (SELECT "guid" FROM ' +
1123
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1124
- ' WHERE "name"=@' +
1395
+ '."guid" AND "name"=@' +
1125
1396
  name +
1126
1397
  ' AND "number"<@' +
1127
1398
  value +
@@ -1140,7 +1411,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1140
1411
  }
1141
1412
  const cdate = `param${++count.i}`;
1142
1413
  curQuery +=
1143
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1414
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1144
1415
  ieTable +
1145
1416
  '."cdate"<=@' +
1146
1417
  cdate;
@@ -1155,7 +1426,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1155
1426
  }
1156
1427
  const mdate = `param${++count.i}`;
1157
1428
  curQuery +=
1158
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1429
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1159
1430
  ieTable +
1160
1431
  '."mdate"<=@' +
1161
1432
  mdate;
@@ -1171,11 +1442,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1171
1442
  const name = `param${++count.i}`;
1172
1443
  const value = `param${++count.i}`;
1173
1444
  curQuery +=
1174
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1445
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1446
+ 'EXISTS (SELECT "guid" FROM ' +
1447
+ PostgreSQLDriver.escape(this.prefix + 'data_' + etype) +
1448
+ ' WHERE "guid"=' +
1175
1449
  ieTable +
1176
- '."guid" IN (SELECT "guid" FROM ' +
1177
- PostgreSQLDriver.escape(this.prefix + 'comparisons_' + etype) +
1178
- ' WHERE "name"=@' +
1450
+ '."guid" AND "name"=@' +
1179
1451
  name +
1180
1452
  ' AND "number"<=@' +
1181
1453
  value +
@@ -1204,11 +1476,12 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1204
1476
  const name = `param${++count.i}`;
1205
1477
  const guid = `param${++count.i}`;
1206
1478
  curQuery +=
1207
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1208
- ieTable +
1209
- '."guid" IN (SELECT "guid" FROM ' +
1479
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1480
+ 'EXISTS (SELECT "guid" FROM ' +
1210
1481
  PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1211
- ' WHERE "name"=@' +
1482
+ ' WHERE "guid"=' +
1483
+ ieTable +
1484
+ '."guid" AND "name"=@' +
1212
1485
  name +
1213
1486
  ' AND "reference"=decode(@' +
1214
1487
  guid +
@@ -1223,29 +1496,37 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1223
1496
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1224
1497
  }
1225
1498
  curQuery +=
1226
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1499
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1227
1500
  '(' +
1228
1501
  subquery.query +
1229
1502
  ')';
1230
1503
  break;
1231
1504
  case 'qref':
1232
1505
  case '!qref':
1506
+ const referenceTableSuffix = makeTableSuffix();
1233
1507
  const [qrefOptions, ...qrefSelectors] = curValue[1];
1234
1508
  const QrefEntityClass = qrefOptions.class;
1235
1509
  etypes.push(QrefEntityClass.ETYPE);
1236
- const qrefQuery = this.makeEntityQuery({ ...qrefOptions, return: 'guid', class: QrefEntityClass }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, (0, guid_1.makeTableSuffix)(), etypes);
1510
+ const qrefQuery = this.makeEntityQuery({ ...qrefOptions, return: 'guid', class: QrefEntityClass }, qrefSelectors, QrefEntityClass.ETYPE, count, params, false, makeTableSuffix(), etypes, 'r' + referenceTableSuffix + '."reference"');
1237
1511
  if (curQuery) {
1238
1512
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1239
1513
  }
1240
1514
  const qrefName = `param${++count.i}`;
1241
1515
  curQuery +=
1242
- ((0, nymph_1.xor)(typeIsNot, clauseNot) ? 'NOT ' : '') +
1243
- ieTable +
1244
- '."guid" IN (SELECT "guid" FROM ' +
1516
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1517
+ 'EXISTS (SELECT "guid" FROM ' +
1245
1518
  PostgreSQLDriver.escape(this.prefix + 'references_' + etype) +
1246
- ' WHERE "name"=@' +
1519
+ ' r' +
1520
+ referenceTableSuffix +
1521
+ ' WHERE r' +
1522
+ referenceTableSuffix +
1523
+ '."guid"=' +
1524
+ ieTable +
1525
+ '."guid" AND r' +
1526
+ referenceTableSuffix +
1527
+ '."name"=@' +
1247
1528
  qrefName +
1248
- ' AND "reference" IN (' +
1529
+ ' AND EXISTS (' +
1249
1530
  qrefQuery.query +
1250
1531
  '))';
1251
1532
  params[qrefName] = curValue[0];
@@ -1255,18 +1536,38 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1255
1536
  return curQuery;
1256
1537
  });
1257
1538
  let sortBy;
1539
+ let sortByInner;
1540
+ let sortJoin = '';
1541
+ let sortJoinInner = '';
1542
+ const order = options.reverse ? ' DESC' : '';
1258
1543
  switch (sort) {
1259
1544
  case 'mdate':
1260
- sortBy = '"mdate"';
1545
+ sortBy = `${eTable}."mdate"${order}`;
1546
+ sortByInner = `${ieTable}."mdate"${order}`;
1261
1547
  break;
1262
1548
  case 'cdate':
1549
+ sortBy = `${eTable}."cdate"${order}`;
1550
+ sortByInner = `${ieTable}."cdate"${order}`;
1551
+ break;
1263
1552
  default:
1264
- sortBy = '"cdate"';
1553
+ const name = `param${++count.i}`;
1554
+ sortJoin = `LEFT JOIN (
1555
+ SELECT "guid", "string", "number"
1556
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1557
+ WHERE "name"=@${name}
1558
+ ORDER BY "number"${order}, "string"${order}
1559
+ ) ${sTable} ON ${eTable}."guid"=${sTable}."guid"`;
1560
+ sortJoinInner = `LEFT JOIN (
1561
+ SELECT "guid", "string", "number"
1562
+ FROM ${PostgreSQLDriver.escape(this.prefix + 'data_' + etype)}
1563
+ WHERE "name"=@${name}
1564
+ ORDER BY "number"${order}, "string"${order}
1565
+ ) ${sTable} ON ${ieTable}."guid"=${sTable}."guid"`;
1566
+ sortBy = `${sTable}."number"${order}, ${sTable}."string"${order}`;
1567
+ sortByInner = sortBy;
1568
+ params[name] = sort;
1265
1569
  break;
1266
1570
  }
1267
- if (options.reverse) {
1268
- sortBy += ' DESC';
1269
- }
1270
1571
  let query;
1271
1572
  if (queryParts.length) {
1272
1573
  if (subquery) {
@@ -1282,18 +1583,21 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1282
1583
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1283
1584
  }
1284
1585
  const whereClause = queryParts.join(') AND (');
1586
+ const guidClause = guidSelector
1587
+ ? `${ieTable}."guid"=${guidSelector} AND `
1588
+ : '';
1285
1589
  if (options.return === 'count') {
1286
1590
  if (limit || offset) {
1287
1591
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1288
- SELECT COUNT(${ieTable}."guid") AS "guid"
1592
+ SELECT ${ieTable}."guid" AS "guid"
1289
1593
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1290
- WHERE (${whereClause})${limit}${offset}
1594
+ WHERE ${guidClause}(${whereClause})${limit}${offset}
1291
1595
  ) ${countTable}`;
1292
1596
  }
1293
1597
  else {
1294
1598
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1295
1599
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1296
- WHERE (${whereClause})`;
1600
+ WHERE ${guidClause}(${whereClause})`;
1297
1601
  }
1298
1602
  }
1299
1603
  else if (options.return === 'guid') {
@@ -1302,8 +1606,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1302
1606
  : `${ieTable}."guid"`;
1303
1607
  query = `SELECT ${guidColumn} AS "guid"
1304
1608
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1305
- WHERE (${whereClause})
1306
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1609
+ ${sortJoinInner}
1610
+ WHERE ${guidClause}(${whereClause})
1611
+ ORDER BY ${sortByInner}, ${ieTable}."guid"${limit}${offset}`;
1307
1612
  }
1308
1613
  else {
1309
1614
  query = `SELECT
@@ -1313,18 +1618,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1313
1618
  ${eTable}."mdate",
1314
1619
  ${dTable}."name",
1315
1620
  ${dTable}."value",
1316
- ${cTable}."string",
1317
- ${cTable}."number"
1621
+ ${dTable}."json",
1622
+ ${dTable}."string",
1623
+ ${dTable}."number"
1318
1624
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1319
1625
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1320
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1626
+ ${sortJoin}
1321
1627
  INNER JOIN (
1322
1628
  SELECT ${ieTable}."guid"
1323
1629
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1324
- WHERE (${whereClause})
1325
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1630
+ ${sortJoinInner}
1631
+ WHERE ${guidClause}(${whereClause})
1632
+ ORDER BY ${sortByInner}${limit}${offset}
1326
1633
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1327
- ORDER BY ${eTable}.${sortBy}`;
1634
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1328
1635
  }
1329
1636
  }
1330
1637
  }
@@ -1341,16 +1648,19 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1341
1648
  if ('offset' in options) {
1342
1649
  offset = ` OFFSET ${Math.floor(isNaN(Number(options.offset)) ? 0 : Number(options.offset))}`;
1343
1650
  }
1651
+ const guidClause = guidSelector
1652
+ ? ` WHERE ${ieTable}."guid"=${guidSelector}`
1653
+ : '';
1344
1654
  if (options.return === 'count') {
1345
1655
  if (limit || offset) {
1346
1656
  query = `SELECT COUNT(${countTable}."guid") AS "count" FROM (
1347
- SELECT COUNT(${ieTable}."guid") AS "guid"
1348
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${limit}${offset}
1657
+ SELECT ${ieTable}."guid" AS "guid"
1658
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}${limit}${offset}
1349
1659
  ) ${countTable}`;
1350
1660
  }
1351
1661
  else {
1352
1662
  query = `SELECT COUNT(${ieTable}."guid") AS "count"
1353
- FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}`;
1663
+ FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}${guidClause}`;
1354
1664
  }
1355
1665
  }
1356
1666
  else if (options.return === 'guid') {
@@ -1359,7 +1669,9 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1359
1669
  : `${ieTable}."guid"`;
1360
1670
  query = `SELECT ${guidColumn} AS "guid"
1361
1671
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1362
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}`;
1672
+ ${sortJoinInner}
1673
+ ${guidClause}
1674
+ ORDER BY ${sortByInner}, ${ieTable}."guid"${limit}${offset}`;
1363
1675
  }
1364
1676
  else {
1365
1677
  if (limit || offset) {
@@ -1370,17 +1682,20 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1370
1682
  ${eTable}."mdate",
1371
1683
  ${dTable}."name",
1372
1684
  ${dTable}."value",
1373
- ${cTable}."string",
1374
- ${cTable}."number"
1685
+ ${dTable}."json",
1686
+ ${dTable}."string",
1687
+ ${dTable}."number"
1375
1688
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1376
1689
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1377
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1690
+ ${sortJoin}
1378
1691
  INNER JOIN (
1379
1692
  SELECT ${ieTable}."guid"
1380
1693
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${ieTable}
1381
- ORDER BY ${ieTable}.${sortBy}${limit}${offset}
1694
+ ${sortJoinInner}
1695
+ ${guidClause}
1696
+ ORDER BY ${sortByInner}${limit}${offset}
1382
1697
  ) ${fTable} ON ${eTable}."guid"=${fTable}."guid"
1383
- ORDER BY ${eTable}.${sortBy}`;
1698
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1384
1699
  }
1385
1700
  else {
1386
1701
  query = `SELECT
@@ -1390,12 +1705,14 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1390
1705
  ${eTable}."mdate",
1391
1706
  ${dTable}."name",
1392
1707
  ${dTable}."value",
1393
- ${cTable}."string",
1394
- ${cTable}."number"
1708
+ ${dTable}."json",
1709
+ ${dTable}."string",
1710
+ ${dTable}."number"
1395
1711
  FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ${eTable}
1396
1712
  LEFT JOIN ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ${dTable} ON ${eTable}."guid"=${dTable}."guid"
1397
- INNER JOIN ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} ${cTable} ON ${dTable}."guid"=${cTable}."guid" AND ${dTable}."name"=${cTable}."name"
1398
- ORDER BY ${eTable}.${sortBy}`;
1713
+ ${sortJoin}
1714
+ ${guidSelector ? `WHERE ${eTable}."guid"=${guidSelector}` : ''}
1715
+ ORDER BY ${sortBy}, ${eTable}."guid"`;
1399
1716
  }
1400
1717
  }
1401
1718
  }
@@ -1411,58 +1728,41 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1411
1728
  }
1412
1729
  performQuery(options, formattedSelectors, etype) {
1413
1730
  const { query, params, etypes } = this.makeEntityQuery(options, formattedSelectors, etype);
1414
- const result = this.queryIter(query, { etypes, params }).then((val) => val[Symbol.iterator]());
1415
- return {
1416
- result,
1417
- };
1418
- }
1419
- performQuerySync(options, formattedSelectors, etype) {
1420
- const { query, params, etypes } = this.makeEntityQuery(options, formattedSelectors, etype);
1421
- const result = (this.queryIterSync(query, { etypes, params }) || [])[Symbol.iterator]();
1731
+ const result = this.queryArray(query, { etypes, params }).then((val) => val[Symbol.iterator]());
1422
1732
  return {
1423
1733
  result,
1424
1734
  };
1425
1735
  }
1426
1736
  async getEntities(options = {}, ...selectors) {
1427
- const { result: resultPromise, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuery(options, formattedSelectors, etype), () => {
1737
+ const { result: resultPromise, process } = this.getEntitiesRowLike(
1738
+ // @ts-ignore: options is correct here.
1739
+ options, selectors, ({ options, selectors, etype }) => this.performQuery(options, selectors, etype), () => {
1428
1740
  const next = result.next();
1429
1741
  return next.done ? null : next.value;
1430
1742
  }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
1431
- tags: row.tags,
1432
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1433
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1743
+ tags: row.tags.filter((tag) => tag),
1744
+ cdate: isNaN(Number(row.cdate)) ? Date.now() : Number(row.cdate),
1745
+ mdate: isNaN(Number(row.mdate)) ? Date.now() : Number(row.mdate),
1434
1746
  }), (row) => ({
1435
1747
  name: row.name,
1436
1748
  svalue: row.value === 'N'
1437
1749
  ? JSON.stringify(Number(row.number))
1438
1750
  : row.value === 'S'
1439
- ? JSON.stringify(row.string)
1440
- : row.value,
1751
+ ? JSON.stringify(PostgreSQLDriver.unescapeNulls(row.string))
1752
+ : row.value === 'J'
1753
+ ? PostgreSQLDriver.unescapeNullSequences(JSON.stringify(row.json))
1754
+ : row.value,
1441
1755
  }));
1442
1756
  const result = await resultPromise;
1443
- return process();
1444
- }
1445
- getEntitiesSync(options = {}, ...selectors) {
1446
- const { result, process } = this.getEntitesRowLike(options, selectors, (options, formattedSelectors, etype) => this.performQuerySync(options, formattedSelectors, etype), () => {
1447
- const next = result.next();
1448
- return next.done ? null : next.value;
1449
- }, () => undefined, (row) => Number(row.count), (row) => row.guid, (row) => ({
1450
- tags: row.tags,
1451
- cdate: isNaN(Number(row.cdate)) ? null : Number(row.cdate),
1452
- mdate: isNaN(Number(row.mdate)) ? null : Number(row.mdate),
1453
- }), (row) => ({
1454
- name: row.name,
1455
- svalue: row.value === 'N'
1456
- ? JSON.stringify(Number(row.number))
1457
- : row.value === 'S'
1458
- ? JSON.stringify(row.string)
1459
- : row.value,
1460
- }));
1461
- return process();
1757
+ const value = process();
1758
+ if (value instanceof Error) {
1759
+ throw value;
1760
+ }
1761
+ return value;
1462
1762
  }
1463
1763
  async getUID(name) {
1464
1764
  if (name == null) {
1465
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
1765
+ throw new InvalidParametersError('Name not given for UID.');
1466
1766
  }
1467
1767
  const result = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1468
1768
  params: {
@@ -1471,50 +1771,61 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1471
1771
  });
1472
1772
  return result?.cur_uid == null ? null : Number(result.cur_uid);
1473
1773
  }
1474
- async import(filename) {
1774
+ async importEntity(entity) {
1775
+ return await this.importEntityInternal(entity, false);
1776
+ }
1777
+ async importEntityTokens(entity) {
1778
+ return await this.importEntityInternal(entity, true);
1779
+ }
1780
+ async importEntityInternal({ guid, cdate, mdate, tags, sdata, etype, }, onlyTokens) {
1475
1781
  try {
1476
- const result = await this.importFromFile(filename, async (guid, tags, sdata, etype) => {
1477
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1782
+ let promises = [];
1783
+ if (!onlyTokens) {
1784
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1478
1785
  etypes: [etype],
1479
1786
  params: {
1480
1787
  guid,
1481
1788
  },
1482
- });
1483
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`, {
1789
+ }));
1790
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1484
1791
  etypes: [etype],
1485
1792
  params: {
1486
1793
  guid,
1487
- tags,
1488
- cdate: isNaN(Number(JSON.parse(sdata.cdate)))
1489
- ? null
1490
- : Number(JSON.parse(sdata.cdate)),
1491
- mdate: isNaN(Number(JSON.parse(sdata.mdate)))
1492
- ? null
1493
- : Number(JSON.parse(sdata.mdate)),
1494
1794
  },
1495
- });
1496
- const promises = [];
1497
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1795
+ }));
1796
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1498
1797
  etypes: [etype],
1499
1798
  params: {
1500
1799
  guid,
1501
1800
  },
1502
1801
  }));
1503
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1802
+ }
1803
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1804
+ etypes: [etype],
1805
+ params: {
1806
+ guid,
1807
+ },
1808
+ }));
1809
+ if (!onlyTokens) {
1810
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1504
1811
  etypes: [etype],
1505
1812
  params: {
1506
1813
  guid,
1507
1814
  },
1508
1815
  }));
1509
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1816
+ }
1817
+ await Promise.all(promises);
1818
+ promises = [];
1819
+ if (!onlyTokens) {
1820
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @mdate);`, {
1510
1821
  etypes: [etype],
1511
1822
  params: {
1512
1823
  guid,
1824
+ tags,
1825
+ cdate: isNaN(cdate) ? null : cdate,
1826
+ mdate: isNaN(mdate) ? null : mdate,
1513
1827
  },
1514
- }));
1515
- await Promise.all(promises);
1516
- delete sdata.cdate;
1517
- delete sdata.mdate;
1828
+ });
1518
1829
  for (const name in sdata) {
1519
1830
  const value = sdata[name];
1520
1831
  const uvalue = JSON.parse(value);
@@ -1525,24 +1836,22 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1525
1836
  ? 'N'
1526
1837
  : typeof uvalue === 'string'
1527
1838
  ? 'S'
1528
- : value;
1529
- const promises = [];
1530
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
1839
+ : 'J';
1840
+ const jsonValue = storageValue === 'J'
1841
+ ? PostgreSQLDriver.escapeNullSequences(value)
1842
+ : null;
1843
+ 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);`, {
1531
1844
  etypes: [etype],
1532
1845
  params: {
1533
1846
  guid,
1534
1847
  name,
1535
1848
  storageValue,
1536
- },
1537
- }));
1538
- 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);`, {
1539
- etypes: [etype],
1540
- params: {
1541
- guid,
1542
- name,
1543
- truthy: !!uvalue,
1544
- string: `${uvalue}`,
1849
+ jsonValue,
1850
+ string: storageValue === 'J'
1851
+ ? null
1852
+ : PostgreSQLDriver.escapeNulls(`${uvalue}`),
1545
1853
  number: isNaN(Number(uvalue)) ? null : Number(uvalue),
1854
+ truthy: !!uvalue,
1546
1855
  },
1547
1856
  }));
1548
1857
  const references = this.findReferences(value);
@@ -1557,43 +1866,111 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1557
1866
  }));
1558
1867
  }
1559
1868
  }
1560
- await Promise.all(promises);
1561
- }, async (name, curUid) => {
1562
- await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1563
- params: {
1564
- name,
1565
- },
1566
- });
1567
- await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
1568
- params: {
1569
- name,
1570
- curUid,
1571
- },
1869
+ }
1870
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
1871
+ for (let name in sdata) {
1872
+ let tokenString = null;
1873
+ try {
1874
+ tokenString = EntityClass.getFTSText(name, JSON.parse(sdata[name]));
1875
+ }
1876
+ catch (e) {
1877
+ // Ignore error.
1878
+ }
1879
+ if (tokenString != null) {
1880
+ const tokens = this.tokenizer.tokenize(tokenString);
1881
+ while (tokens.length) {
1882
+ const currentTokens = tokens.splice(0, 100);
1883
+ const params = {
1884
+ guid,
1885
+ name,
1886
+ };
1887
+ const values = [];
1888
+ for (let i = 0; i < currentTokens.length; i++) {
1889
+ const token = currentTokens[i];
1890
+ params['token' + i] = token.token;
1891
+ params['position' + i] = token.position;
1892
+ params['stem' + i] = token.stem;
1893
+ values.push("(decode(@guid, 'hex'), @name, @token" +
1894
+ i +
1895
+ ', @position' +
1896
+ i +
1897
+ ', @stem' +
1898
+ i +
1899
+ ')');
1900
+ }
1901
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`, {
1902
+ etypes: [etype],
1903
+ params,
1904
+ }));
1905
+ }
1906
+ }
1907
+ }
1908
+ if (!onlyTokens) {
1909
+ const uniques = await EntityClass.getUniques({
1910
+ guid,
1911
+ cdate,
1912
+ mdate,
1913
+ tags,
1914
+ data: {},
1915
+ sdata,
1572
1916
  });
1573
- }, async () => {
1574
- await this.internalTransaction('nymph-import');
1575
- }, async () => {
1576
- await this.commit('nymph-import');
1917
+ for (const unique of uniques) {
1918
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
1919
+ etypes: [etype],
1920
+ params: {
1921
+ guid,
1922
+ unique,
1923
+ },
1924
+ }).catch((e) => {
1925
+ if (e instanceof EntityUniqueConstraintError) {
1926
+ this.nymph.config.debugError('postgresql', `Import entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
1927
+ }
1928
+ return e;
1929
+ }));
1930
+ }
1931
+ }
1932
+ await Promise.all(promises);
1933
+ }
1934
+ catch (e) {
1935
+ this.nymph.config.debugError('postgresql', `Import entity error: "${e}"`);
1936
+ throw e;
1937
+ }
1938
+ }
1939
+ async importUID({ name, value }) {
1940
+ try {
1941
+ await this.internalTransaction(`nymph-import-uid-${name}`);
1942
+ await this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name;`, {
1943
+ params: {
1944
+ name,
1945
+ },
1577
1946
  });
1578
- return result;
1947
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @value);`, {
1948
+ params: {
1949
+ name,
1950
+ value,
1951
+ },
1952
+ });
1953
+ await this.commit(`nymph-import-uid-${name}`);
1579
1954
  }
1580
1955
  catch (e) {
1581
- await this.rollback('nymph-import');
1582
- return false;
1956
+ this.nymph.config.debugError('postgresql', `Import UID error: "${e}"`);
1957
+ await this.rollback(`nymph-import-uid-${name}`);
1958
+ throw e;
1583
1959
  }
1584
1960
  }
1585
1961
  async newUID(name) {
1586
1962
  if (name == null) {
1587
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
1963
+ throw new InvalidParametersError('Name not given for UID.');
1588
1964
  }
1589
1965
  await this.internalTransaction('nymph-newuid');
1966
+ let curUid = undefined;
1590
1967
  try {
1591
1968
  const lock = await this.queryGet(`SELECT "cur_uid" FROM ${PostgreSQLDriver.escape(`${this.prefix}uids`)} WHERE "name"=@name FOR UPDATE;`, {
1592
1969
  params: {
1593
1970
  name,
1594
1971
  },
1595
1972
  });
1596
- let curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
1973
+ curUid = lock?.cur_uid == null ? undefined : Number(lock.cur_uid);
1597
1974
  if (curUid == null) {
1598
1975
  curUid = 1;
1599
1976
  await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uids`)} ("name", "cur_uid") VALUES (@name, @curUid);`, {
@@ -1612,17 +1989,18 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1612
1989
  },
1613
1990
  });
1614
1991
  }
1615
- await this.commit('nymph-newuid');
1616
- return curUid;
1617
1992
  }
1618
1993
  catch (e) {
1994
+ this.nymph.config.debugError('postgresql', `New UID error: "${e}"`);
1619
1995
  await this.rollback('nymph-newuid');
1620
1996
  throw e;
1621
1997
  }
1998
+ await this.commit('nymph-newuid');
1999
+ return curUid;
1622
2000
  }
1623
2001
  async renameUID(oldName, newName) {
1624
2002
  if (oldName == null || newName == null) {
1625
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2003
+ throw new InvalidParametersError('Name not given for UID.');
1626
2004
  }
1627
2005
  await this.queryRun(`UPDATE ${PostgreSQLDriver.escape(`${this.prefix}uids`)} SET "name"=@newName WHERE "name"=@oldName;`, {
1628
2006
  params: {
@@ -1634,7 +2012,7 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1634
2012
  }
1635
2013
  async rollback(name) {
1636
2014
  if (name == null || typeof name !== 'string' || name.length === 0) {
1637
- throw new nymph_1.InvalidParametersError('Transaction rollback attempted without a name.');
2015
+ throw new InvalidParametersError('Transaction rollback attempted without a name.');
1638
2016
  }
1639
2017
  if (!this.transaction || this.transaction.count === 0) {
1640
2018
  this.transaction = null;
@@ -1651,7 +2029,8 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1651
2029
  return true;
1652
2030
  }
1653
2031
  async saveEntity(entity) {
1654
- const insertData = async (guid, data, sdata, etype) => {
2032
+ const insertData = async (guid, data, sdata, uniques, etype) => {
2033
+ const EntityClass = this.nymph.getEntityClassByEtype(etype);
1655
2034
  const runInsertQuery = async (name, value, svalue) => {
1656
2035
  if (value === undefined) {
1657
2036
  return;
@@ -1660,24 +2039,23 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1660
2039
  ? 'N'
1661
2040
  : typeof value === 'string'
1662
2041
  ? 'S'
1663
- : svalue;
2042
+ : 'J';
2043
+ const jsonValue = storageValue === 'J'
2044
+ ? PostgreSQLDriver.escapeNullSequences(svalue)
2045
+ : null;
1664
2046
  const promises = [];
1665
- promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}data_${etype}`)} ("guid", "name", "value") VALUES (decode(@guid, 'hex'), @name, @storageValue);`, {
2047
+ 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);`, {
1666
2048
  etypes: [etype],
1667
2049
  params: {
1668
2050
  guid,
1669
2051
  name,
1670
2052
  storageValue,
1671
- },
1672
- }));
1673
- 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);`, {
1674
- etypes: [etype],
1675
- params: {
1676
- guid,
1677
- name,
1678
- truthy: !!value,
1679
- string: `${value}`,
2053
+ jsonValue,
2054
+ string: storageValue === 'J'
2055
+ ? null
2056
+ : PostgreSQLDriver.escapeNulls(`${value}`),
1680
2057
  number: isNaN(Number(value)) ? null : Number(value),
2058
+ truthy: !!value,
1681
2059
  },
1682
2060
  }));
1683
2061
  const references = this.findReferences(svalue);
@@ -1691,8 +2069,60 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1691
2069
  },
1692
2070
  }));
1693
2071
  }
2072
+ let tokenString = null;
2073
+ try {
2074
+ tokenString = EntityClass.getFTSText(name, value);
2075
+ }
2076
+ catch (e) {
2077
+ // Ignore error.
2078
+ }
2079
+ if (tokenString != null) {
2080
+ const tokens = this.tokenizer.tokenize(tokenString);
2081
+ while (tokens.length) {
2082
+ const currentTokens = tokens.splice(0, 100);
2083
+ const params = {
2084
+ guid,
2085
+ name,
2086
+ };
2087
+ const values = [];
2088
+ for (let i = 0; i < currentTokens.length; i++) {
2089
+ const token = currentTokens[i];
2090
+ params['token' + i] = token.token;
2091
+ params['position' + i] = token.position;
2092
+ params['stem' + i] = token.stem;
2093
+ values.push("(decode(@guid, 'hex'), @name, @token" +
2094
+ i +
2095
+ ', @position' +
2096
+ i +
2097
+ ', @stem' +
2098
+ i +
2099
+ ')');
2100
+ }
2101
+ promises.push(this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} ("guid", "name", "token", "position", "stem") VALUES ${values.join(', ')};`, {
2102
+ etypes: [etype],
2103
+ params,
2104
+ }));
2105
+ }
2106
+ }
1694
2107
  await Promise.all(promises);
1695
2108
  };
2109
+ for (const unique of uniques) {
2110
+ try {
2111
+ await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} ("guid", "unique") VALUES (decode(@guid, 'hex'), @unique);`, {
2112
+ etypes: [etype],
2113
+ params: {
2114
+ guid,
2115
+ unique,
2116
+ },
2117
+ });
2118
+ }
2119
+ catch (e) {
2120
+ if (e instanceof EntityUniqueConstraintError) {
2121
+ this.nymph.config.debugError('postgresql', `Save entity unique constraint violation for GUID "${guid}" on etype "${etype}": "${unique}"`);
2122
+ }
2123
+ throw e;
2124
+ }
2125
+ }
1696
2126
  for (const name in data) {
1697
2127
  await runInsertQuery(name, data[name], JSON.stringify(data[name]));
1698
2128
  }
@@ -1700,8 +2130,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1700
2130
  await runInsertQuery(name, JSON.parse(sdata[name]), sdata[name]);
1701
2131
  }
1702
2132
  };
2133
+ let inTransaction = false;
1703
2134
  try {
1704
- const result = await this.saveEntityRowLike(entity, async (_entity, guid, tags, data, sdata, cdate, etype) => {
2135
+ const result = await this.saveEntityRowLike(entity, async ({ guid, tags, data, sdata, uniques, cdate, etype }) => {
2136
+ if (Object.keys(data).length === 0 &&
2137
+ Object.keys(sdata).length === 0) {
2138
+ return false;
2139
+ }
1705
2140
  await this.queryRun(`INSERT INTO ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} ("guid", "tags", "cdate", "mdate") VALUES (decode(@guid, 'hex'), @tags, @cdate, @cdate);`, {
1706
2141
  etypes: [etype],
1707
2142
  params: {
@@ -1710,9 +2145,13 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1710
2145
  cdate,
1711
2146
  },
1712
2147
  });
1713
- await insertData(guid, data, sdata, etype);
2148
+ await insertData(guid, data, sdata, uniques, etype);
1714
2149
  return true;
1715
- }, async (entity, guid, tags, data, sdata, mdate, etype) => {
2150
+ }, async ({ entity, guid, tags, data, sdata, uniques, mdate, etype }) => {
2151
+ if (Object.keys(data).length === 0 &&
2152
+ Object.keys(sdata).length === 0) {
2153
+ return false;
2154
+ }
1716
2155
  const promises = [];
1717
2156
  promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}entities_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1718
2157
  etypes: [etype],
@@ -1726,13 +2165,19 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1726
2165
  guid,
1727
2166
  },
1728
2167
  }));
1729
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2168
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1730
2169
  etypes: [etype],
1731
2170
  params: {
1732
2171
  guid,
1733
2172
  },
1734
2173
  }));
1735
- promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2174
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
2175
+ etypes: [etype],
2176
+ params: {
2177
+ guid,
2178
+ },
2179
+ }));
2180
+ promises.push(this.queryRun(`SELECT 1 FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex') FOR UPDATE;`, {
1736
2181
  etypes: [etype],
1737
2182
  params: {
1738
2183
  guid,
@@ -1757,44 +2202,57 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1757
2202
  guid,
1758
2203
  },
1759
2204
  }));
1760
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}comparisons_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2205
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1761
2206
  etypes: [etype],
1762
2207
  params: {
1763
2208
  guid,
1764
2209
  },
1765
2210
  }));
1766
- promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}references_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2211
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}tokens_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
2212
+ etypes: [etype],
2213
+ params: {
2214
+ guid,
2215
+ },
2216
+ }));
2217
+ promises.push(this.queryRun(`DELETE FROM ${PostgreSQLDriver.escape(`${this.prefix}uniques_${etype}`)} WHERE "guid"=decode(@guid, 'hex');`, {
1767
2218
  etypes: [etype],
1768
2219
  params: {
1769
2220
  guid,
1770
2221
  },
1771
2222
  }));
1772
2223
  await Promise.all(promises);
1773
- await insertData(guid, data, sdata, etype);
2224
+ await insertData(guid, data, sdata, uniques, etype);
1774
2225
  success = true;
1775
2226
  }
1776
2227
  return success;
1777
2228
  }, async () => {
1778
2229
  await this.internalTransaction('nymph-save');
2230
+ inTransaction = true;
1779
2231
  }, async (success) => {
1780
- if (success) {
1781
- await this.commit('nymph-save');
1782
- }
1783
- else {
1784
- await this.rollback('nymph-save');
2232
+ if (inTransaction) {
2233
+ inTransaction = false;
2234
+ if (success) {
2235
+ await this.commit('nymph-save');
2236
+ }
2237
+ else {
2238
+ await this.rollback('nymph-save');
2239
+ }
1785
2240
  }
1786
2241
  return success;
1787
2242
  });
1788
2243
  return result;
1789
2244
  }
1790
2245
  catch (e) {
1791
- await this.rollback('nymph-save');
2246
+ this.nymph.config.debugError('postgresql', `Save entity error: "${e}"`);
2247
+ if (inTransaction) {
2248
+ await this.rollback('nymph-save');
2249
+ }
1792
2250
  throw e;
1793
2251
  }
1794
2252
  }
1795
2253
  async setUID(name, curUid) {
1796
2254
  if (name == null) {
1797
- throw new nymph_1.InvalidParametersError('Name not given for UID.');
2255
+ throw new InvalidParametersError('Name not given for UID.');
1798
2256
  }
1799
2257
  await this.internalTransaction('nymph-setuid');
1800
2258
  try {
@@ -1810,23 +2268,25 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1810
2268
  curUid,
1811
2269
  },
1812
2270
  });
1813
- await this.commit('nymph-setuid');
1814
- return true;
1815
2271
  }
1816
2272
  catch (e) {
1817
2273
  await this.rollback('nymph-setuid');
1818
2274
  throw e;
1819
2275
  }
2276
+ await this.commit('nymph-setuid');
2277
+ return true;
1820
2278
  }
1821
2279
  async internalTransaction(name) {
1822
2280
  if (name == null || typeof name !== 'string' || name.length === 0) {
1823
- throw new nymph_1.InvalidParametersError('Transaction start attempted without a name.');
2281
+ throw new InvalidParametersError('Transaction start attempted without a name.');
1824
2282
  }
1825
2283
  if (!this.transaction || this.transaction.count === 0) {
2284
+ // Lock to one connection.
1826
2285
  this.transaction = {
1827
2286
  count: 0,
1828
2287
  connection: await this.getConnection(),
1829
2288
  };
2289
+ // We're not in a transaction yet, so start one.
1830
2290
  await this.queryRun('BEGIN;');
1831
2291
  }
1832
2292
  await this.queryRun(`SAVEPOINT ${PostgreSQLDriver.escape(name)};`);
@@ -1834,16 +2294,51 @@ class PostgreSQLDriver extends nymph_1.NymphDriver {
1834
2294
  return this.transaction;
1835
2295
  }
1836
2296
  async startTransaction(name) {
1837
- const inTransaction = this.inTransaction();
2297
+ const inTransaction = await this.inTransaction();
1838
2298
  const transaction = await this.internalTransaction(name);
1839
2299
  if (!inTransaction) {
1840
2300
  this.transaction = null;
1841
2301
  }
1842
2302
  const nymph = this.nymph.clone();
1843
- nymph.driver = new PostgreSQLDriver(this.config, this.link, transaction);
1844
- nymph.driver.init(nymph);
2303
+ nymph.driver.transaction = transaction;
1845
2304
  return nymph;
1846
2305
  }
2306
+ async needsMigration() {
2307
+ 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;', {
2308
+ params: {
2309
+ db: this.config.database,
2310
+ prefix: this.prefix + 'data_' + '%',
2311
+ },
2312
+ });
2313
+ if (table?.table_name) {
2314
+ 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\';', {
2315
+ params: {
2316
+ db: this.config.database,
2317
+ table: table.table_name,
2318
+ },
2319
+ });
2320
+ if (!result?.exists) {
2321
+ return 'json';
2322
+ }
2323
+ }
2324
+ const table2 = 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 @tokenTable LIMIT 1;', {
2325
+ params: {
2326
+ db: this.config.database,
2327
+ tokenTable: this.prefix + 'tokens_' + '%',
2328
+ },
2329
+ });
2330
+ if (!table2 || !table2.table_name) {
2331
+ return 'tokens';
2332
+ }
2333
+ return false;
2334
+ }
2335
+ async liveMigration(_migrationType) {
2336
+ const etypes = await this.getEtypes();
2337
+ const connection = await this.getConnection(true);
2338
+ for (let etype of etypes) {
2339
+ await this.createTokensTable(etype, connection);
2340
+ }
2341
+ connection.done();
2342
+ }
1847
2343
  }
1848
- exports.default = PostgreSQLDriver;
1849
2344
  //# sourceMappingURL=PostgreSQLDriver.js.map