@constructive-io/query-builder 3.10.4 → 3.10.6

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.
@@ -0,0 +1,92 @@
1
+ import { keywordKindOf } from '@pgsql/quotes';
2
+ import { nodes } from 'pg-ast';
3
+ function requireArgCount(keyword, args, min, max) {
4
+ if (args.length >= min && (max === undefined || args.length <= max))
5
+ return;
6
+ const expected = max === undefined
7
+ ? `at least ${min}`
8
+ : min === max
9
+ ? `exactly ${min}`
10
+ : `between ${min} and ${max}`;
11
+ throw new Error(`${keyword} requires ${expected} argument(s), received ${args.length}.`);
12
+ }
13
+ const KEYWORD_EXPR_BUILDERS = {
14
+ coalesce: (args) => {
15
+ requireArgCount('COALESCE', args, 1);
16
+ return nodes.coalesceExpr({ args: args });
17
+ },
18
+ nullif: (args) => {
19
+ requireArgCount('NULLIF', args, 2, 2);
20
+ return nodes.nullIfExpr({ args: args });
21
+ },
22
+ greatest: (args) => {
23
+ requireArgCount('GREATEST', args, 1);
24
+ return nodes.minMaxExpr({ args: args, op: 'IS_GREATEST' });
25
+ },
26
+ least: (args) => {
27
+ requireArgCount('LEAST', args, 1);
28
+ return nodes.minMaxExpr({ args: args, op: 'IS_LEAST' });
29
+ }
30
+ };
31
+ // Keyword names that really are functions in pg_catalog, so the quoted call the
32
+ // deparser emits resolves and runs (verified against PostgreSQL 18 by
33
+ // intersecting pg_proc with the COL_NAME/RESERVED keyword lists). These stay
34
+ // FuncCall. Note that the function forms of EXTRACT/POSITION/SUBSTRING take
35
+ // their arguments positionally, which is not always the argument order the
36
+ // grammar uses: `position(a, b)` is `POSITION(b IN a)`.
37
+ const KEYWORD_FUNCTIONS = new Set([
38
+ 'bit',
39
+ 'char',
40
+ 'current_user',
41
+ 'extract',
42
+ 'interval',
43
+ 'json_object',
44
+ 'normalize',
45
+ 'numeric',
46
+ 'overlay',
47
+ 'position',
48
+ 'session_user',
49
+ 'substring',
50
+ 'system_user',
51
+ 'time',
52
+ 'timestamp',
53
+ 'varchar',
54
+ 'xmlexists'
55
+ ]);
56
+ // Keyword constructs with neither a node mapping nor a function behind them,
57
+ // with the call to reach for instead.
58
+ const KEYWORD_ALTERNATIVES = {
59
+ trim: `use fn('btrim'), fn('ltrim') or fn('rtrim')`,
60
+ cast: `use cast(expr, 'type')`,
61
+ collate: `no builder equivalent; select the collated expression from a view`
62
+ };
63
+ function isNamedArg(node) {
64
+ return !!node && typeof node === 'object' && 'NamedArgExpr' in node;
65
+ }
66
+ // The node for a keyword-expression call, or undefined when `name` is an
67
+ // ordinary function name. Throws when the name is a keyword the builder has no
68
+ // node for and PostgreSQL has no function for, or when a keyword expression is
69
+ // used where only a function is legal.
70
+ export function keywordExprNode(name, args, position) {
71
+ const lower = name.toLowerCase();
72
+ const build = KEYWORD_EXPR_BUILDERS[lower];
73
+ const keyword = lower.toUpperCase();
74
+ if (build) {
75
+ if (position === 'from') {
76
+ throw new Error(`${keyword}(...) is an expression, not a set-returning function, so it cannot appear in FROM. Select it as a computed column instead, e.g. .selectExpr('value', fn('${lower}', [...])).`);
77
+ }
78
+ if (args.some(isNamedArg)) {
79
+ throw new Error(`${keyword}(...) does not accept named arguments; pass them positionally, e.g. fn('${lower}', [col('a'), col('b')]).`);
80
+ }
81
+ return build(args);
82
+ }
83
+ if (KEYWORD_FUNCTIONS.has(lower))
84
+ return undefined;
85
+ const kind = keywordKindOf(lower);
86
+ if (kind === 'COL_NAME_KEYWORD' || kind === 'RESERVED_KEYWORD') {
87
+ const alternative = KEYWORD_ALTERNATIVES[lower] ??
88
+ `use an equivalent regular function, or pass the schema explicitly (e.g. { schema: 'pg_catalog' }) if a function with this name really exists`;
89
+ throw new Error(`'${name}' is a PostgreSQL ${kind}: ${keyword} is grammar, not a function, so the call would deparse to "${lower}"(...) and fail at runtime with "function ${lower}(...) does not exist". Instead, ${alternative}.`);
90
+ }
91
+ return undefined;
92
+ }
@@ -1,5 +1,6 @@
1
1
  import { ast, nodes } from 'pg-ast';
2
2
  import { deparseSync } from 'pgsql-deparser';
3
+ import { keywordExprNode } from './keyword-exprs';
3
4
  // ---------------------------------------------------------------------------
4
5
  // AST helpers (thin wrappers for readability)
5
6
  // ---------------------------------------------------------------------------
@@ -92,6 +93,17 @@ function funcCallNode(name, args, schema) {
92
93
  args: args,
93
94
  });
94
95
  }
96
+ // A call by name: a FuncCall, or the dedicated node for the keyword-expression
97
+ // family (COALESCE, NULLIF, GREATEST, LEAST). A schema-qualified name is always
98
+ // a real function call and is never remapped.
99
+ function callNode(name, args, schema, position = 'expression') {
100
+ if (!schema) {
101
+ const keywordNode = keywordExprNode(name, args, position);
102
+ if (keywordNode)
103
+ return keywordNode;
104
+ }
105
+ return funcCallNode(name, args, schema);
106
+ }
95
107
  function namedArgNode(name, val) {
96
108
  return nodes.namedArgExpr({
97
109
  arg: val,
@@ -136,7 +148,7 @@ export function fn(name, args, opts) {
136
148
  schema = name.slice(0, idx);
137
149
  fname = name.slice(idx + 1);
138
150
  }
139
- return (alloc) => funcCallNode(fname, buildFnArgNodes(args, alloc), schema);
151
+ return (alloc) => callNode(fname, buildFnArgNodes(args, alloc), schema);
140
152
  }
141
153
  function operandToNode(v, alloc) {
142
154
  return isExpr(v) ? v(alloc) : alloc(v);
@@ -726,7 +738,7 @@ export class QueryBuilder {
726
738
  return [result];
727
739
  }
728
740
  _rangeFunctionNode(spec) {
729
- const funcNode = funcCallNode(spec.name, buildFnArgNodes(spec.args, this._alloc), spec.schema);
741
+ const funcNode = callNode(spec.name, buildFnArgNodes(spec.args, this._alloc), spec.schema, 'from');
730
742
  return nodes.rangeFunction({
731
743
  functions: [nodes.list({ items: [funcNode] })],
732
744
  lateral: false,
@@ -893,7 +905,7 @@ export class QueryBuilder {
893
905
  this._resetParams();
894
906
  const fc = this._funcCall;
895
907
  const funcArgs = buildFnArgNodes(fc.args, this._alloc);
896
- const funcNode = funcCallNode(fc.name, funcArgs, fc.schema);
908
+ const funcNode = callNode(fc.name, funcArgs, fc.schema, this._selectItems.length > 0 ? 'from' : 'expression');
897
909
  if (this._selectItems.length > 0) {
898
910
  const targetList = this._buildTargetList(this._selectItems);
899
911
  const rangeFunc = nodes.rangeFunction({
@@ -0,0 +1,2 @@
1
+ export type CallPosition = 'expression' | 'from';
2
+ export declare function keywordExprNode(name: string, args: unknown[], position: CallPosition): unknown | undefined;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keywordExprNode = keywordExprNode;
4
+ const quotes_1 = require("@pgsql/quotes");
5
+ const pg_ast_1 = require("pg-ast");
6
+ function requireArgCount(keyword, args, min, max) {
7
+ if (args.length >= min && (max === undefined || args.length <= max))
8
+ return;
9
+ const expected = max === undefined
10
+ ? `at least ${min}`
11
+ : min === max
12
+ ? `exactly ${min}`
13
+ : `between ${min} and ${max}`;
14
+ throw new Error(`${keyword} requires ${expected} argument(s), received ${args.length}.`);
15
+ }
16
+ const KEYWORD_EXPR_BUILDERS = {
17
+ coalesce: (args) => {
18
+ requireArgCount('COALESCE', args, 1);
19
+ return pg_ast_1.nodes.coalesceExpr({ args: args });
20
+ },
21
+ nullif: (args) => {
22
+ requireArgCount('NULLIF', args, 2, 2);
23
+ return pg_ast_1.nodes.nullIfExpr({ args: args });
24
+ },
25
+ greatest: (args) => {
26
+ requireArgCount('GREATEST', args, 1);
27
+ return pg_ast_1.nodes.minMaxExpr({ args: args, op: 'IS_GREATEST' });
28
+ },
29
+ least: (args) => {
30
+ requireArgCount('LEAST', args, 1);
31
+ return pg_ast_1.nodes.minMaxExpr({ args: args, op: 'IS_LEAST' });
32
+ }
33
+ };
34
+ // Keyword names that really are functions in pg_catalog, so the quoted call the
35
+ // deparser emits resolves and runs (verified against PostgreSQL 18 by
36
+ // intersecting pg_proc with the COL_NAME/RESERVED keyword lists). These stay
37
+ // FuncCall. Note that the function forms of EXTRACT/POSITION/SUBSTRING take
38
+ // their arguments positionally, which is not always the argument order the
39
+ // grammar uses: `position(a, b)` is `POSITION(b IN a)`.
40
+ const KEYWORD_FUNCTIONS = new Set([
41
+ 'bit',
42
+ 'char',
43
+ 'current_user',
44
+ 'extract',
45
+ 'interval',
46
+ 'json_object',
47
+ 'normalize',
48
+ 'numeric',
49
+ 'overlay',
50
+ 'position',
51
+ 'session_user',
52
+ 'substring',
53
+ 'system_user',
54
+ 'time',
55
+ 'timestamp',
56
+ 'varchar',
57
+ 'xmlexists'
58
+ ]);
59
+ // Keyword constructs with neither a node mapping nor a function behind them,
60
+ // with the call to reach for instead.
61
+ const KEYWORD_ALTERNATIVES = {
62
+ trim: `use fn('btrim'), fn('ltrim') or fn('rtrim')`,
63
+ cast: `use cast(expr, 'type')`,
64
+ collate: `no builder equivalent; select the collated expression from a view`
65
+ };
66
+ function isNamedArg(node) {
67
+ return !!node && typeof node === 'object' && 'NamedArgExpr' in node;
68
+ }
69
+ // The node for a keyword-expression call, or undefined when `name` is an
70
+ // ordinary function name. Throws when the name is a keyword the builder has no
71
+ // node for and PostgreSQL has no function for, or when a keyword expression is
72
+ // used where only a function is legal.
73
+ function keywordExprNode(name, args, position) {
74
+ const lower = name.toLowerCase();
75
+ const build = KEYWORD_EXPR_BUILDERS[lower];
76
+ const keyword = lower.toUpperCase();
77
+ if (build) {
78
+ if (position === 'from') {
79
+ throw new Error(`${keyword}(...) is an expression, not a set-returning function, so it cannot appear in FROM. Select it as a computed column instead, e.g. .selectExpr('value', fn('${lower}', [...])).`);
80
+ }
81
+ if (args.some(isNamedArg)) {
82
+ throw new Error(`${keyword}(...) does not accept named arguments; pass them positionally, e.g. fn('${lower}', [col('a'), col('b')]).`);
83
+ }
84
+ return build(args);
85
+ }
86
+ if (KEYWORD_FUNCTIONS.has(lower))
87
+ return undefined;
88
+ const kind = (0, quotes_1.keywordKindOf)(lower);
89
+ if (kind === 'COL_NAME_KEYWORD' || kind === 'RESERVED_KEYWORD') {
90
+ const alternative = KEYWORD_ALTERNATIVES[lower] ??
91
+ `use an equivalent regular function, or pass the schema explicitly (e.g. { schema: 'pg_catalog' }) if a function with this name really exists`;
92
+ throw new Error(`'${name}' is a PostgreSQL ${kind}: ${keyword} is grammar, not a function, so the call would deparse to "${lower}"(...) and fail at runtime with "function ${lower}(...) does not exist". Instead, ${alternative}.`);
93
+ }
94
+ return undefined;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/query-builder",
3
- "version": "3.10.4",
3
+ "version": "3.10.6",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PostgreSQL Query Builder",
6
6
  "main": "index.js",
@@ -36,12 +36,14 @@
36
36
  "database"
37
37
  ],
38
38
  "devDependencies": {
39
- "makage": "^0.3.0"
39
+ "makage": "^0.3.0",
40
+ "pgsql-test": "^5.11.1"
40
41
  },
41
42
  "dependencies": {
43
+ "@pgsql/quotes": "^18.2.4",
42
44
  "pg-ast": "^3.10.4",
43
45
  "pgsql-deparser": "^18.3.6",
44
46
  "query-spec": "^0.11.0"
45
47
  },
46
- "gitHead": "d0d796b7eb0d16c8c19405a49c0ca62ce3df117b"
48
+ "gitHead": "ce544846eafd96280fac2607f873960392ae2c89"
47
49
  }
package/query-builder.js CHANGED
@@ -13,6 +13,7 @@ exports.or = or;
13
13
  exports.not = not;
14
14
  const pg_ast_1 = require("pg-ast");
15
15
  const pgsql_deparser_1 = require("pgsql-deparser");
16
+ const keyword_exprs_1 = require("./keyword-exprs");
16
17
  // ---------------------------------------------------------------------------
17
18
  // AST helpers (thin wrappers for readability)
18
19
  // ---------------------------------------------------------------------------
@@ -105,6 +106,17 @@ function funcCallNode(name, args, schema) {
105
106
  args: args,
106
107
  });
107
108
  }
109
+ // A call by name: a FuncCall, or the dedicated node for the keyword-expression
110
+ // family (COALESCE, NULLIF, GREATEST, LEAST). A schema-qualified name is always
111
+ // a real function call and is never remapped.
112
+ function callNode(name, args, schema, position = 'expression') {
113
+ if (!schema) {
114
+ const keywordNode = (0, keyword_exprs_1.keywordExprNode)(name, args, position);
115
+ if (keywordNode)
116
+ return keywordNode;
117
+ }
118
+ return funcCallNode(name, args, schema);
119
+ }
108
120
  function namedArgNode(name, val) {
109
121
  return pg_ast_1.nodes.namedArgExpr({
110
122
  arg: val,
@@ -149,7 +161,7 @@ function fn(name, args, opts) {
149
161
  schema = name.slice(0, idx);
150
162
  fname = name.slice(idx + 1);
151
163
  }
152
- return (alloc) => funcCallNode(fname, buildFnArgNodes(args, alloc), schema);
164
+ return (alloc) => callNode(fname, buildFnArgNodes(args, alloc), schema);
153
165
  }
154
166
  function operandToNode(v, alloc) {
155
167
  return isExpr(v) ? v(alloc) : alloc(v);
@@ -739,7 +751,7 @@ class QueryBuilder {
739
751
  return [result];
740
752
  }
741
753
  _rangeFunctionNode(spec) {
742
- const funcNode = funcCallNode(spec.name, buildFnArgNodes(spec.args, this._alloc), spec.schema);
754
+ const funcNode = callNode(spec.name, buildFnArgNodes(spec.args, this._alloc), spec.schema, 'from');
743
755
  return pg_ast_1.nodes.rangeFunction({
744
756
  functions: [pg_ast_1.nodes.list({ items: [funcNode] })],
745
757
  lateral: false,
@@ -906,7 +918,7 @@ class QueryBuilder {
906
918
  this._resetParams();
907
919
  const fc = this._funcCall;
908
920
  const funcArgs = buildFnArgNodes(fc.args, this._alloc);
909
- const funcNode = funcCallNode(fc.name, funcArgs, fc.schema);
921
+ const funcNode = callNode(fc.name, funcArgs, fc.schema, this._selectItems.length > 0 ? 'from' : 'expression');
910
922
  if (this._selectItems.length > 0) {
911
923
  const targetList = this._buildTargetList(this._selectItems);
912
924
  const rangeFunc = pg_ast_1.nodes.rangeFunction({