@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.
- package/esm/keyword-exprs.js +92 -0
- package/esm/query-builder.js +15 -3
- package/keyword-exprs.d.ts +2 -0
- package/keyword-exprs.js +95 -0
- package/package.json +5 -3
- package/query-builder.js +15 -3
|
@@ -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
|
+
}
|
package/esm/query-builder.js
CHANGED
|
@@ -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) =>
|
|
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 =
|
|
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 =
|
|
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({
|
package/keyword-exprs.js
ADDED
|
@@ -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.
|
|
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": "
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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({
|