@graffy/pg 0.15.13 → 0.15.14-alpha.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 +129 -128
- package/index.mjs +130 -129
- package/package.json +4 -4
- package/types/Db.d.ts +1 -0
- package/types/filter/getSql.d.ts +1 -3
- package/types/index.d.ts +2 -1
- package/types/sql/clauses.d.ts +1 -2
package/index.cjs
CHANGED
|
@@ -53,8 +53,7 @@ const valid = {
|
|
|
53
53
|
$all: true,
|
|
54
54
|
$has: true,
|
|
55
55
|
$cts: true,
|
|
56
|
-
$ctd: true
|
|
57
|
-
$ovl: true
|
|
56
|
+
$ctd: true
|
|
58
57
|
};
|
|
59
58
|
const inverse = {
|
|
60
59
|
$eq: "$neq",
|
|
@@ -67,10 +66,8 @@ const inverse = {
|
|
|
67
66
|
$lte: "$gt"
|
|
68
67
|
};
|
|
69
68
|
function getAst(filter) {
|
|
70
|
-
counter = 0;
|
|
71
69
|
return simplify(construct(filter));
|
|
72
70
|
}
|
|
73
|
-
let counter;
|
|
74
71
|
function construct(node, prop, op) {
|
|
75
72
|
if (!node || typeof node !== "object" || prop && op) {
|
|
76
73
|
if (op && prop)
|
|
@@ -91,14 +88,6 @@ function construct(node, prop, op) {
|
|
|
91
88
|
if (key === "$not") {
|
|
92
89
|
return [key, construct(val, prop, op)];
|
|
93
90
|
}
|
|
94
|
-
if (key === "$any" || key === "$all") {
|
|
95
|
-
const elkey = "el$" + counter++;
|
|
96
|
-
return [key, prop, elkey, construct(val, elkey)];
|
|
97
|
-
}
|
|
98
|
-
if (key === "$has") {
|
|
99
|
-
const elkey = "el$" + counter++;
|
|
100
|
-
return [key, prop, elkey, construct(val, elkey)[1]];
|
|
101
|
-
}
|
|
102
91
|
if (key[0] === "$") {
|
|
103
92
|
if (!valid[key])
|
|
104
93
|
throw Error("pgast.invalid_op:" + key);
|
|
@@ -123,10 +112,21 @@ function simplify(node) {
|
|
|
123
112
|
node[1] = node[1].map((subnode) => simplify(subnode));
|
|
124
113
|
} else if (op === "$not") {
|
|
125
114
|
node[1] = simplify(node[1]);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
115
|
+
}
|
|
116
|
+
if (op === "$and") {
|
|
117
|
+
if (!node[1].length)
|
|
118
|
+
return true;
|
|
119
|
+
if (node[1].includes(false))
|
|
120
|
+
return false;
|
|
121
|
+
node[1] = node[1].filter((item) => item !== true);
|
|
122
|
+
} else if (op === "$or") {
|
|
123
|
+
if (!node[1].length)
|
|
124
|
+
return false;
|
|
125
|
+
if (node[1].includes(true))
|
|
126
|
+
return true;
|
|
127
|
+
node[1] = node[1].filter((item) => item !== false);
|
|
128
|
+
} else if (op === "$not" && typeof node[1] === "boolean") {
|
|
129
|
+
return !node[1];
|
|
130
130
|
}
|
|
131
131
|
if (op === "$or") {
|
|
132
132
|
const { eqmap, noneq, change } = node[1].reduce((acc, item) => {
|
|
@@ -148,112 +148,75 @@ function simplify(node) {
|
|
|
148
148
|
];
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
if (op === "$and" || op === "$or") {
|
|
152
|
-
|
|
153
|
-
throw Error("pgast.expected_children:" + op);
|
|
154
|
-
return node[1].length === 1 ? node[1][0] : node;
|
|
151
|
+
if ((op === "$and" || op === "$or") && node[1].length === 1) {
|
|
152
|
+
return node[1][0];
|
|
155
153
|
}
|
|
156
154
|
if (op === "$not") {
|
|
157
155
|
const [subop, ...subargs] = node[1];
|
|
158
156
|
const invop = inverse[subop];
|
|
159
157
|
return invop ? [invop, ...subargs] : node;
|
|
160
158
|
}
|
|
161
|
-
if (op === "$any" || op === "$all") {
|
|
162
|
-
const [_, list, elkey, subnode] = node;
|
|
163
|
-
const [subop, elk, val] = subnode;
|
|
164
|
-
return (subop === "$eq" || subop === "$in") && elkey === elk ? [op === "$any" ? "$ovl" : "$ctd", list, subop === "$eq" ? [val] : val] : node;
|
|
165
|
-
}
|
|
166
|
-
if (op === "$has") {
|
|
167
|
-
const [_, list, elkey, limbs] = node;
|
|
168
|
-
return limbs.every(([subop, elk]) => subop === "$eq" && elk === elkey) ? ["$cts", list, limbs.map(([_op, _prop, val]) => val)] : node;
|
|
169
|
-
}
|
|
170
159
|
return node;
|
|
171
160
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return
|
|
161
|
+
const opSql = {
|
|
162
|
+
$and: `AND`,
|
|
163
|
+
$or: `OR`,
|
|
164
|
+
$not: sql__default["default"]`NOT`,
|
|
165
|
+
$eq: sql__default["default"]`=`,
|
|
166
|
+
$neq: sql__default["default"]`<>`,
|
|
167
|
+
$in: sql__default["default"]`IN`,
|
|
168
|
+
$nin: sql__default["default"]`NOT IN`,
|
|
169
|
+
$lt: sql__default["default"]`<`,
|
|
170
|
+
$lte: sql__default["default"]`<=`,
|
|
171
|
+
$gt: sql__default["default"]`>`,
|
|
172
|
+
$gte: sql__default["default"]`>=`,
|
|
173
|
+
$re: sql__default["default"]`~`,
|
|
174
|
+
$ire: sql__default["default"]`~*`,
|
|
175
|
+
$cts: sql__default["default"]`@>`,
|
|
176
|
+
$ctd: sql__default["default"]`<@`
|
|
177
|
+
};
|
|
178
|
+
function castValue$1(value, type, op) {
|
|
179
|
+
if (value === null && op === "$eq")
|
|
180
|
+
return sql__default["default"]`IS NULL`;
|
|
181
|
+
if (value === null && op === "$neq")
|
|
182
|
+
return sql__default["default"]`IS NOT NULL`;
|
|
183
|
+
const sqlOp = opSql[op];
|
|
184
|
+
if (!sqlOp)
|
|
185
|
+
throw Error("pg.getSql_unknown_operator " + op);
|
|
186
|
+
if (op === "$in" || op === "$nin") {
|
|
187
|
+
return sql__default["default"]`${sqlOp} (${sql.join(value)})`;
|
|
192
188
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return sql__default["default"]`(${lookup2(left, rType)})::${sql.raw(rType)} ${sql.raw(op)} ${right}`;
|
|
189
|
+
if (type === "jsonb") {
|
|
190
|
+
return sql__default["default"]`${sqlOp} ${JSON.stringify(value)}::jsonb`;
|
|
191
|
+
}
|
|
192
|
+
if (type === "cube") {
|
|
193
|
+
if (!Array.isArray(value) || value.length !== 2 || !Array.isArray(value[0])) {
|
|
194
|
+
throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
|
|
200
195
|
}
|
|
196
|
+
return sql__default["default"]`${sqlOp} cube(${sql.join(value)})`;
|
|
201
197
|
}
|
|
198
|
+
return sql__default["default"]`${sqlOp} ${value}`;
|
|
199
|
+
}
|
|
200
|
+
function getSql(filter, options) {
|
|
202
201
|
function getNodeSql(ast) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return binop(">", ast[1], ast[2]);
|
|
218
|
-
case "$gte":
|
|
219
|
-
return binop(">=", ast[1], ast[2]);
|
|
220
|
-
case "$re":
|
|
221
|
-
return binop("~", ast[1], ast[2]);
|
|
222
|
-
case "$ire":
|
|
223
|
-
return binop("~*", ast[1], ast[2]);
|
|
224
|
-
case "$in":
|
|
225
|
-
return sql__default["default"]`${lookup2(ast[1])} IN (${sql.join(ast[2])})`;
|
|
226
|
-
case "$nin":
|
|
227
|
-
return sql__default["default"]`${lookup2(ast[1])} NOT IN (${sql.join(ast[2])})`;
|
|
228
|
-
case "$cts":
|
|
229
|
-
return sql__default["default"]`${lookup2(ast[1])} @> ${ast[2]}`;
|
|
230
|
-
case "$ctd":
|
|
231
|
-
return sql__default["default"]`${lookup2(ast[1])} <@ ${ast[2]}`;
|
|
232
|
-
case "$ovl":
|
|
233
|
-
switch (getColumnType(ast[1])) {
|
|
234
|
-
case "jsonb":
|
|
235
|
-
case "any":
|
|
236
|
-
return sql__default["default"]`${lookup2(ast[1])} ?| ${Array.isArray(ast[2]) ? ast[2] : Object.keys(ast[2])}`;
|
|
237
|
-
case "array":
|
|
238
|
-
return sql__default["default"]`${lookup2(ast[1])} && ${ast[2]}`;
|
|
239
|
-
default:
|
|
240
|
-
throw Error("pg.getSql_ovl_unknown_column_type");
|
|
241
|
-
}
|
|
242
|
-
case "$and":
|
|
243
|
-
return sql__default["default"]`(${sql.join(ast[1].map((node) => getNodeSql(node)), `) AND (`)})`;
|
|
244
|
-
case "$or":
|
|
245
|
-
return sql__default["default"]`(${sql.join(ast[1].map((node) => getNodeSql(node)), `) OR (`)})`;
|
|
246
|
-
case "$not":
|
|
247
|
-
return sql__default["default"]`NOT (${getNodeSql(ast[1])})`;
|
|
248
|
-
case "$any":
|
|
249
|
-
return sql__default["default"]`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
250
|
-
case "$all":
|
|
251
|
-
return sql__default["default"]`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
252
|
-
case "$has":
|
|
253
|
-
return sql__default["default"]`(SELECT bool_or(${sql.join(ast[3].map((node) => getNodeSql(node)), `) AND bool_or(`)}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
254
|
-
default:
|
|
255
|
-
throw Error("pg.getSql_unknown_operator: " + ast[0]);
|
|
202
|
+
if (typeof ast === "boolean")
|
|
203
|
+
return ast;
|
|
204
|
+
const op = ast[0];
|
|
205
|
+
if (op === "$and" || op === "$or") {
|
|
206
|
+
return sql__default["default"]`(${sql.join(ast[1].map((node) => getNodeSql(node)), `) ${opSql[op]} (`)})`;
|
|
207
|
+
} else if (op === "$not") {
|
|
208
|
+
return sql__default["default"]`${opSql[op]} (${getNodeSql(ast[1])})`;
|
|
209
|
+
}
|
|
210
|
+
const [prefix, ...suffix] = common.encodePath(ast[1]);
|
|
211
|
+
const { types } = options.schema;
|
|
212
|
+
if (!types[prefix])
|
|
213
|
+
throw Error("pg.no_column " + prefix);
|
|
214
|
+
if (suffix.length && types[prefix] !== "jsonb") {
|
|
215
|
+
throw Error("pg.lookup_not_jsonb " + prefix);
|
|
256
216
|
}
|
|
217
|
+
const [lhs, type] = suffix.length ? [sql__default["default"]`"${sql.raw(prefix)}" #> ${suffix}`, "jsonb"] : [sql__default["default"]`"${sql.raw(prefix)}"`, types[prefix]];
|
|
218
|
+
const rhs = castValue$1(ast[2], type, op);
|
|
219
|
+
return sql__default["default"]`${lhs} ${rhs}`;
|
|
257
220
|
}
|
|
258
221
|
return getNodeSql(getAst(filter));
|
|
259
222
|
}
|
|
@@ -271,14 +234,9 @@ const getJsonBuildValue = (value) => {
|
|
|
271
234
|
return sql__default["default"]`${value}::text`;
|
|
272
235
|
return sql__default["default"]`${JSON.stringify(stripAttributes(value))}::jsonb`;
|
|
273
236
|
};
|
|
274
|
-
const lookup = (prop
|
|
237
|
+
const lookup = (prop) => {
|
|
275
238
|
const [prefix, ...suffix] = common.encodePath(prop);
|
|
276
|
-
|
|
277
|
-
return suffix.length ? sql__default["default"]`"${sql.raw(prefix)}" ${op} ${suffix}` : sql__default["default"]`"${sql.raw(prefix)}"`;
|
|
278
|
-
};
|
|
279
|
-
const getType = (prop) => {
|
|
280
|
-
const [_prefix, ...suffix] = common.encodePath(prop);
|
|
281
|
-
return suffix.length ? "jsonb" : "any";
|
|
239
|
+
return suffix.length ? sql__default["default"]`"${sql.raw(prefix)}" #> ${suffix}` : sql__default["default"]`"${sql.raw(prefix)}"`;
|
|
282
240
|
};
|
|
283
241
|
const aggSql = {
|
|
284
242
|
$sum: (prop) => sql__default["default"]`sum((${lookup(prop)})::numeric)`,
|
|
@@ -306,19 +264,38 @@ const getSelectCols = (table, projection = null) => {
|
|
|
306
264
|
}
|
|
307
265
|
return sql__default["default"]`jsonb_build_object(${sql.join(sqls, ", ")})`;
|
|
308
266
|
};
|
|
267
|
+
function infJoin(array) {
|
|
268
|
+
return sql__default["default"]`array[${sql.join(array.map((num) => num === Infinity ? sql__default["default"]`'Infinity'` : num === -Infinity ? sql__default["default"]`'-Infinity'` : num))}]::float8[]`;
|
|
269
|
+
}
|
|
270
|
+
function castValue(value, type, name) {
|
|
271
|
+
if (!type)
|
|
272
|
+
throw Error("pg.write_no_column " + name);
|
|
273
|
+
if (value instanceof sql.Sql)
|
|
274
|
+
return value;
|
|
275
|
+
if (value === null)
|
|
276
|
+
return sql__default["default"]`NULL`;
|
|
277
|
+
if (type === "jsonb") {
|
|
278
|
+
return value.$put ? JSON.stringify(stripAttributes(value)) : sql__default["default"]`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})`;
|
|
279
|
+
}
|
|
280
|
+
if (type === "cube") {
|
|
281
|
+
if (!Array.isArray(value) || !value.length || Array.isArray(value[0]) && value.length !== 2) {
|
|
282
|
+
throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
|
|
283
|
+
}
|
|
284
|
+
return Array.isArray(value[0]) ? sql__default["default"]`cube(${infJoin(value[0])}, ${infJoin(value[1])})` : sql__default["default"]`cube(${infJoin(value)})`;
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
309
288
|
const getInsert = (row, options) => {
|
|
310
289
|
const cols = [];
|
|
311
290
|
const vals = [];
|
|
312
|
-
Object.entries(row).filter(([
|
|
291
|
+
Object.entries(row).filter(([col]) => col !== options.verCol && col[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
|
|
313
292
|
cols.push(sql__default["default"]`"${sql.raw(col)}"`);
|
|
314
|
-
vals.push(val
|
|
293
|
+
vals.push(castValue(val, options.schema.types[col], col));
|
|
315
294
|
});
|
|
316
295
|
return { cols: sql.join(cols, ", "), vals: sql.join(vals, ", ") };
|
|
317
296
|
};
|
|
318
297
|
const getUpdates = (row, options) => {
|
|
319
|
-
return sql.join(Object.entries(row).filter(([
|
|
320
|
-
return sql__default["default"]`"${sql.raw(name)}" = ${value instanceof sql.Sql || typeof value !== "object" || !value ? value : !value.$put ? sql__default["default"]`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})` : sql__default["default"]`${JSON.stringify(stripAttributes(value))}::jsonb`}`;
|
|
321
|
-
}).concat(sql__default["default"]`"${sql.raw(options.verCol)}" = ${nowTimestamp}`), ", ");
|
|
298
|
+
return sql.join(Object.entries(row).filter(([col]) => col !== options.idCol && col[0] !== "$").map(([col, val]) => sql__default["default"]`"${sql.raw(col)}" = ${castValue(val, options.schema.types[col], col)}`).concat(sql__default["default"]`"${sql.raw(options.verCol)}" = ${nowTimestamp}`), ", ");
|
|
322
299
|
};
|
|
323
300
|
function getJsonUpdate(_a, col, path) {
|
|
324
301
|
var _b = _a, { $put } = _b, object = __objRest(_b, ["$put"]);
|
|
@@ -366,13 +343,13 @@ function getArgSql(_c, options) {
|
|
|
366
343
|
throw Error("pg_arg.order_and_group_unsupported in " + prefix);
|
|
367
344
|
}
|
|
368
345
|
const meta = (key2) => $group ? getAggMeta(key2, $group) : getArgMeta(key2, prefix, idCol);
|
|
369
|
-
const groupCols = Array.isArray($group) && $group.length
|
|
346
|
+
const groupCols = Array.isArray($group) && $group.length && $group.map(lookup);
|
|
370
347
|
const group = groupCols ? sql.join(groupCols, ", ") : void 0;
|
|
371
348
|
const hasRangeArg = $before || $after || $since || $until || $first || $last || $all || $order;
|
|
372
349
|
let key;
|
|
373
350
|
const where = [];
|
|
374
351
|
if (!common.isEmpty(filter)) {
|
|
375
|
-
where.push(getSql(filter,
|
|
352
|
+
where.push(getSql(filter, options));
|
|
376
353
|
key = sql__default["default"]`${JSON.stringify(filter)}::jsonb`;
|
|
377
354
|
}
|
|
378
355
|
if (!hasRangeArg)
|
|
@@ -533,24 +510,46 @@ class Db {
|
|
|
533
510
|
}
|
|
534
511
|
}
|
|
535
512
|
async readSql(sql2) {
|
|
536
|
-
|
|
537
|
-
result = result.rows.flat();
|
|
513
|
+
const result = (await this.query(sql2)).rows.flat();
|
|
538
514
|
log("Read result", result);
|
|
539
515
|
return result;
|
|
540
516
|
}
|
|
541
517
|
async writeSql(sql2) {
|
|
542
|
-
|
|
518
|
+
const res = await this.query(sql2);
|
|
543
519
|
log("Rows written", res.rowCount);
|
|
544
520
|
if (!res.rowCount) {
|
|
545
521
|
throw Error("pg.nothing_written " + sql2.text + " with " + sql2.values);
|
|
546
522
|
}
|
|
547
523
|
return res.rows[0][0];
|
|
548
524
|
}
|
|
525
|
+
async ensureSchema(tableOptions) {
|
|
526
|
+
if (tableOptions.schema)
|
|
527
|
+
return;
|
|
528
|
+
const { table } = tableOptions;
|
|
529
|
+
const types = (await this.query(sql__default["default"]`SELECT jsonb_object_agg(
|
|
530
|
+
column_name,
|
|
531
|
+
udt_name
|
|
532
|
+
)
|
|
533
|
+
FROM information_schema.columns
|
|
534
|
+
WHERE
|
|
535
|
+
table_name = ${table} AND
|
|
536
|
+
table_schema = (
|
|
537
|
+
SELECT table_schema
|
|
538
|
+
FROM information_schema.tables
|
|
539
|
+
WHERE table_name = ${table}
|
|
540
|
+
ORDER BY array_position(current_schemas(false)::text[], table_schema::text) ASC
|
|
541
|
+
LIMIT 1)`)).rows[0][0];
|
|
542
|
+
if (!types)
|
|
543
|
+
throw Error(`pg.missing_table ${table}`);
|
|
544
|
+
log("ensureSchema", types);
|
|
545
|
+
tableOptions.schema = { types };
|
|
546
|
+
}
|
|
549
547
|
async read(rootQuery, tableOptions) {
|
|
550
548
|
const idQueries = {};
|
|
551
549
|
const promises = [];
|
|
552
550
|
const results = [];
|
|
553
551
|
const { prefix } = tableOptions;
|
|
552
|
+
await this.ensureSchema(tableOptions);
|
|
554
553
|
const getByArgs = async (args, projection) => {
|
|
555
554
|
const result = await this.readSql(selectByArgs(args, projection, tableOptions));
|
|
556
555
|
const wrappedGraph = common.encodeGraph(common.wrapObject(result, prefix));
|
|
@@ -591,6 +590,7 @@ class Db {
|
|
|
591
590
|
}
|
|
592
591
|
async write(rootChange, tableOptions) {
|
|
593
592
|
const { prefix } = tableOptions;
|
|
593
|
+
await this.ensureSchema(tableOptions);
|
|
594
594
|
const change = common.unwrap(rootChange, prefix);
|
|
595
595
|
const sqls = change.map((node) => {
|
|
596
596
|
const arg = common.decodeArgs(node);
|
|
@@ -618,7 +618,7 @@ class Db {
|
|
|
618
618
|
return result;
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
|
-
const pg = ({ table, idCol, verCol, connection }) => (store) => {
|
|
621
|
+
const pg = ({ table, idCol, verCol, connection, schema }) => (store) => {
|
|
622
622
|
store.on("read", read);
|
|
623
623
|
store.on("write", write);
|
|
624
624
|
const prefix = store.path;
|
|
@@ -626,7 +626,8 @@ const pg = ({ table, idCol, verCol, connection }) => (store) => {
|
|
|
626
626
|
prefix,
|
|
627
627
|
table: table || prefix[prefix.length - 1] || "default",
|
|
628
628
|
idCol: idCol || "id",
|
|
629
|
-
verCol: verCol || "updatedAt"
|
|
629
|
+
verCol: verCol || "updatedAt",
|
|
630
|
+
schema
|
|
630
631
|
};
|
|
631
632
|
const defaultDb = new Db(connection);
|
|
632
633
|
function read(query, options, next) {
|
package/index.mjs
CHANGED
|
@@ -26,7 +26,7 @@ var __objRest = (source, exclude) => {
|
|
|
26
26
|
}
|
|
27
27
|
return target;
|
|
28
28
|
};
|
|
29
|
-
import {
|
|
29
|
+
import { encodePath, isEmpty, isPlainObject, unwrap, decodeArgs, decodeQuery, slice, finalize, wrap, isRange, decodeGraph, mergeObject, merge, encodeGraph, wrapObject, remove } from "@graffy/common";
|
|
30
30
|
import { Pool, Client } from "pg";
|
|
31
31
|
import sql, { join, raw, Sql, empty } from "sql-template-tag";
|
|
32
32
|
import debug from "debug";
|
|
@@ -45,8 +45,7 @@ const valid = {
|
|
|
45
45
|
$all: true,
|
|
46
46
|
$has: true,
|
|
47
47
|
$cts: true,
|
|
48
|
-
$ctd: true
|
|
49
|
-
$ovl: true
|
|
48
|
+
$ctd: true
|
|
50
49
|
};
|
|
51
50
|
const inverse = {
|
|
52
51
|
$eq: "$neq",
|
|
@@ -59,10 +58,8 @@ const inverse = {
|
|
|
59
58
|
$lte: "$gt"
|
|
60
59
|
};
|
|
61
60
|
function getAst(filter) {
|
|
62
|
-
counter = 0;
|
|
63
61
|
return simplify(construct(filter));
|
|
64
62
|
}
|
|
65
|
-
let counter;
|
|
66
63
|
function construct(node, prop, op) {
|
|
67
64
|
if (!node || typeof node !== "object" || prop && op) {
|
|
68
65
|
if (op && prop)
|
|
@@ -83,14 +80,6 @@ function construct(node, prop, op) {
|
|
|
83
80
|
if (key === "$not") {
|
|
84
81
|
return [key, construct(val, prop, op)];
|
|
85
82
|
}
|
|
86
|
-
if (key === "$any" || key === "$all") {
|
|
87
|
-
const elkey = "el$" + counter++;
|
|
88
|
-
return [key, prop, elkey, construct(val, elkey)];
|
|
89
|
-
}
|
|
90
|
-
if (key === "$has") {
|
|
91
|
-
const elkey = "el$" + counter++;
|
|
92
|
-
return [key, prop, elkey, construct(val, elkey)[1]];
|
|
93
|
-
}
|
|
94
83
|
if (key[0] === "$") {
|
|
95
84
|
if (!valid[key])
|
|
96
85
|
throw Error("pgast.invalid_op:" + key);
|
|
@@ -115,10 +104,21 @@ function simplify(node) {
|
|
|
115
104
|
node[1] = node[1].map((subnode) => simplify(subnode));
|
|
116
105
|
} else if (op === "$not") {
|
|
117
106
|
node[1] = simplify(node[1]);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
}
|
|
108
|
+
if (op === "$and") {
|
|
109
|
+
if (!node[1].length)
|
|
110
|
+
return true;
|
|
111
|
+
if (node[1].includes(false))
|
|
112
|
+
return false;
|
|
113
|
+
node[1] = node[1].filter((item) => item !== true);
|
|
114
|
+
} else if (op === "$or") {
|
|
115
|
+
if (!node[1].length)
|
|
116
|
+
return false;
|
|
117
|
+
if (node[1].includes(true))
|
|
118
|
+
return true;
|
|
119
|
+
node[1] = node[1].filter((item) => item !== false);
|
|
120
|
+
} else if (op === "$not" && typeof node[1] === "boolean") {
|
|
121
|
+
return !node[1];
|
|
122
122
|
}
|
|
123
123
|
if (op === "$or") {
|
|
124
124
|
const { eqmap, noneq, change } = node[1].reduce((acc, item) => {
|
|
@@ -140,112 +140,75 @@ function simplify(node) {
|
|
|
140
140
|
];
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
if (op === "$and" || op === "$or") {
|
|
144
|
-
|
|
145
|
-
throw Error("pgast.expected_children:" + op);
|
|
146
|
-
return node[1].length === 1 ? node[1][0] : node;
|
|
143
|
+
if ((op === "$and" || op === "$or") && node[1].length === 1) {
|
|
144
|
+
return node[1][0];
|
|
147
145
|
}
|
|
148
146
|
if (op === "$not") {
|
|
149
147
|
const [subop, ...subargs] = node[1];
|
|
150
148
|
const invop = inverse[subop];
|
|
151
149
|
return invop ? [invop, ...subargs] : node;
|
|
152
150
|
}
|
|
153
|
-
if (op === "$any" || op === "$all") {
|
|
154
|
-
const [_, list, elkey, subnode] = node;
|
|
155
|
-
const [subop, elk, val] = subnode;
|
|
156
|
-
return (subop === "$eq" || subop === "$in") && elkey === elk ? [op === "$any" ? "$ovl" : "$ctd", list, subop === "$eq" ? [val] : val] : node;
|
|
157
|
-
}
|
|
158
|
-
if (op === "$has") {
|
|
159
|
-
const [_, list, elkey, limbs] = node;
|
|
160
|
-
return limbs.every(([subop, elk]) => subop === "$eq" && elk === elkey) ? ["$cts", list, limbs.map(([_op, _prop, val]) => val)] : node;
|
|
161
|
-
}
|
|
162
151
|
return node;
|
|
163
152
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
153
|
+
const opSql = {
|
|
154
|
+
$and: `AND`,
|
|
155
|
+
$or: `OR`,
|
|
156
|
+
$not: sql`NOT`,
|
|
157
|
+
$eq: sql`=`,
|
|
158
|
+
$neq: sql`<>`,
|
|
159
|
+
$in: sql`IN`,
|
|
160
|
+
$nin: sql`NOT IN`,
|
|
161
|
+
$lt: sql`<`,
|
|
162
|
+
$lte: sql`<=`,
|
|
163
|
+
$gt: sql`>`,
|
|
164
|
+
$gte: sql`>=`,
|
|
165
|
+
$re: sql`~`,
|
|
166
|
+
$ire: sql`~*`,
|
|
167
|
+
$cts: sql`@>`,
|
|
168
|
+
$ctd: sql`<@`
|
|
169
|
+
};
|
|
170
|
+
function castValue$1(value, type, op) {
|
|
171
|
+
if (value === null && op === "$eq")
|
|
172
|
+
return sql`IS NULL`;
|
|
173
|
+
if (value === null && op === "$neq")
|
|
174
|
+
return sql`IS NOT NULL`;
|
|
175
|
+
const sqlOp = opSql[op];
|
|
176
|
+
if (!sqlOp)
|
|
177
|
+
throw Error("pg.getSql_unknown_operator " + op);
|
|
178
|
+
if (op === "$in" || op === "$nin") {
|
|
179
|
+
return sql`${sqlOp} (${join(value)})`;
|
|
184
180
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return sql`(${lookup2(left, rType)})::${raw(rType)} ${raw(op)} ${right}`;
|
|
181
|
+
if (type === "jsonb") {
|
|
182
|
+
return sql`${sqlOp} ${JSON.stringify(value)}::jsonb`;
|
|
183
|
+
}
|
|
184
|
+
if (type === "cube") {
|
|
185
|
+
if (!Array.isArray(value) || value.length !== 2 || !Array.isArray(value[0])) {
|
|
186
|
+
throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
|
|
192
187
|
}
|
|
188
|
+
return sql`${sqlOp} cube(${join(value)})`;
|
|
193
189
|
}
|
|
190
|
+
return sql`${sqlOp} ${value}`;
|
|
191
|
+
}
|
|
192
|
+
function getSql(filter, options) {
|
|
194
193
|
function getNodeSql(ast) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return binop(">", ast[1], ast[2]);
|
|
210
|
-
case "$gte":
|
|
211
|
-
return binop(">=", ast[1], ast[2]);
|
|
212
|
-
case "$re":
|
|
213
|
-
return binop("~", ast[1], ast[2]);
|
|
214
|
-
case "$ire":
|
|
215
|
-
return binop("~*", ast[1], ast[2]);
|
|
216
|
-
case "$in":
|
|
217
|
-
return sql`${lookup2(ast[1])} IN (${join(ast[2])})`;
|
|
218
|
-
case "$nin":
|
|
219
|
-
return sql`${lookup2(ast[1])} NOT IN (${join(ast[2])})`;
|
|
220
|
-
case "$cts":
|
|
221
|
-
return sql`${lookup2(ast[1])} @> ${ast[2]}`;
|
|
222
|
-
case "$ctd":
|
|
223
|
-
return sql`${lookup2(ast[1])} <@ ${ast[2]}`;
|
|
224
|
-
case "$ovl":
|
|
225
|
-
switch (getColumnType(ast[1])) {
|
|
226
|
-
case "jsonb":
|
|
227
|
-
case "any":
|
|
228
|
-
return sql`${lookup2(ast[1])} ?| ${Array.isArray(ast[2]) ? ast[2] : Object.keys(ast[2])}`;
|
|
229
|
-
case "array":
|
|
230
|
-
return sql`${lookup2(ast[1])} && ${ast[2]}`;
|
|
231
|
-
default:
|
|
232
|
-
throw Error("pg.getSql_ovl_unknown_column_type");
|
|
233
|
-
}
|
|
234
|
-
case "$and":
|
|
235
|
-
return sql`(${join(ast[1].map((node) => getNodeSql(node)), `) AND (`)})`;
|
|
236
|
-
case "$or":
|
|
237
|
-
return sql`(${join(ast[1].map((node) => getNodeSql(node)), `) OR (`)})`;
|
|
238
|
-
case "$not":
|
|
239
|
-
return sql`NOT (${getNodeSql(ast[1])})`;
|
|
240
|
-
case "$any":
|
|
241
|
-
return sql`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
242
|
-
case "$all":
|
|
243
|
-
return sql`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
244
|
-
case "$has":
|
|
245
|
-
return sql`(SELECT bool_or(${join(ast[3].map((node) => getNodeSql(node)), `) AND bool_or(`)}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
246
|
-
default:
|
|
247
|
-
throw Error("pg.getSql_unknown_operator: " + ast[0]);
|
|
194
|
+
if (typeof ast === "boolean")
|
|
195
|
+
return ast;
|
|
196
|
+
const op = ast[0];
|
|
197
|
+
if (op === "$and" || op === "$or") {
|
|
198
|
+
return sql`(${join(ast[1].map((node) => getNodeSql(node)), `) ${opSql[op]} (`)})`;
|
|
199
|
+
} else if (op === "$not") {
|
|
200
|
+
return sql`${opSql[op]} (${getNodeSql(ast[1])})`;
|
|
201
|
+
}
|
|
202
|
+
const [prefix, ...suffix] = encodePath(ast[1]);
|
|
203
|
+
const { types } = options.schema;
|
|
204
|
+
if (!types[prefix])
|
|
205
|
+
throw Error("pg.no_column " + prefix);
|
|
206
|
+
if (suffix.length && types[prefix] !== "jsonb") {
|
|
207
|
+
throw Error("pg.lookup_not_jsonb " + prefix);
|
|
248
208
|
}
|
|
209
|
+
const [lhs, type] = suffix.length ? [sql`"${raw(prefix)}" #> ${suffix}`, "jsonb"] : [sql`"${raw(prefix)}"`, types[prefix]];
|
|
210
|
+
const rhs = castValue$1(ast[2], type, op);
|
|
211
|
+
return sql`${lhs} ${rhs}`;
|
|
249
212
|
}
|
|
250
213
|
return getNodeSql(getAst(filter));
|
|
251
214
|
}
|
|
@@ -263,14 +226,9 @@ const getJsonBuildValue = (value) => {
|
|
|
263
226
|
return sql`${value}::text`;
|
|
264
227
|
return sql`${JSON.stringify(stripAttributes(value))}::jsonb`;
|
|
265
228
|
};
|
|
266
|
-
const lookup = (prop
|
|
229
|
+
const lookup = (prop) => {
|
|
267
230
|
const [prefix, ...suffix] = encodePath(prop);
|
|
268
|
-
|
|
269
|
-
return suffix.length ? sql`"${raw(prefix)}" ${op} ${suffix}` : sql`"${raw(prefix)}"`;
|
|
270
|
-
};
|
|
271
|
-
const getType = (prop) => {
|
|
272
|
-
const [_prefix, ...suffix] = encodePath(prop);
|
|
273
|
-
return suffix.length ? "jsonb" : "any";
|
|
231
|
+
return suffix.length ? sql`"${raw(prefix)}" #> ${suffix}` : sql`"${raw(prefix)}"`;
|
|
274
232
|
};
|
|
275
233
|
const aggSql = {
|
|
276
234
|
$sum: (prop) => sql`sum((${lookup(prop)})::numeric)`,
|
|
@@ -298,19 +256,38 @@ const getSelectCols = (table, projection = null) => {
|
|
|
298
256
|
}
|
|
299
257
|
return sql`jsonb_build_object(${join(sqls, ", ")})`;
|
|
300
258
|
};
|
|
259
|
+
function infJoin(array) {
|
|
260
|
+
return sql`array[${join(array.map((num) => num === Infinity ? sql`'Infinity'` : num === -Infinity ? sql`'-Infinity'` : num))}]::float8[]`;
|
|
261
|
+
}
|
|
262
|
+
function castValue(value, type, name) {
|
|
263
|
+
if (!type)
|
|
264
|
+
throw Error("pg.write_no_column " + name);
|
|
265
|
+
if (value instanceof Sql)
|
|
266
|
+
return value;
|
|
267
|
+
if (value === null)
|
|
268
|
+
return sql`NULL`;
|
|
269
|
+
if (type === "jsonb") {
|
|
270
|
+
return value.$put ? JSON.stringify(stripAttributes(value)) : sql`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})`;
|
|
271
|
+
}
|
|
272
|
+
if (type === "cube") {
|
|
273
|
+
if (!Array.isArray(value) || !value.length || Array.isArray(value[0]) && value.length !== 2) {
|
|
274
|
+
throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
|
|
275
|
+
}
|
|
276
|
+
return Array.isArray(value[0]) ? sql`cube(${infJoin(value[0])}, ${infJoin(value[1])})` : sql`cube(${infJoin(value)})`;
|
|
277
|
+
}
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
301
280
|
const getInsert = (row, options) => {
|
|
302
281
|
const cols = [];
|
|
303
282
|
const vals = [];
|
|
304
|
-
Object.entries(row).filter(([
|
|
283
|
+
Object.entries(row).filter(([col]) => col !== options.verCol && col[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
|
|
305
284
|
cols.push(sql`"${raw(col)}"`);
|
|
306
|
-
vals.push(val
|
|
285
|
+
vals.push(castValue(val, options.schema.types[col], col));
|
|
307
286
|
});
|
|
308
287
|
return { cols: join(cols, ", "), vals: join(vals, ", ") };
|
|
309
288
|
};
|
|
310
289
|
const getUpdates = (row, options) => {
|
|
311
|
-
return join(Object.entries(row).filter(([
|
|
312
|
-
return sql`"${raw(name)}" = ${value instanceof Sql || typeof value !== "object" || !value ? value : !value.$put ? sql`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})` : sql`${JSON.stringify(stripAttributes(value))}::jsonb`}`;
|
|
313
|
-
}).concat(sql`"${raw(options.verCol)}" = ${nowTimestamp}`), ", ");
|
|
290
|
+
return join(Object.entries(row).filter(([col]) => col !== options.idCol && col[0] !== "$").map(([col, val]) => sql`"${raw(col)}" = ${castValue(val, options.schema.types[col], col)}`).concat(sql`"${raw(options.verCol)}" = ${nowTimestamp}`), ", ");
|
|
314
291
|
};
|
|
315
292
|
function getJsonUpdate(_a, col, path) {
|
|
316
293
|
var _b = _a, { $put } = _b, object = __objRest(_b, ["$put"]);
|
|
@@ -358,13 +335,13 @@ function getArgSql(_c, options) {
|
|
|
358
335
|
throw Error("pg_arg.order_and_group_unsupported in " + prefix);
|
|
359
336
|
}
|
|
360
337
|
const meta = (key2) => $group ? getAggMeta(key2, $group) : getArgMeta(key2, prefix, idCol);
|
|
361
|
-
const groupCols = Array.isArray($group) && $group.length
|
|
338
|
+
const groupCols = Array.isArray($group) && $group.length && $group.map(lookup);
|
|
362
339
|
const group = groupCols ? join(groupCols, ", ") : void 0;
|
|
363
340
|
const hasRangeArg = $before || $after || $since || $until || $first || $last || $all || $order;
|
|
364
341
|
let key;
|
|
365
342
|
const where = [];
|
|
366
343
|
if (!isEmpty(filter)) {
|
|
367
|
-
where.push(getSql(filter,
|
|
344
|
+
where.push(getSql(filter, options));
|
|
368
345
|
key = sql`${JSON.stringify(filter)}::jsonb`;
|
|
369
346
|
}
|
|
370
347
|
if (!hasRangeArg)
|
|
@@ -525,24 +502,46 @@ class Db {
|
|
|
525
502
|
}
|
|
526
503
|
}
|
|
527
504
|
async readSql(sql2) {
|
|
528
|
-
|
|
529
|
-
result = result.rows.flat();
|
|
505
|
+
const result = (await this.query(sql2)).rows.flat();
|
|
530
506
|
log("Read result", result);
|
|
531
507
|
return result;
|
|
532
508
|
}
|
|
533
509
|
async writeSql(sql2) {
|
|
534
|
-
|
|
510
|
+
const res = await this.query(sql2);
|
|
535
511
|
log("Rows written", res.rowCount);
|
|
536
512
|
if (!res.rowCount) {
|
|
537
513
|
throw Error("pg.nothing_written " + sql2.text + " with " + sql2.values);
|
|
538
514
|
}
|
|
539
515
|
return res.rows[0][0];
|
|
540
516
|
}
|
|
517
|
+
async ensureSchema(tableOptions) {
|
|
518
|
+
if (tableOptions.schema)
|
|
519
|
+
return;
|
|
520
|
+
const { table } = tableOptions;
|
|
521
|
+
const types = (await this.query(sql`SELECT jsonb_object_agg(
|
|
522
|
+
column_name,
|
|
523
|
+
udt_name
|
|
524
|
+
)
|
|
525
|
+
FROM information_schema.columns
|
|
526
|
+
WHERE
|
|
527
|
+
table_name = ${table} AND
|
|
528
|
+
table_schema = (
|
|
529
|
+
SELECT table_schema
|
|
530
|
+
FROM information_schema.tables
|
|
531
|
+
WHERE table_name = ${table}
|
|
532
|
+
ORDER BY array_position(current_schemas(false)::text[], table_schema::text) ASC
|
|
533
|
+
LIMIT 1)`)).rows[0][0];
|
|
534
|
+
if (!types)
|
|
535
|
+
throw Error(`pg.missing_table ${table}`);
|
|
536
|
+
log("ensureSchema", types);
|
|
537
|
+
tableOptions.schema = { types };
|
|
538
|
+
}
|
|
541
539
|
async read(rootQuery, tableOptions) {
|
|
542
540
|
const idQueries = {};
|
|
543
541
|
const promises = [];
|
|
544
542
|
const results = [];
|
|
545
543
|
const { prefix } = tableOptions;
|
|
544
|
+
await this.ensureSchema(tableOptions);
|
|
546
545
|
const getByArgs = async (args, projection) => {
|
|
547
546
|
const result = await this.readSql(selectByArgs(args, projection, tableOptions));
|
|
548
547
|
const wrappedGraph = encodeGraph(wrapObject(result, prefix));
|
|
@@ -583,6 +582,7 @@ class Db {
|
|
|
583
582
|
}
|
|
584
583
|
async write(rootChange, tableOptions) {
|
|
585
584
|
const { prefix } = tableOptions;
|
|
585
|
+
await this.ensureSchema(tableOptions);
|
|
586
586
|
const change = unwrap(rootChange, prefix);
|
|
587
587
|
const sqls = change.map((node) => {
|
|
588
588
|
const arg = decodeArgs(node);
|
|
@@ -610,7 +610,7 @@ class Db {
|
|
|
610
610
|
return result;
|
|
611
611
|
}
|
|
612
612
|
}
|
|
613
|
-
const pg = ({ table, idCol, verCol, connection }) => (store) => {
|
|
613
|
+
const pg = ({ table, idCol, verCol, connection, schema }) => (store) => {
|
|
614
614
|
store.on("read", read);
|
|
615
615
|
store.on("write", write);
|
|
616
616
|
const prefix = store.path;
|
|
@@ -618,7 +618,8 @@ const pg = ({ table, idCol, verCol, connection }) => (store) => {
|
|
|
618
618
|
prefix,
|
|
619
619
|
table: table || prefix[prefix.length - 1] || "default",
|
|
620
620
|
idCol: idCol || "id",
|
|
621
|
-
verCol: verCol || "updatedAt"
|
|
621
|
+
verCol: verCol || "updatedAt",
|
|
622
|
+
schema
|
|
622
623
|
};
|
|
623
624
|
const defaultDb = new Db(connection);
|
|
624
625
|
function read(query, options, next) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@graffy/pg",
|
|
3
3
|
"description": "The standard Postgres module for Graffy. Each instance this module mounts a Postgres table as a Graffy subtree.",
|
|
4
4
|
"author": "aravind (https://github.com/aravindet)",
|
|
5
|
-
"version": "0.15.
|
|
5
|
+
"version": "0.15.14-alpha.1",
|
|
6
6
|
"main": "./index.cjs",
|
|
7
7
|
"exports": {
|
|
8
8
|
"import": "./index.mjs",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@graffy/common": "0.15.
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
"@graffy/common": "0.15.14-alpha.1",
|
|
20
|
+
"sql-template-tag": "^4.0.0",
|
|
21
|
+
"debug": "^4.3.2"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"pg": "^8.0.0"
|
package/types/Db.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export default class Db {
|
|
|
4
4
|
query(sql: any): Promise<any>;
|
|
5
5
|
readSql(sql: any): Promise<any>;
|
|
6
6
|
writeSql(sql: any): Promise<any>;
|
|
7
|
+
ensureSchema(tableOptions: any): Promise<void>;
|
|
7
8
|
read(rootQuery: any, tableOptions: any): Promise<any>;
|
|
8
9
|
write(rootChange: any, tableOptions: any): Promise<any[]>;
|
|
9
10
|
}
|
package/types/filter/getSql.d.ts
CHANGED
package/types/index.d.ts
CHANGED
package/types/sql/clauses.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export const nowTimestamp: Sql;
|
|
2
2
|
export function getJsonBuildObject(variadic: any): Sql;
|
|
3
|
-
export function lookup(prop: any
|
|
4
|
-
export function getType(prop: any): "any" | "jsonb";
|
|
3
|
+
export function lookup(prop: any): Sql;
|
|
5
4
|
export function getSelectCols(table: any, projection?: any): Sql;
|
|
6
5
|
export function getInsert(row: any, options: any): {
|
|
7
6
|
cols: Sql;
|