@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 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
- } else if (op === "$all" || op === "$any") {
127
- node[3] = simplify(node[3]);
128
- } else if (op === "$has") {
129
- node[3] = node[3].map((subnode) => simplify(subnode));
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
- if (!node[1].length)
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
- function defaultColumnType(_) {
173
- return "any";
174
- }
175
- function getCompatibleTypes(value) {
176
- if (value === null)
177
- return "any";
178
- if (Array.isArray(value))
179
- return "array";
180
- if (typeof value === "object")
181
- return "jsonb";
182
- if (typeof value === "number")
183
- return "numeric";
184
- if (typeof value === "string")
185
- return "text";
186
- }
187
- function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
188
- function lookup2(string, type) {
189
- if (string.substr(0, 3) === "el$")
190
- return sql__default["default"]`"${sql.raw(string)}"`;
191
- return getLookupSql(string, type);
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
- function binop(op, left, right) {
194
- const lType = left.substr(0, 3) === "el$" ? "any" : getColumnType(left);
195
- const rType = getCompatibleTypes(right);
196
- if (lType === "any" || rType === "any" || rType === lType) {
197
- return sql__default["default"]`${lookup2(left)} ${sql.raw(op)} ${right}`;
198
- } else {
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
- switch (ast[0]) {
204
- case "$eq":
205
- if (ast[2] === null)
206
- return sql__default["default"]`${lookup2(ast[1])} IS NULL`;
207
- return binop("=", ast[1], ast[2]);
208
- case "$neq":
209
- if (ast[2] === null)
210
- return sql__default["default"]`${lookup2(ast[1])} IS NOT NULL`;
211
- return binop("<>", ast[1], ast[2]);
212
- case "$lt":
213
- return binop("<", ast[1], ast[2]);
214
- case "$lte":
215
- return binop("<=", ast[1], ast[2]);
216
- case "$gt":
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, type) => {
237
+ const lookup = (prop) => {
275
238
  const [prefix, ...suffix] = common.encodePath(prop);
276
- const op = type === "text" ? sql__default["default"]`#>>` : sql__default["default"]`#>`;
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(([name]) => name !== options.verCol && name[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
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 instanceof sql.Sql || typeof val !== "object" || !val ? val : sql__default["default"]`${JSON.stringify(stripAttributes(val))}::jsonb`);
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(([name]) => name !== options.idCol && name[0] !== "$").map(([name, value]) => {
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 ? $group.map((col) => lookup(col)) : void 0;
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, lookup, getType));
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
- let result = await this.query(sql2);
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
- let res = await this.query(sql2);
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 { isEmpty, encodePath, isPlainObject, unwrap, decodeArgs, decodeQuery, slice, finalize, wrap, isRange, decodeGraph, mergeObject, merge, encodeGraph, wrapObject, remove } from "@graffy/common";
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
- } else if (op === "$all" || op === "$any") {
119
- node[3] = simplify(node[3]);
120
- } else if (op === "$has") {
121
- node[3] = node[3].map((subnode) => simplify(subnode));
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
- if (!node[1].length)
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
- function defaultColumnType(_) {
165
- return "any";
166
- }
167
- function getCompatibleTypes(value) {
168
- if (value === null)
169
- return "any";
170
- if (Array.isArray(value))
171
- return "array";
172
- if (typeof value === "object")
173
- return "jsonb";
174
- if (typeof value === "number")
175
- return "numeric";
176
- if (typeof value === "string")
177
- return "text";
178
- }
179
- function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
180
- function lookup2(string, type) {
181
- if (string.substr(0, 3) === "el$")
182
- return sql`"${raw(string)}"`;
183
- return getLookupSql(string, type);
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
- function binop(op, left, right) {
186
- const lType = left.substr(0, 3) === "el$" ? "any" : getColumnType(left);
187
- const rType = getCompatibleTypes(right);
188
- if (lType === "any" || rType === "any" || rType === lType) {
189
- return sql`${lookup2(left)} ${raw(op)} ${right}`;
190
- } else {
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
- switch (ast[0]) {
196
- case "$eq":
197
- if (ast[2] === null)
198
- return sql`${lookup2(ast[1])} IS NULL`;
199
- return binop("=", ast[1], ast[2]);
200
- case "$neq":
201
- if (ast[2] === null)
202
- return sql`${lookup2(ast[1])} IS NOT NULL`;
203
- return binop("<>", ast[1], ast[2]);
204
- case "$lt":
205
- return binop("<", ast[1], ast[2]);
206
- case "$lte":
207
- return binop("<=", ast[1], ast[2]);
208
- case "$gt":
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, type) => {
229
+ const lookup = (prop) => {
267
230
  const [prefix, ...suffix] = encodePath(prop);
268
- const op = type === "text" ? sql`#>>` : sql`#>`;
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(([name]) => name !== options.verCol && name[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
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 instanceof Sql || typeof val !== "object" || !val ? val : sql`${JSON.stringify(stripAttributes(val))}::jsonb`);
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(([name]) => name !== options.idCol && name[0] !== "$").map(([name, value]) => {
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 ? $group.map((col) => lookup(col)) : void 0;
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, lookup, getType));
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
- let result = await this.query(sql2);
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
- let res = await this.query(sql2);
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.13",
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.13",
20
- "debug": "^4.3.2",
21
- "sql-template-tag": "^4.0.0"
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
  }
@@ -1,3 +1 @@
1
- export default function getSql(filter: any, getLookupSql: any, getColumnType?: typeof defaultColumnType): any;
2
- declare function defaultColumnType(_: any): string;
3
- export {};
1
+ export default function getSql(filter: any, options: any): any;
package/types/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export function pg({ table, idCol, verCol, connection }: {
1
+ export function pg({ table, idCol, verCol, connection, schema }: {
2
2
  table?: string;
3
3
  idCol?: string;
4
4
  verCol?: string;
5
5
  connection?: any;
6
+ schema?: any;
6
7
  }): (store: any) => void;
@@ -1,7 +1,6 @@
1
1
  export const nowTimestamp: Sql;
2
2
  export function getJsonBuildObject(variadic: any): Sql;
3
- export function lookup(prop: any, type: any): Sql;
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;