@graffy/pg 0.19.1-alpha.1 → 0.19.1
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.
- package/index.cjs +996 -0
- package/index.mjs +996 -0
- package/package.json +9 -15
- package/{Db.d.ts → types/Db.d.ts} +1 -0
- package/types/index.d.ts +13 -0
- package/types/sql/clauses.d.ts +12 -0
- package/types/sql/format.d.ts +5 -0
- package/types/sql/getArgSql.d.ts +28 -0
- package/types/sql/getMeta.d.ts +12 -0
- package/types/sql/select.d.ts +2 -0
- package/types/sql/upsert.d.ts +3 -0
- package/Db.js +0 -236
- package/filter/filterObject.js +0 -39
- package/filter/getAst.js +0 -152
- package/filter/getSql.js +0 -100
- package/filter/index.d.ts +0 -2
- package/index.d.ts +0 -8
- package/index.js +0 -68
- package/sql/clauses.d.ts +0 -12
- package/sql/clauses.js +0 -224
- package/sql/format.d.ts +0 -5
- package/sql/format.js +0 -36
- package/sql/getArgSql.d.ts +0 -34
- package/sql/getArgSql.js +0 -82
- package/sql/getMeta.d.ts +0 -12
- package/sql/getMeta.js +0 -11
- package/sql/index.d.ts +0 -2
- package/sql/select.d.ts +0 -2
- package/sql/select.js +0 -41
- package/sql/upsert.d.ts +0 -3
- package/sql/upsert.js +0 -73
- /package/{filter → types/filter}/filterObject.d.ts +0 -0
- /package/{filter → types/filter}/getAst.d.ts +0 -0
- /package/{filter → types/filter}/getSql.d.ts +0 -0
- /package/{filter/index.js → types/filter/index.d.ts} +0 -0
- /package/{sql/index.js → types/sql/index.d.ts} +0 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
import { isEmpty, isPlainObject, encodePath, unwrap, decodeArgs, decodeQuery, finalize, wrap, isRange, cmp, decodeGraph, mergeObject, wrapObject, merge, encodeGraph, remove } from "@graffy/common";
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
import pg$1, { escapeLiteral } from "pg";
|
|
4
|
+
import { format } from "sql-formatter";
|
|
5
|
+
class Sql {
|
|
6
|
+
constructor(rawStrings, rawValues) {
|
|
7
|
+
if (rawStrings.length - 1 !== rawValues.length) {
|
|
8
|
+
if (rawStrings.length === 0) {
|
|
9
|
+
throw new TypeError("Expected at least 1 string");
|
|
10
|
+
}
|
|
11
|
+
throw new TypeError(`Expected ${rawStrings.length} strings to have ${rawStrings.length - 1} values`);
|
|
12
|
+
}
|
|
13
|
+
const valuesLength = rawValues.reduce((len, value) => len + (value instanceof Sql ? value.values.length : 1), 0);
|
|
14
|
+
this.values = new Array(valuesLength);
|
|
15
|
+
this.strings = new Array(valuesLength + 1);
|
|
16
|
+
this.strings[0] = rawStrings[0];
|
|
17
|
+
let i = 0, pos = 0;
|
|
18
|
+
while (i < rawValues.length) {
|
|
19
|
+
const child = rawValues[i++];
|
|
20
|
+
const rawString = rawStrings[i];
|
|
21
|
+
if (child instanceof Sql) {
|
|
22
|
+
this.strings[pos] += child.strings[0];
|
|
23
|
+
let childIndex = 0;
|
|
24
|
+
while (childIndex < child.values.length) {
|
|
25
|
+
this.values[pos++] = child.values[childIndex++];
|
|
26
|
+
this.strings[pos] = child.strings[childIndex];
|
|
27
|
+
}
|
|
28
|
+
this.strings[pos] += rawString;
|
|
29
|
+
} else {
|
|
30
|
+
this.values[pos++] = child;
|
|
31
|
+
this.strings[pos] = rawString;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
get sql() {
|
|
36
|
+
const len = this.strings.length;
|
|
37
|
+
let i = 1;
|
|
38
|
+
let value = this.strings[0];
|
|
39
|
+
while (i < len)
|
|
40
|
+
value += `?${this.strings[i++]}`;
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
get statement() {
|
|
44
|
+
const len = this.strings.length;
|
|
45
|
+
let i = 1;
|
|
46
|
+
let value = this.strings[0];
|
|
47
|
+
while (i < len)
|
|
48
|
+
value += `:${i}${this.strings[i++]}`;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
get text() {
|
|
52
|
+
const len = this.strings.length;
|
|
53
|
+
let i = 1;
|
|
54
|
+
let value = this.strings[0];
|
|
55
|
+
while (i < len)
|
|
56
|
+
value += `$${i}${this.strings[i++]}`;
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
inspect() {
|
|
60
|
+
return {
|
|
61
|
+
sql: this.sql,
|
|
62
|
+
statement: this.statement,
|
|
63
|
+
text: this.text,
|
|
64
|
+
values: this.values
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function join(values, separator = ",", prefix = "", suffix = "") {
|
|
69
|
+
if (values.length === 0) {
|
|
70
|
+
throw new TypeError("Expected `join([])` to be called with an array of multiple elements, but got an empty array");
|
|
71
|
+
}
|
|
72
|
+
return new Sql([prefix, ...Array(values.length - 1).fill(separator), suffix], values);
|
|
73
|
+
}
|
|
74
|
+
function raw(value) {
|
|
75
|
+
return new Sql([value], []);
|
|
76
|
+
}
|
|
77
|
+
const empty = raw("");
|
|
78
|
+
function sql(strings, ...values) {
|
|
79
|
+
return new Sql(strings, values);
|
|
80
|
+
}
|
|
81
|
+
function formatSqlParam(value) {
|
|
82
|
+
if (value === null || value === void 0) return "null";
|
|
83
|
+
if (["number", "boolean"].includes(typeof value)) return value.toString();
|
|
84
|
+
switch (typeof value) {
|
|
85
|
+
case "number":
|
|
86
|
+
case "boolean":
|
|
87
|
+
return value.toString();
|
|
88
|
+
case "string":
|
|
89
|
+
return escapeLiteral(value);
|
|
90
|
+
case "object":
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
return `array[${value.map(formatSqlParam).join(", ")}]`;
|
|
93
|
+
return `'${JSON.stringify(value)}'`;
|
|
94
|
+
default:
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function formatSql(sql2) {
|
|
99
|
+
return format(sql2.text, {
|
|
100
|
+
language: "postgresql",
|
|
101
|
+
params: Object.fromEntries(
|
|
102
|
+
sql2.values.map((value, idx) => [idx + 1, formatSqlParam(value)])
|
|
103
|
+
)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const getJsonBuildTrusted = (variadic) => {
|
|
107
|
+
const args = join(
|
|
108
|
+
Object.entries(variadic).map(([name, value]) => {
|
|
109
|
+
return sql`'${raw(name)}', ${getJsonBuildValue(value)}`;
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
return sql`jsonb_build_object(${args})`;
|
|
113
|
+
};
|
|
114
|
+
const getJsonBuildValue = (value) => {
|
|
115
|
+
if (value instanceof Sql) return value;
|
|
116
|
+
if (typeof value === "string") return sql`${value}::text`;
|
|
117
|
+
return sql`${JSON.stringify(stripAttributes(value))}::jsonb`;
|
|
118
|
+
};
|
|
119
|
+
function colName(prefix, options) {
|
|
120
|
+
if (!options.schema.types[prefix]) throw Error(`pg.no_column ${prefix}`);
|
|
121
|
+
return raw(prefix);
|
|
122
|
+
}
|
|
123
|
+
const lookup = (prop, options) => {
|
|
124
|
+
const [prefix, ...suffix] = prop.split(".");
|
|
125
|
+
if (!suffix.length) return sql`"${colName(prefix, options)}"`;
|
|
126
|
+
const { types: types2 } = options.schema;
|
|
127
|
+
if (types2[prefix] === "jsonb") {
|
|
128
|
+
return sql`"${raw(prefix)}" #> ${suffix}`;
|
|
129
|
+
}
|
|
130
|
+
if (types2[prefix] === "cube" && suffix.length === 1) {
|
|
131
|
+
return sql`"${raw(prefix)}" ~> ${Number.parseInt(suffix[0], 10)}`;
|
|
132
|
+
}
|
|
133
|
+
throw Error(`pg.cannot_lookup ${prop}`);
|
|
134
|
+
};
|
|
135
|
+
const lookupNumeric = (prop) => {
|
|
136
|
+
const [prefix, ...suffix] = prop.split(".");
|
|
137
|
+
return suffix.length ? sql`CASE WHEN "${raw(prefix)}" #> ${suffix} = 'null'::jsonb
|
|
138
|
+
THEN 0 ELSE ("${raw(prefix)}" #> ${suffix})::numeric END` : sql`"${raw(prefix)}"`;
|
|
139
|
+
};
|
|
140
|
+
const aggSql = {
|
|
141
|
+
$sum: (prop) => sql`sum((${lookupNumeric(prop)})::numeric)`,
|
|
142
|
+
$card: (prop, options) => sql`count(distinct(${lookup(prop, options)}))`,
|
|
143
|
+
$avg: (prop) => sql`avg((${lookupNumeric(prop)})::numeric)`,
|
|
144
|
+
$max: (prop) => sql`max((${lookupNumeric(prop)})::numeric)`,
|
|
145
|
+
$min: (prop) => sql`min((${lookupNumeric(prop)})::numeric)`
|
|
146
|
+
};
|
|
147
|
+
const getOptimisedJsonBuild = (object, path, parentPropertyName, options) => {
|
|
148
|
+
const propertyNames = Object.keys(object);
|
|
149
|
+
const propertyPath = [...path, parentPropertyName];
|
|
150
|
+
const buildConfig = [];
|
|
151
|
+
path = path || [];
|
|
152
|
+
propertyNames.forEach((propertyName) => {
|
|
153
|
+
const property = object[propertyName];
|
|
154
|
+
if (typeof property === "object") {
|
|
155
|
+
const childJSONBuild = getOptimisedJsonBuild(
|
|
156
|
+
property,
|
|
157
|
+
propertyPath,
|
|
158
|
+
propertyName,
|
|
159
|
+
options
|
|
160
|
+
);
|
|
161
|
+
buildConfig.push(
|
|
162
|
+
sql`${propertyName}::text, jsonb_build_object(${join(childJSONBuild, ", ")})`
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
if (property === true) {
|
|
166
|
+
buildConfig.push(
|
|
167
|
+
sql`${propertyName}::text, ${lookup([...propertyPath, propertyName].join("."), options)}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return buildConfig;
|
|
173
|
+
};
|
|
174
|
+
const getSelectCols = (options, projection = null) => {
|
|
175
|
+
if (!projection) return sql`*`;
|
|
176
|
+
const sqls = [];
|
|
177
|
+
for (const key in projection) {
|
|
178
|
+
if (key === "$count") {
|
|
179
|
+
sqls.push(sql`count(*) AS "$count"`);
|
|
180
|
+
} else if (aggSql[key]) {
|
|
181
|
+
const subSqls = [];
|
|
182
|
+
for (const prop in projection[key]) {
|
|
183
|
+
subSqls.push(sql`${prop}::text, ${aggSql[key](prop, options)}`);
|
|
184
|
+
}
|
|
185
|
+
sqls.push(
|
|
186
|
+
sql`jsonb_build_object(${join(subSqls, ", ")}) AS "${raw(key)}"`
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
if (typeof projection[key] === "object") {
|
|
190
|
+
const optimisedJsonBuild = getOptimisedJsonBuild(
|
|
191
|
+
projection[key],
|
|
192
|
+
[],
|
|
193
|
+
key,
|
|
194
|
+
options
|
|
195
|
+
);
|
|
196
|
+
sqls.push(
|
|
197
|
+
sql`jsonb_build_object(${join(optimisedJsonBuild, ", ")}) AS "${raw(key)}"`
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
sqls.push(sql`"${raw(key)}"`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return join(sqls, ", ");
|
|
205
|
+
};
|
|
206
|
+
function vertexSql(array, nullValue) {
|
|
207
|
+
return sql`array[${join(
|
|
208
|
+
array.map((num) => num === null ? nullValue : num)
|
|
209
|
+
)}]::float8[]`;
|
|
210
|
+
}
|
|
211
|
+
function cubeLiteralSql(value) {
|
|
212
|
+
if (!(Array.isArray(value) && value.length) || Array.isArray(value[0]) && value.length !== 2) {
|
|
213
|
+
throw Error(`pg.castValue_bad_cube${JSON.stringify(value)}`);
|
|
214
|
+
}
|
|
215
|
+
return Array.isArray(value[0]) ? sql`cube(${vertexSql(value[0], sql`'-Infinity'`)}, ${vertexSql(
|
|
216
|
+
value[1],
|
|
217
|
+
sql`'Infinity'`
|
|
218
|
+
)})` : sql`cube(${vertexSql(value, 0)})`;
|
|
219
|
+
}
|
|
220
|
+
function castValue(value, type, name, isPut) {
|
|
221
|
+
if (!type) throw Error(`pg.write_no_column ${name}`);
|
|
222
|
+
if (value instanceof Sql) return value;
|
|
223
|
+
if (value === null) return sql`NULL`;
|
|
224
|
+
if (type === "jsonb") {
|
|
225
|
+
return isPut ? JSON.stringify(stripAttributes(value)) : getJsonUpdate(value, name, [])[0];
|
|
226
|
+
}
|
|
227
|
+
if (type === "cube") return cubeLiteralSql(value);
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
const getInsert = (rows, options) => {
|
|
231
|
+
const { verCol, schema } = options;
|
|
232
|
+
const cols = [];
|
|
233
|
+
const colSqls = [];
|
|
234
|
+
const colIx = {};
|
|
235
|
+
const colUsed = [];
|
|
236
|
+
for (const col of Object.keys(options.schema.types)) {
|
|
237
|
+
colIx[col] = cols.length;
|
|
238
|
+
colUsed[cols.length] = false;
|
|
239
|
+
cols.push(col);
|
|
240
|
+
colSqls.push(sql`"${raw(col)}"`);
|
|
241
|
+
}
|
|
242
|
+
colUsed[colIx[verCol]] = true;
|
|
243
|
+
const vals = [];
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
const rowVals = Array(cols.length).fill(sql`default`);
|
|
246
|
+
for (const col of cols) {
|
|
247
|
+
if (col === verCol || !(col in row)) continue;
|
|
248
|
+
const ix = colIx[col];
|
|
249
|
+
colUsed[ix] = true;
|
|
250
|
+
rowVals[ix] = castValue(row[col], schema.types[col], col, row.$put);
|
|
251
|
+
}
|
|
252
|
+
vals.push(rowVals);
|
|
253
|
+
}
|
|
254
|
+
const isUsed = (_, ix) => colUsed[ix];
|
|
255
|
+
return {
|
|
256
|
+
cols: join(colSqls.filter(isUsed), ", "),
|
|
257
|
+
vals: join(
|
|
258
|
+
vals.map((rowVals) => sql`(${join(rowVals.filter(isUsed), ", ")})`),
|
|
259
|
+
", "
|
|
260
|
+
),
|
|
261
|
+
updates: join(
|
|
262
|
+
colSqls.map((col, _ix) => sql`${col} = "excluded".${col}`).filter(isUsed),
|
|
263
|
+
", "
|
|
264
|
+
)
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
const getUpdates = (row, options) => {
|
|
268
|
+
return join(
|
|
269
|
+
Object.entries(row).filter(([col]) => col !== options.idCol && col[0] !== "$").map(
|
|
270
|
+
([col, val]) => sql`"${raw(col)}" = ${castValue(
|
|
271
|
+
val,
|
|
272
|
+
options.schema.types[col],
|
|
273
|
+
col,
|
|
274
|
+
row.$put
|
|
275
|
+
)}`
|
|
276
|
+
).concat(sql`"${raw(options.verCol)}" = default`),
|
|
277
|
+
", "
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
function getJsonUpdate(object, col, path) {
|
|
281
|
+
if (!object || typeof object !== "object" || Array.isArray(object) || object.$put) {
|
|
282
|
+
const patch2 = stripAttributes(object);
|
|
283
|
+
return [sql`${JSON.stringify(patch2)}::jsonb`, patch2 === null];
|
|
284
|
+
}
|
|
285
|
+
if ("$val" in object) {
|
|
286
|
+
const value = object.$val === true ? stripAttributes(object) : object.$val;
|
|
287
|
+
return [sql`${JSON.stringify({ $val: value })}::jsonb`, false];
|
|
288
|
+
}
|
|
289
|
+
const curr = sql`"${raw(col)}"${path.length ? sql`#>${path}` : empty}`;
|
|
290
|
+
if (isEmpty(object)) return [curr, false];
|
|
291
|
+
const baseSql = sql`case jsonb_typeof(${curr})
|
|
292
|
+
when 'object' then ${curr}
|
|
293
|
+
else '{}'::jsonb
|
|
294
|
+
end`;
|
|
295
|
+
let maybeNull = true;
|
|
296
|
+
let hasNulls = false;
|
|
297
|
+
const patchSqls = Object.entries(object).map(([key, value]) => {
|
|
298
|
+
const [valSql, nullable] = getJsonUpdate(value, col, path.concat(key));
|
|
299
|
+
maybeNull &&= nullable;
|
|
300
|
+
hasNulls ||= nullable;
|
|
301
|
+
return sql`${key}::text, ${valSql}`;
|
|
302
|
+
});
|
|
303
|
+
let clause = sql`${baseSql} || jsonb_build_object(${join(patchSqls, ", ")})`;
|
|
304
|
+
if (hasNulls) {
|
|
305
|
+
clause = sql`(select jsonb_object_agg(key, value)
|
|
306
|
+
from jsonb_each(${clause}) where value <> 'null'::jsonb)`;
|
|
307
|
+
}
|
|
308
|
+
if (maybeNull) {
|
|
309
|
+
clause = sql`nullif(${clause}, '{}'::jsonb)`;
|
|
310
|
+
}
|
|
311
|
+
return [clause, maybeNull];
|
|
312
|
+
}
|
|
313
|
+
function stripAttributes(object) {
|
|
314
|
+
if (typeof object !== "object" || !object) return object;
|
|
315
|
+
if (Array.isArray(object)) {
|
|
316
|
+
return object.map((item) => stripAttributes(item));
|
|
317
|
+
}
|
|
318
|
+
return Object.entries(object).reduce(
|
|
319
|
+
(out, [key, val]) => {
|
|
320
|
+
if (key === "$put" || val === null) return out;
|
|
321
|
+
if (out === null) out = {};
|
|
322
|
+
out[key] = stripAttributes(val);
|
|
323
|
+
return out;
|
|
324
|
+
},
|
|
325
|
+
null
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const valid = {
|
|
329
|
+
$eq: true,
|
|
330
|
+
$lt: true,
|
|
331
|
+
$gt: true,
|
|
332
|
+
$lte: true,
|
|
333
|
+
$gte: true,
|
|
334
|
+
$re: true,
|
|
335
|
+
$ire: true,
|
|
336
|
+
$text: true,
|
|
337
|
+
$and: true,
|
|
338
|
+
$or: true,
|
|
339
|
+
$any: true,
|
|
340
|
+
$all: true,
|
|
341
|
+
$has: true,
|
|
342
|
+
$cts: true,
|
|
343
|
+
$ctd: true,
|
|
344
|
+
$keycts: true,
|
|
345
|
+
$keyctd: true
|
|
346
|
+
};
|
|
347
|
+
const inverse = {
|
|
348
|
+
$eq: "$neq",
|
|
349
|
+
$neq: "$eq",
|
|
350
|
+
$in: "$nin",
|
|
351
|
+
$nin: "$in",
|
|
352
|
+
$lt: "$gte",
|
|
353
|
+
$gte: "$lt",
|
|
354
|
+
$gt: "$lte",
|
|
355
|
+
$lte: "$gt"
|
|
356
|
+
};
|
|
357
|
+
function getAst(filter) {
|
|
358
|
+
return simplify(construct(filter));
|
|
359
|
+
}
|
|
360
|
+
function isValidSubQuery(node) {
|
|
361
|
+
if (!node || typeof node !== "object") return false;
|
|
362
|
+
const keys = Object.keys(node);
|
|
363
|
+
for (const key of keys) {
|
|
364
|
+
if (key[0] === "$" && !["$and", "$or", "$not"].includes(key)) return false;
|
|
365
|
+
if (key[0] !== "$") return true;
|
|
366
|
+
}
|
|
367
|
+
for (const key in node) {
|
|
368
|
+
if (!isValidSubQuery(node[key])) return false;
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
function construct(node, prop, op) {
|
|
373
|
+
if (!node || typeof node !== "object" || prop && op) {
|
|
374
|
+
if (op && prop) return [op, prop, node];
|
|
375
|
+
if (prop) return ["$eq", prop, node];
|
|
376
|
+
throw Error(`pgast.expected_prop_before:${JSON.stringify(node)}`);
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(node)) {
|
|
379
|
+
return ["$or", node.map((item) => construct(item, prop, op))];
|
|
380
|
+
}
|
|
381
|
+
if (prop && isValidSubQuery(node)) {
|
|
382
|
+
return ["$sub", prop, construct(node)];
|
|
383
|
+
}
|
|
384
|
+
return [
|
|
385
|
+
"$and",
|
|
386
|
+
Object.entries(node).map(([key, val]) => {
|
|
387
|
+
if (key === "$or" || key === "$and") {
|
|
388
|
+
return [key, construct(val, prop, op)[1]];
|
|
389
|
+
}
|
|
390
|
+
if (key === "$not") {
|
|
391
|
+
return [key, construct(val, prop, op)];
|
|
392
|
+
}
|
|
393
|
+
if (key[0] === "$") {
|
|
394
|
+
if (!valid[key]) throw Error(`pgast.invalid_op:${key}`);
|
|
395
|
+
if (op) throw Error(`pgast.unexpected_op:${op} before:${key}`);
|
|
396
|
+
if (!prop) throw Error(`pgast.expected_prop_before:${key}`);
|
|
397
|
+
return construct(val, prop, key);
|
|
398
|
+
}
|
|
399
|
+
return construct(val, key);
|
|
400
|
+
})
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
function simplify(node) {
|
|
404
|
+
const op = node[0];
|
|
405
|
+
if (op === "$and" || op === "$or") {
|
|
406
|
+
node[1] = node[1].map((subnode) => simplify(subnode));
|
|
407
|
+
} else if (op === "$not") {
|
|
408
|
+
node[1] = simplify(node[1]);
|
|
409
|
+
} else if (op === "$sub") {
|
|
410
|
+
node[2] = simplify(node[2]);
|
|
411
|
+
}
|
|
412
|
+
if (op === "$and") {
|
|
413
|
+
if (!node[1].length) return true;
|
|
414
|
+
if (node[1].includes(false)) return false;
|
|
415
|
+
node[1] = node[1].filter((item) => item !== true);
|
|
416
|
+
} else if (op === "$or") {
|
|
417
|
+
if (!node[1].length) return false;
|
|
418
|
+
if (node[1].includes(true)) return true;
|
|
419
|
+
node[1] = node[1].filter((item) => item !== false);
|
|
420
|
+
} else if (op === "$not" && typeof node[1] === "boolean") {
|
|
421
|
+
return !node[1];
|
|
422
|
+
}
|
|
423
|
+
if (op === "$or") {
|
|
424
|
+
const { eqmap, noneq, change } = node[1].reduce(
|
|
425
|
+
(acc, item) => {
|
|
426
|
+
if (item[0] !== "$eq" || item[2] === null) {
|
|
427
|
+
acc.noneq.push(item);
|
|
428
|
+
} else if (acc.eqmap[item[1]]) {
|
|
429
|
+
acc.change = true;
|
|
430
|
+
acc.eqmap[item[1]].push(item[2]);
|
|
431
|
+
return acc;
|
|
432
|
+
} else {
|
|
433
|
+
acc.eqmap[item[1]] = [item[2]];
|
|
434
|
+
}
|
|
435
|
+
return acc;
|
|
436
|
+
},
|
|
437
|
+
{ eqmap: {}, noneq: [], change: false }
|
|
438
|
+
);
|
|
439
|
+
if (change) {
|
|
440
|
+
node[1] = [
|
|
441
|
+
...noneq,
|
|
442
|
+
...Object.entries(eqmap).map(
|
|
443
|
+
([prop, val]) => val.length > 1 ? ["$in", prop, val] : ["$eq", prop, val[0]]
|
|
444
|
+
)
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if ((op === "$and" || op === "$or") && node[1].length === 1) {
|
|
449
|
+
return node[1][0];
|
|
450
|
+
}
|
|
451
|
+
if (op === "$not") {
|
|
452
|
+
const [subop, ...subargs] = node[1];
|
|
453
|
+
const invop = inverse[subop];
|
|
454
|
+
return invop ? [invop, ...subargs] : node;
|
|
455
|
+
}
|
|
456
|
+
return node;
|
|
457
|
+
}
|
|
458
|
+
const opSql = {
|
|
459
|
+
$and: "AND",
|
|
460
|
+
// Not SQL as these are used as delimiters
|
|
461
|
+
$or: "OR",
|
|
462
|
+
$not: sql`NOT`,
|
|
463
|
+
$eq: sql`=`,
|
|
464
|
+
$neq: sql`<>`,
|
|
465
|
+
$in: sql`IN`,
|
|
466
|
+
$nin: sql`NOT IN`,
|
|
467
|
+
$lt: sql`<`,
|
|
468
|
+
$lte: sql`<=`,
|
|
469
|
+
$gt: sql`>`,
|
|
470
|
+
$gte: sql`>=`,
|
|
471
|
+
$re: sql`~`,
|
|
472
|
+
$ire: sql`~*`,
|
|
473
|
+
$cts: sql`@>`,
|
|
474
|
+
$ctd: sql`<@`,
|
|
475
|
+
$keycts: sql`?|`,
|
|
476
|
+
$keyctd: sql`?&`
|
|
477
|
+
};
|
|
478
|
+
function getBinarySql(lhs, type, op, value, textLhs) {
|
|
479
|
+
if (value === null && op === "$eq") return sql`${lhs} IS NULL`;
|
|
480
|
+
if (value === null && op === "$neq") return sql`${lhs} IS NOT NULL`;
|
|
481
|
+
const sqlOp = opSql[op];
|
|
482
|
+
if (!sqlOp) throw Error(`pg.getSql_unknown_operator ${op}`);
|
|
483
|
+
if (op === "$in" || op === "$nin") {
|
|
484
|
+
if (type === "jsonb" && typeof value[0] === "string") lhs = textLhs;
|
|
485
|
+
return sql`${lhs} ${sqlOp} (${join(value)})`;
|
|
486
|
+
}
|
|
487
|
+
if (op === "$re" || op === "$ire") {
|
|
488
|
+
if (type === "jsonb") {
|
|
489
|
+
lhs = textLhs;
|
|
490
|
+
} else if (type !== "text") {
|
|
491
|
+
lhs = sql`(${lhs})::text`;
|
|
492
|
+
}
|
|
493
|
+
return sql`${lhs} ${sqlOp} ${String(value)}`;
|
|
494
|
+
}
|
|
495
|
+
if (type === "jsonb") {
|
|
496
|
+
if (typeof value === "string") {
|
|
497
|
+
return sql`${textLhs} ${sqlOp} ${value}`;
|
|
498
|
+
}
|
|
499
|
+
if ((op === "$keycts" || op === "$keyctd") && Array.isArray(value))
|
|
500
|
+
return sql`${lhs} ${sqlOp} ${value}::text[]`;
|
|
501
|
+
return sql`${lhs} ${sqlOp} ${JSON.stringify(value)}::jsonb`;
|
|
502
|
+
}
|
|
503
|
+
if (type === "cube") return sql`${lhs} ${sqlOp} ${cubeLiteralSql(value)}`;
|
|
504
|
+
return sql`${lhs} ${sqlOp} ${value}`;
|
|
505
|
+
}
|
|
506
|
+
function getNodeSql(ast, options) {
|
|
507
|
+
if (typeof ast === "boolean") return ast;
|
|
508
|
+
const op = ast[0];
|
|
509
|
+
if (op === "$and" || op === "$or") {
|
|
510
|
+
return sql`(${join(
|
|
511
|
+
ast[1].map((node) => getNodeSql(node, options)),
|
|
512
|
+
`) ${opSql[op]} (`
|
|
513
|
+
)})`;
|
|
514
|
+
}
|
|
515
|
+
if (op === "$not") {
|
|
516
|
+
return sql`${opSql[op]} (${getNodeSql(ast[1], options)})`;
|
|
517
|
+
}
|
|
518
|
+
if (op === "$sub") {
|
|
519
|
+
const joinName = ast[1];
|
|
520
|
+
if (!options.joins[joinName]) throw Error(`pg.no_join ${joinName}`);
|
|
521
|
+
const { idCol, schema } = options;
|
|
522
|
+
const joinOptions = options.joins[joinName];
|
|
523
|
+
const { table: joinTable, refCol } = options.joins[joinName];
|
|
524
|
+
return sql`"${raw(idCol)}" IN (SELECT "${raw(refCol)}"::${raw(
|
|
525
|
+
schema.types[idCol]
|
|
526
|
+
)} FROM "${raw(joinTable)}" WHERE ${getNodeSql(ast[2], joinOptions)})`;
|
|
527
|
+
}
|
|
528
|
+
const [prefix, ...suffix] = ast[1].split(".");
|
|
529
|
+
const { types: types2 = {} } = options.schema;
|
|
530
|
+
if (!types2[prefix]) throw Error(`pg.no_column ${prefix}`);
|
|
531
|
+
if (types2[prefix] === "jsonb") {
|
|
532
|
+
const [lhs, textLhs] = suffix.length ? [
|
|
533
|
+
sql`"${raw(prefix)}" #> ${suffix}`,
|
|
534
|
+
sql`"${raw(prefix)}" #>> ${suffix}`
|
|
535
|
+
] : [sql`"${raw(prefix)}"`, sql`"${raw(prefix)}" #>> '{}'`];
|
|
536
|
+
return getBinarySql(lhs, "jsonb", op, ast[2], textLhs);
|
|
537
|
+
}
|
|
538
|
+
if (suffix.length) throw Error(`pg.lookup_not_jsonb ${prefix}`);
|
|
539
|
+
return getBinarySql(sql`"${raw(prefix)}"`, types2[prefix], op, ast[2]);
|
|
540
|
+
}
|
|
541
|
+
function getSql(filter, options) {
|
|
542
|
+
const ast = getAst(filter);
|
|
543
|
+
return getNodeSql(ast, options);
|
|
544
|
+
}
|
|
545
|
+
const getIdMeta = ({ idCol, verDefault }) => sql`"${raw(idCol)}" AS "$key", ${raw(verDefault)} AS "$ver"`;
|
|
546
|
+
const getArgMeta = (key, { prefix, idCol, verDefault }) => sql`
|
|
547
|
+
${key} AS "$key",
|
|
548
|
+
${raw(verDefault)} AS "$ver",
|
|
549
|
+
array[
|
|
550
|
+
${join(prefix.map((k) => sql`${k}::text`))},
|
|
551
|
+
"${raw(idCol)}"
|
|
552
|
+
]::text[] AS "$ref"
|
|
553
|
+
`;
|
|
554
|
+
const getAggMeta = (key, { verDefault }) => sql`${key} AS "$key", ${raw(verDefault)} AS "$ver"`;
|
|
555
|
+
function getArgSql({ $first, $last, $after, $before, $since, $until, $all, $cursor: _, ...rest }, options) {
|
|
556
|
+
const { $order, $group, ...filter } = rest;
|
|
557
|
+
const { prefix, idCol } = options;
|
|
558
|
+
const meta = (key2) => $group ? getAggMeta(key2, options) : getArgMeta(key2, options);
|
|
559
|
+
const hasRangeArg = $before || $after || $since || $until || $first || $last || $all;
|
|
560
|
+
if ($order && $group) {
|
|
561
|
+
throw Error(`pg_arg.order_and_group_unsupported in ${prefix}`);
|
|
562
|
+
}
|
|
563
|
+
if (($order || $group && $group !== true) && !hasRangeArg) {
|
|
564
|
+
throw Error(`pg_arg.range_arg_expected in ${prefix}`);
|
|
565
|
+
}
|
|
566
|
+
const baseKey = sql`${JSON.stringify(rest)}::jsonb`;
|
|
567
|
+
const where = [];
|
|
568
|
+
if (!isEmpty(filter)) where.push(getSql(filter, options));
|
|
569
|
+
if (!hasRangeArg)
|
|
570
|
+
return {
|
|
571
|
+
meta: meta(baseKey),
|
|
572
|
+
where,
|
|
573
|
+
limit: 1,
|
|
574
|
+
ensureSingleRow: $group === true
|
|
575
|
+
};
|
|
576
|
+
const groupCols = Array.isArray($group) && $group.length && $group.map((prop) => lookup(prop, options));
|
|
577
|
+
const group = groupCols ? join(groupCols, ", ") : void 0;
|
|
578
|
+
const orderCols = ($order || [idCol]).map(
|
|
579
|
+
(orderItem) => orderItem[0] === "!" ? sql`-(${lookup(orderItem.slice(1), options)})::float8` : lookup(orderItem, options)
|
|
580
|
+
);
|
|
581
|
+
Object.entries({ $after, $before, $since, $until }).forEach(
|
|
582
|
+
([name, value]) => {
|
|
583
|
+
if (value) where.push(getBoundCond(orderCols, value, name));
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
const order = !$group && join(
|
|
587
|
+
($order || [idCol]).map(
|
|
588
|
+
(orderItem) => orderItem[0] === "!" ? sql`${lookup(orderItem.slice(1), options)} ${$last ? sql`ASC` : sql`DESC`}` : sql`${lookup(orderItem, options)} ${$last ? sql`DESC` : sql`ASC`}`
|
|
589
|
+
),
|
|
590
|
+
", "
|
|
591
|
+
);
|
|
592
|
+
const cursorKey = getJsonBuildTrusted({
|
|
593
|
+
$cursor: $group === true ? sql`''` : sql`jsonb_build_array(${join(groupCols || orderCols)})`
|
|
594
|
+
});
|
|
595
|
+
const key = sql`(${baseKey} || ${cursorKey})`;
|
|
596
|
+
return {
|
|
597
|
+
meta: meta(key),
|
|
598
|
+
where,
|
|
599
|
+
order,
|
|
600
|
+
group,
|
|
601
|
+
limit: $first || $last,
|
|
602
|
+
ensureSingleRow: true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function getBoundCond(orderCols, bound, kind) {
|
|
606
|
+
if (!Array.isArray(bound) || orderCols.length === 0 || bound.length === 0) {
|
|
607
|
+
throw Error(`pg_arg.bad_query bound : ${JSON.stringify(bound)}`);
|
|
608
|
+
}
|
|
609
|
+
switch (kind) {
|
|
610
|
+
case "$after":
|
|
611
|
+
return sql`(${join(orderCols, ",")}) > (${join(bound, ",")})`;
|
|
612
|
+
case "$since":
|
|
613
|
+
return sql`(${join(orderCols, ",")}) >= (${join(bound, ",")})`;
|
|
614
|
+
case "$before":
|
|
615
|
+
return sql`(${join(orderCols, ",")}) < (${join(bound, ",")})`;
|
|
616
|
+
case "$until":
|
|
617
|
+
return sql`(${join(orderCols, ",")}) <= (${join(bound, ",")})`;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const MAX_LIMIT = 4096;
|
|
621
|
+
function selectByArgs(args, projection, options) {
|
|
622
|
+
const { table, idCol } = options;
|
|
623
|
+
const { where, order, group, limit, meta, ensureSingleRow } = getArgSql(
|
|
624
|
+
args,
|
|
625
|
+
options
|
|
626
|
+
);
|
|
627
|
+
const clampedLimit = Math.min(MAX_LIMIT, limit || MAX_LIMIT);
|
|
628
|
+
if (!ensureSingleRow) {
|
|
629
|
+
return sql`
|
|
630
|
+
SELECT
|
|
631
|
+
${getSelectCols(options, projection)}, ${meta}
|
|
632
|
+
FROM "${raw(table)}" WHERE "${raw(idCol)}" = (
|
|
633
|
+
SELECT "${raw(idCol)}" FROM "${raw(table)}"
|
|
634
|
+
${where.length ? sql`WHERE ${join(where, " AND ")}` : empty}
|
|
635
|
+
LIMIT 2
|
|
636
|
+
)
|
|
637
|
+
`;
|
|
638
|
+
}
|
|
639
|
+
return sql`
|
|
640
|
+
SELECT
|
|
641
|
+
${getSelectCols(options, projection)}, ${meta}
|
|
642
|
+
FROM "${raw(table)}"
|
|
643
|
+
${where.length ? sql`WHERE ${join(where, " AND ")}` : empty}
|
|
644
|
+
${group ? sql`GROUP BY ${group}` : empty}
|
|
645
|
+
${order ? sql`ORDER BY ${order}` : empty}
|
|
646
|
+
LIMIT ${clampedLimit}
|
|
647
|
+
`;
|
|
648
|
+
}
|
|
649
|
+
function selectByIds(ids, projection, options) {
|
|
650
|
+
const { table, idCol } = options;
|
|
651
|
+
return sql`
|
|
652
|
+
SELECT
|
|
653
|
+
${getSelectCols(options, projection)}, ${getIdMeta(options)}
|
|
654
|
+
FROM "${raw(table)}"
|
|
655
|
+
WHERE "${raw(idCol)}" IN (${join(ids)})
|
|
656
|
+
`;
|
|
657
|
+
}
|
|
658
|
+
function getSingleSql(arg, options) {
|
|
659
|
+
const { table, idCol } = options;
|
|
660
|
+
if (!isPlainObject(arg)) {
|
|
661
|
+
return {
|
|
662
|
+
where: sql`"${raw(idCol)}" = ${arg}`,
|
|
663
|
+
meta: getIdMeta(options)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const { where, meta } = getArgSql(arg, options);
|
|
667
|
+
if (!where?.length) throw Error("pg_write.no_condition");
|
|
668
|
+
return {
|
|
669
|
+
where: sql`"${raw(idCol)}" = (
|
|
670
|
+
SELECT "${raw(idCol)}"
|
|
671
|
+
FROM "${raw(table)}"
|
|
672
|
+
WHERE ${join(where, " AND ")}
|
|
673
|
+
LIMIT 2
|
|
674
|
+
)`,
|
|
675
|
+
meta
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
function patch(object, arg, options) {
|
|
679
|
+
const { table } = options;
|
|
680
|
+
const { where, meta } = getSingleSql(arg, options);
|
|
681
|
+
const row = object;
|
|
682
|
+
return sql`
|
|
683
|
+
UPDATE "${raw(table)}" SET ${getUpdates(row, options)}
|
|
684
|
+
WHERE ${where}
|
|
685
|
+
RETURNING ${getSelectCols(options)}, ${meta}`;
|
|
686
|
+
}
|
|
687
|
+
function put(puts, options) {
|
|
688
|
+
const { idCol, table } = options;
|
|
689
|
+
const sqls = [];
|
|
690
|
+
const addSql = (rows, meta, conflictTarget) => {
|
|
691
|
+
const { cols, vals, updates } = getInsert(rows, options);
|
|
692
|
+
sqls.push(sql`
|
|
693
|
+
INSERT INTO "${raw(table)}" (${cols}) VALUES ${vals}
|
|
694
|
+
ON CONFLICT (${conflictTarget}) DO UPDATE SET ${updates}
|
|
695
|
+
RETURNING ${getSelectCols(options)}, ${meta}`);
|
|
696
|
+
};
|
|
697
|
+
const idRows = [];
|
|
698
|
+
for (const put2 of puts) {
|
|
699
|
+
const [row, arg] = put2;
|
|
700
|
+
if (!isPlainObject(arg)) {
|
|
701
|
+
idRows.push(row);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const { meta } = getArgSql(arg, options);
|
|
705
|
+
const conflictTarget = join(
|
|
706
|
+
Object.keys(arg).map((col) => sql`"${raw(col)}"`)
|
|
707
|
+
);
|
|
708
|
+
addSql([row], meta, conflictTarget);
|
|
709
|
+
}
|
|
710
|
+
if (idRows.length) {
|
|
711
|
+
const meta = getIdMeta(options);
|
|
712
|
+
const conflictTarget = sql`"${raw(idCol)}"`;
|
|
713
|
+
addSql(idRows, meta, conflictTarget);
|
|
714
|
+
}
|
|
715
|
+
return sqls;
|
|
716
|
+
}
|
|
717
|
+
function del(arg, options) {
|
|
718
|
+
const { table } = options;
|
|
719
|
+
const { where } = getSingleSql(arg, options);
|
|
720
|
+
return sql`
|
|
721
|
+
DELETE FROM "${raw(table)}"
|
|
722
|
+
WHERE ${where}
|
|
723
|
+
RETURNING ${arg} "$key"`;
|
|
724
|
+
}
|
|
725
|
+
const log = debug("graffy:pg:db");
|
|
726
|
+
const { Pool, Client, types } = pg$1;
|
|
727
|
+
class Db {
|
|
728
|
+
constructor(connection) {
|
|
729
|
+
if (typeof connection === "object" && connection && (connection instanceof Pool || connection instanceof Client)) {
|
|
730
|
+
this.client = connection;
|
|
731
|
+
} else {
|
|
732
|
+
this.client = new Pool(connection);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async query(sql2, tableOptions) {
|
|
736
|
+
log(`Making SQL query: ${sql2.text}`, sql2.values);
|
|
737
|
+
const cubeOid = Number.parseInt(tableOptions?.schema?.typeOids?.cube || "0", 10) || null;
|
|
738
|
+
try {
|
|
739
|
+
sql2.types = {
|
|
740
|
+
getTypeParser: (oid, format2) => {
|
|
741
|
+
if (oid === types.builtins.INT8) {
|
|
742
|
+
return (value) => Number.parseInt(value, 10);
|
|
743
|
+
}
|
|
744
|
+
if (oid === cubeOid) {
|
|
745
|
+
return (value) => {
|
|
746
|
+
const array = value.slice(1, -1).split(/\)\s*,\s*\(/).map(
|
|
747
|
+
(corner) => corner.split(",").map((coord) => Number.parseFloat(coord.trim()))
|
|
748
|
+
);
|
|
749
|
+
return array.length > 1 ? array : array[0];
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
return types.getTypeParser(oid, format2);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
return await this.client.query(sql2);
|
|
756
|
+
} catch (e) {
|
|
757
|
+
const message = [
|
|
758
|
+
e.message,
|
|
759
|
+
e.detail,
|
|
760
|
+
e.hint,
|
|
761
|
+
e.where,
|
|
762
|
+
sql2.text,
|
|
763
|
+
JSON.stringify(sql2.values)
|
|
764
|
+
].filter(Boolean).join("; ");
|
|
765
|
+
throw Error(`pg.sql_error ${message}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async readSql(sql2, tableOptions) {
|
|
769
|
+
const result = (await this.query(sql2, tableOptions)).rows;
|
|
770
|
+
log("Read result", result);
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
async writeSql(sql2, tableOptions) {
|
|
774
|
+
const res = await this.query(sql2, tableOptions);
|
|
775
|
+
log("Rows written", res.rowCount);
|
|
776
|
+
if (!res.rowCount) {
|
|
777
|
+
throw Error(`pg.nothing_written ${sql2.text} with ${sql2.values}`);
|
|
778
|
+
}
|
|
779
|
+
return res.rows;
|
|
780
|
+
}
|
|
781
|
+
/*
|
|
782
|
+
Adds .schema to tableOptions if it doesn't exist yet.
|
|
783
|
+
It mutates the argument, to "persist" the results and
|
|
784
|
+
avoid this query in every operation.
|
|
785
|
+
*/
|
|
786
|
+
async ensureSchema(tableOptions, typeOids) {
|
|
787
|
+
if (tableOptions.schema) return;
|
|
788
|
+
const { table, verCol, joins } = tableOptions;
|
|
789
|
+
const tableInfoSchema = (await this.query(sql`
|
|
790
|
+
SELECT table_schema, table_type
|
|
791
|
+
FROM information_schema.tables
|
|
792
|
+
WHERE table_name = ${table}
|
|
793
|
+
ORDER BY array_position(current_schemas(false)::text[], table_schema::text) ASC
|
|
794
|
+
LIMIT 1`)).rows[0];
|
|
795
|
+
const tableSchema = tableInfoSchema.table_schema;
|
|
796
|
+
const tableType = tableInfoSchema.table_type;
|
|
797
|
+
const types2 = (await this.query(sql`
|
|
798
|
+
SELECT jsonb_object_agg(column_name, udt_name) AS column_types
|
|
799
|
+
FROM information_schema.columns
|
|
800
|
+
WHERE
|
|
801
|
+
table_name = ${table} AND
|
|
802
|
+
table_schema = ${tableSchema}`)).rows[0].column_types;
|
|
803
|
+
if (!types2) throw Error(`pg.missing_table ${table}`);
|
|
804
|
+
typeOids = typeOids || (await this.query(sql`
|
|
805
|
+
SELECT jsonb_object_agg(typname, oid) AS type_oids
|
|
806
|
+
FROM pg_type
|
|
807
|
+
WHERE typname = 'cube'`)).rows[0].type_oids;
|
|
808
|
+
const verDefault = (await this.query(sql`
|
|
809
|
+
SELECT column_default
|
|
810
|
+
FROM information_schema.columns
|
|
811
|
+
WHERE
|
|
812
|
+
table_name = ${table} AND
|
|
813
|
+
table_schema = ${tableSchema} AND
|
|
814
|
+
column_name = ${verCol}`)).rows[0].column_default;
|
|
815
|
+
if (!verDefault && tableType !== "VIEW") {
|
|
816
|
+
throw Error(`pg.verCol_without_default ${verCol}`);
|
|
817
|
+
}
|
|
818
|
+
await Promise.all(
|
|
819
|
+
Object.values(joins).map(
|
|
820
|
+
(joinOptions) => this.ensureSchema(joinOptions, typeOids)
|
|
821
|
+
)
|
|
822
|
+
);
|
|
823
|
+
log("ensureSchema", types2);
|
|
824
|
+
tableOptions.schema = { types: types2, typeOids };
|
|
825
|
+
tableOptions.verDefault = verDefault;
|
|
826
|
+
}
|
|
827
|
+
async read(rootQuery, tableOptions) {
|
|
828
|
+
const idQueries = {};
|
|
829
|
+
const promises = [];
|
|
830
|
+
const results = [];
|
|
831
|
+
const { prefix: rawPrefix } = tableOptions;
|
|
832
|
+
const prefix = encodePath(rawPrefix);
|
|
833
|
+
await this.ensureSchema(tableOptions);
|
|
834
|
+
const getByArgs = async (args, projection) => {
|
|
835
|
+
const sql2 = selectByArgs(args, projection, tableOptions);
|
|
836
|
+
const result = await this.readSql(sql2, tableOptions);
|
|
837
|
+
const wrappedGraph = encodeGraph(wrapObject(result, rawPrefix));
|
|
838
|
+
log("getByArgs", wrappedGraph);
|
|
839
|
+
merge(results, wrappedGraph);
|
|
840
|
+
};
|
|
841
|
+
const explainArgs = async (args, _projection) => {
|
|
842
|
+
const { analyze, $explain: qArgs } = args;
|
|
843
|
+
const qSql = selectByArgs(qArgs, null, tableOptions);
|
|
844
|
+
const sql$1 = sql`EXPLAIN (${analyze ? sql`ANALYZE, BUFFERS, TIMING, ` : sql``}COSTS, VERBOSE, FORMAT JSON) ${qSql}`;
|
|
845
|
+
const result = await this.readSql(sql$1, tableOptions);
|
|
846
|
+
const wrappedGraph = encodeGraph(
|
|
847
|
+
wrapObject(
|
|
848
|
+
{
|
|
849
|
+
$key: args,
|
|
850
|
+
sql: formatSql(qSql),
|
|
851
|
+
plan: result[0]["QUERY PLAN"][0]
|
|
852
|
+
},
|
|
853
|
+
rawPrefix
|
|
854
|
+
)
|
|
855
|
+
);
|
|
856
|
+
log("explainArgs", wrappedGraph);
|
|
857
|
+
merge(results, wrappedGraph);
|
|
858
|
+
};
|
|
859
|
+
const getByIds = async () => {
|
|
860
|
+
const sql2 = selectByIds(Object.keys(idQueries), null, tableOptions);
|
|
861
|
+
const result = await this.readSql(sql2, tableOptions);
|
|
862
|
+
for (const object of result) {
|
|
863
|
+
const wrappedGraph = encodeGraph(wrapObject(object, rawPrefix));
|
|
864
|
+
log("getByIds", wrappedGraph);
|
|
865
|
+
merge(results, wrappedGraph);
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
const query = unwrap(rootQuery, prefix);
|
|
869
|
+
for (const node of query) {
|
|
870
|
+
const args = decodeArgs(node);
|
|
871
|
+
if (isPlainObject(args)) {
|
|
872
|
+
if (node.prefix) {
|
|
873
|
+
for (const childNode of node.children) {
|
|
874
|
+
const childArgs = decodeArgs(childNode);
|
|
875
|
+
const projection = childNode.children ? decodeQuery(childNode.children) : null;
|
|
876
|
+
promises.push(getByArgs({ ...args, ...childArgs }, projection));
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
const projection = node.children ? decodeQuery(node.children) : null;
|
|
880
|
+
if (args.$explain) {
|
|
881
|
+
promises.push(explainArgs(args));
|
|
882
|
+
} else {
|
|
883
|
+
promises.push(getByArgs(args, projection));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
idQueries[args] = node.children;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (!isEmpty(idQueries)) promises.push(getByIds());
|
|
891
|
+
await Promise.all(promises);
|
|
892
|
+
log("dbRead", rootQuery, results);
|
|
893
|
+
return finalize(results, wrap(query, prefix));
|
|
894
|
+
}
|
|
895
|
+
async write(rootChange, tableOptions) {
|
|
896
|
+
const { prefix: rawPrefix } = tableOptions;
|
|
897
|
+
const prefix = encodePath(rawPrefix);
|
|
898
|
+
await this.ensureSchema(tableOptions);
|
|
899
|
+
const change = unwrap(rootChange, prefix);
|
|
900
|
+
const puts = [];
|
|
901
|
+
const sqls = [];
|
|
902
|
+
for (const node of change) {
|
|
903
|
+
const arg = decodeArgs(node);
|
|
904
|
+
if (isRange(node)) {
|
|
905
|
+
if (cmp(node.key, node.end) === 0) {
|
|
906
|
+
log("delete", node);
|
|
907
|
+
sqls.push(del(arg, tableOptions));
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
throw Error("pg_write.write_range_unsupported");
|
|
911
|
+
}
|
|
912
|
+
const object = decodeGraph(node.children);
|
|
913
|
+
if (isPlainObject(arg)) {
|
|
914
|
+
mergeObject(object, arg);
|
|
915
|
+
} else {
|
|
916
|
+
object[tableOptions.idCol] = arg;
|
|
917
|
+
}
|
|
918
|
+
if (object.$put && object.$put !== true) {
|
|
919
|
+
throw Error("pg_write.partial_put_unsupported");
|
|
920
|
+
}
|
|
921
|
+
if (object.$put) {
|
|
922
|
+
puts.push([object, arg]);
|
|
923
|
+
} else {
|
|
924
|
+
sqls.push(patch(object, arg, tableOptions));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (puts.length) sqls.push(...put(puts, tableOptions));
|
|
928
|
+
const result = [];
|
|
929
|
+
await Promise.all(
|
|
930
|
+
sqls.map(
|
|
931
|
+
(sql2) => this.writeSql(sql2, tableOptions).then((object) => {
|
|
932
|
+
log("returned_object_wrapped", wrapObject(object, rawPrefix));
|
|
933
|
+
merge(result, encodeGraph(wrapObject(object, rawPrefix)));
|
|
934
|
+
})
|
|
935
|
+
)
|
|
936
|
+
);
|
|
937
|
+
log("dbWrite", rootChange, result);
|
|
938
|
+
return result;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function getTableOpts(name, { table, idCol, verCol, joins, schema, verDefault } = {}, parentName = null) {
|
|
942
|
+
const tableName = table || name;
|
|
943
|
+
return {
|
|
944
|
+
table: table || name,
|
|
945
|
+
idCol: idCol || "id",
|
|
946
|
+
verCol: verCol || "updatedAt",
|
|
947
|
+
joins: Object.fromEntries(
|
|
948
|
+
Object.entries(joins || {}).map(
|
|
949
|
+
([joinName, { refCol = parentName, ...joinOptions }]) => [
|
|
950
|
+
joinName,
|
|
951
|
+
{
|
|
952
|
+
refCol,
|
|
953
|
+
...getTableOpts(joinName, joinOptions, tableName)
|
|
954
|
+
}
|
|
955
|
+
]
|
|
956
|
+
)
|
|
957
|
+
),
|
|
958
|
+
schema,
|
|
959
|
+
verDefault
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
const pg = ({ connection, ...rawOptions }) => (store) => {
|
|
963
|
+
store.on("read", read);
|
|
964
|
+
store.on("write", write);
|
|
965
|
+
const prefix = store.path;
|
|
966
|
+
const tableOpts = getTableOpts(prefix[prefix.length - 1], rawOptions);
|
|
967
|
+
tableOpts.prefix = prefix;
|
|
968
|
+
const defaultDb = new Db(connection);
|
|
969
|
+
function read(query, options, next) {
|
|
970
|
+
const { pgClient } = options;
|
|
971
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
972
|
+
const readPromise = db.read(query, tableOpts);
|
|
973
|
+
const remainingQuery = remove(query, encodePath(prefix));
|
|
974
|
+
const nextPromise = next(remainingQuery);
|
|
975
|
+
return Promise.all([readPromise, nextPromise]).then(
|
|
976
|
+
([readRes, nextRes]) => {
|
|
977
|
+
return merge(readRes, nextRes);
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
function write(change, options, next) {
|
|
982
|
+
const { pgClient } = options;
|
|
983
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
984
|
+
const writePromise = db.write(change, tableOpts);
|
|
985
|
+
const remainingChange = remove(change, encodePath(prefix));
|
|
986
|
+
const nextPromise = next(remainingChange);
|
|
987
|
+
return Promise.all([writePromise, nextPromise]).then(
|
|
988
|
+
([writeRes, nextRes]) => {
|
|
989
|
+
return merge(writeRes, nextRes);
|
|
990
|
+
}
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
export {
|
|
995
|
+
pg
|
|
996
|
+
};
|