@constructive-io/query-builder 3.10.6 → 3.11.0
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/README.md +21 -1
- package/esm/query-builder.js +36 -0
- package/index.d.ts +1 -1
- package/package.json +2 -2
- package/query-builder.d.ts +8 -0
- package/query-builder.js +36 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ npm install @constructive-io/query-builder
|
|
|
31
31
|
- Fluent, chainable API
|
|
32
32
|
- SDK-style JSON filters for `WHERE`/`HAVING` (`{ status: { equalTo: 'active' } }`), matching the generated ORM/SDK filter grammar
|
|
33
33
|
- Composable expressions (`col()`, `fn()`, `add()`, `eq()`, ...) usable in SELECT, WHERE, SET, ON CONFLICT, and RETURNING
|
|
34
|
-
- Full support for: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `JOIN`, `CTE`, `ON CONFLICT`, `RETURNING`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`/`OFFSET`, `DISTINCT`, function/procedure calls
|
|
34
|
+
- Full support for: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `JOIN`, `CTE`, `ON CONFLICT`, `RETURNING`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`/`OFFSET`, `FOR UPDATE`/`FOR SHARE`, `DISTINCT`, function/procedure calls
|
|
35
35
|
|
|
36
36
|
## Usage
|
|
37
37
|
|
|
@@ -509,6 +509,25 @@ new QueryBuilder()
|
|
|
509
509
|
.limit(10)
|
|
510
510
|
.offset(20)
|
|
511
511
|
.build();
|
|
512
|
+
|
|
513
|
+
// FOR UPDATE — lock the row a read-then-write is about to change
|
|
514
|
+
new QueryBuilder()
|
|
515
|
+
.schema('app_public')
|
|
516
|
+
.table('runs')
|
|
517
|
+
.select(['id', 'status'])
|
|
518
|
+
.where({ id: { equalTo: runId } })
|
|
519
|
+
.lock()
|
|
520
|
+
.build();
|
|
521
|
+
|
|
522
|
+
// FOR UPDATE SKIP LOCKED — a queue claim moves on rather than waiting
|
|
523
|
+
new QueryBuilder()
|
|
524
|
+
.table('jobs')
|
|
525
|
+
.select(['id'])
|
|
526
|
+
.where({ status: { equalTo: 'pending' } })
|
|
527
|
+
.orderBy('created_at', 'ASC')
|
|
528
|
+
.limit(1)
|
|
529
|
+
.lock('update', { skipLocked: true })
|
|
530
|
+
.build();
|
|
512
531
|
```
|
|
513
532
|
|
|
514
533
|
## API
|
|
@@ -535,6 +554,7 @@ new QueryBuilder()
|
|
|
535
554
|
| `.having(...predicates)` | HAVING clause (JSON filters and/or expressions) |
|
|
536
555
|
| `.limit(n)` | LIMIT clause |
|
|
537
556
|
| `.offset(n)` | OFFSET clause |
|
|
557
|
+
| `.lock(strength?, opts?)` | Row lock: `'update'` (default), `'noKeyUpdate'`, `'share'`, `'keyShare'`; `opts: { skipLocked?, noWait? }` |
|
|
538
558
|
| `.distinct()` | SELECT DISTINCT |
|
|
539
559
|
| `.returning(columns)` | RETURNING clause (column names or `{ expr, as }` items) |
|
|
540
560
|
| `.onConflict(opts)` | ON CONFLICT (DO NOTHING or DO UPDATE) |
|
package/esm/query-builder.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { ast, nodes } from 'pg-ast';
|
|
2
2
|
import { deparseSync } from 'pgsql-deparser';
|
|
3
3
|
import { keywordExprNode } from './keyword-exprs';
|
|
4
|
+
const LOCK_STRENGTHS = {
|
|
5
|
+
update: 'LCS_FORUPDATE',
|
|
6
|
+
noKeyUpdate: 'LCS_FORNOKEYUPDATE',
|
|
7
|
+
share: 'LCS_FORSHARE',
|
|
8
|
+
keyShare: 'LCS_FORKEYSHARE',
|
|
9
|
+
};
|
|
4
10
|
// ---------------------------------------------------------------------------
|
|
5
11
|
// AST helpers (thin wrappers for readability)
|
|
6
12
|
// ---------------------------------------------------------------------------
|
|
@@ -268,6 +274,7 @@ export class QueryBuilder {
|
|
|
268
274
|
_ctes = [];
|
|
269
275
|
_onConflict;
|
|
270
276
|
_funcCall;
|
|
277
|
+
_lock;
|
|
271
278
|
// -------------------------------------------------------------------------
|
|
272
279
|
// Schema / Table
|
|
273
280
|
// -------------------------------------------------------------------------
|
|
@@ -392,6 +399,18 @@ export class QueryBuilder {
|
|
|
392
399
|
this._offsetValue = n;
|
|
393
400
|
return this;
|
|
394
401
|
}
|
|
402
|
+
// Row-level lock on the rows this SELECT reads:
|
|
403
|
+
// .lock() -> FOR UPDATE
|
|
404
|
+
// .lock('share') -> FOR SHARE
|
|
405
|
+
// .lock('update', { skipLocked: true }) -> FOR UPDATE SKIP LOCKED
|
|
406
|
+
// .lock('update', { noWait: true }) -> FOR UPDATE NOWAIT
|
|
407
|
+
lock(strength = 'update', opts) {
|
|
408
|
+
if (opts?.skipLocked && opts?.noWait) {
|
|
409
|
+
throw new Error('A lock either skips locked rows or refuses to wait for them, not both.');
|
|
410
|
+
}
|
|
411
|
+
this._lock = { strength, opts };
|
|
412
|
+
return this;
|
|
413
|
+
}
|
|
395
414
|
// -------------------------------------------------------------------------
|
|
396
415
|
// RETURNING
|
|
397
416
|
// -------------------------------------------------------------------------
|
|
@@ -456,6 +475,7 @@ export class QueryBuilder {
|
|
|
456
475
|
}
|
|
457
476
|
: undefined;
|
|
458
477
|
c._funcCall = this._funcCall ? { ...this._funcCall } : undefined;
|
|
478
|
+
c._lock = this._lock ? { ...this._lock, opts: this._lock.opts ? { ...this._lock.opts } : undefined } : undefined;
|
|
459
479
|
c._fromFunction = this._fromFunction ? { ...this._fromFunction } : undefined;
|
|
460
480
|
return c;
|
|
461
481
|
}
|
|
@@ -769,6 +789,7 @@ export class QueryBuilder {
|
|
|
769
789
|
: undefined;
|
|
770
790
|
return nodes.selectStmt({
|
|
771
791
|
distinctClause: this._distinct ? [] : undefined,
|
|
792
|
+
lockingClause: this._buildLockingClause(),
|
|
772
793
|
targetList: targetList,
|
|
773
794
|
fromClause: this._buildFromClause(),
|
|
774
795
|
whereClause: whereClause,
|
|
@@ -781,6 +802,21 @@ export class QueryBuilder {
|
|
|
781
802
|
op: 'SETOP_NONE',
|
|
782
803
|
});
|
|
783
804
|
}
|
|
805
|
+
_buildLockingClause() {
|
|
806
|
+
if (!this._lock)
|
|
807
|
+
return undefined;
|
|
808
|
+
const { strength, opts } = this._lock;
|
|
809
|
+
return [
|
|
810
|
+
nodes.lockingClause({
|
|
811
|
+
strength: LOCK_STRENGTHS[strength],
|
|
812
|
+
waitPolicy: (opts?.skipLocked
|
|
813
|
+
? 'LockWaitSkip'
|
|
814
|
+
: opts?.noWait
|
|
815
|
+
? 'LockWaitError'
|
|
816
|
+
: undefined),
|
|
817
|
+
}),
|
|
818
|
+
];
|
|
819
|
+
}
|
|
784
820
|
_buildSelect() {
|
|
785
821
|
this._resetParams();
|
|
786
822
|
const withClause = this._buildWithClause();
|
package/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { Expr, FieldFilter, Filter, FnArg, FnArgs, Operand, ParamAllocator, QueryOutput, SelectExpr, SelectItem, SqlValue, } from './query-builder';
|
|
1
|
+
export type { Expr, FieldFilter, Filter, FnArg, FnArgs, LockOptions, LockStrength, Operand, ParamAllocator, QueryOutput, SelectExpr, SelectItem, SqlValue, } from './query-builder';
|
|
2
2
|
export { add, and, cast, col, div, eq, fn, gt, gte, isNotNull, isNull, lit, lt, lte, mul, neq, not, or, param, QueryBuilder, sub } from './query-builder';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/query-builder",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PostgreSQL Query Builder",
|
|
6
6
|
"main": "index.js",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"pgsql-deparser": "^18.3.6",
|
|
46
46
|
"query-spec": "^0.11.0"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "6880388c8cd5fb21745decff8bd23c3a430338b3"
|
|
49
49
|
}
|
package/query-builder.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type Filter = {
|
|
|
28
28
|
type SortDir = 'ASC' | 'DESC';
|
|
29
29
|
type NullsOrder = 'FIRST' | 'LAST';
|
|
30
30
|
type ConflictAction = 'nothing' | 'update';
|
|
31
|
+
export type LockStrength = 'update' | 'noKeyUpdate' | 'share' | 'keyShare';
|
|
32
|
+
export interface LockOptions {
|
|
33
|
+
skipLocked?: boolean;
|
|
34
|
+
noWait?: boolean;
|
|
35
|
+
}
|
|
31
36
|
export interface SelectExpr {
|
|
32
37
|
expr: Expr;
|
|
33
38
|
as?: string;
|
|
@@ -91,6 +96,7 @@ export declare class QueryBuilder {
|
|
|
91
96
|
private _ctes;
|
|
92
97
|
private _onConflict;
|
|
93
98
|
private _funcCall;
|
|
99
|
+
private _lock;
|
|
94
100
|
schema(name: string): this;
|
|
95
101
|
table(name: string, alias?: string): this;
|
|
96
102
|
select(columns?: SelectItem[]): this;
|
|
@@ -119,6 +125,7 @@ export declare class QueryBuilder {
|
|
|
119
125
|
orderBy(column: string | Expr, direction?: SortDir, nulls?: NullsOrder): this;
|
|
120
126
|
limit(n: number): this;
|
|
121
127
|
offset(n: number): this;
|
|
128
|
+
lock(strength?: LockStrength, opts?: LockOptions): this;
|
|
122
129
|
returning(columns?: SelectItem[]): this;
|
|
123
130
|
with(name: string, query: QueryBuilder, opts?: {
|
|
124
131
|
columns?: string[];
|
|
@@ -158,6 +165,7 @@ export declare class QueryBuilder {
|
|
|
158
165
|
private _buildFromClause;
|
|
159
166
|
private _rangeFunctionNode;
|
|
160
167
|
private _buildSelectInternal;
|
|
168
|
+
private _buildLockingClause;
|
|
161
169
|
private _buildSelect;
|
|
162
170
|
private _buildInsert;
|
|
163
171
|
private _buildOnConflictClause;
|
package/query-builder.js
CHANGED
|
@@ -14,6 +14,12 @@ exports.not = not;
|
|
|
14
14
|
const pg_ast_1 = require("pg-ast");
|
|
15
15
|
const pgsql_deparser_1 = require("pgsql-deparser");
|
|
16
16
|
const keyword_exprs_1 = require("./keyword-exprs");
|
|
17
|
+
const LOCK_STRENGTHS = {
|
|
18
|
+
update: 'LCS_FORUPDATE',
|
|
19
|
+
noKeyUpdate: 'LCS_FORNOKEYUPDATE',
|
|
20
|
+
share: 'LCS_FORSHARE',
|
|
21
|
+
keyShare: 'LCS_FORKEYSHARE',
|
|
22
|
+
};
|
|
17
23
|
// ---------------------------------------------------------------------------
|
|
18
24
|
// AST helpers (thin wrappers for readability)
|
|
19
25
|
// ---------------------------------------------------------------------------
|
|
@@ -281,6 +287,7 @@ class QueryBuilder {
|
|
|
281
287
|
_ctes = [];
|
|
282
288
|
_onConflict;
|
|
283
289
|
_funcCall;
|
|
290
|
+
_lock;
|
|
284
291
|
// -------------------------------------------------------------------------
|
|
285
292
|
// Schema / Table
|
|
286
293
|
// -------------------------------------------------------------------------
|
|
@@ -405,6 +412,18 @@ class QueryBuilder {
|
|
|
405
412
|
this._offsetValue = n;
|
|
406
413
|
return this;
|
|
407
414
|
}
|
|
415
|
+
// Row-level lock on the rows this SELECT reads:
|
|
416
|
+
// .lock() -> FOR UPDATE
|
|
417
|
+
// .lock('share') -> FOR SHARE
|
|
418
|
+
// .lock('update', { skipLocked: true }) -> FOR UPDATE SKIP LOCKED
|
|
419
|
+
// .lock('update', { noWait: true }) -> FOR UPDATE NOWAIT
|
|
420
|
+
lock(strength = 'update', opts) {
|
|
421
|
+
if (opts?.skipLocked && opts?.noWait) {
|
|
422
|
+
throw new Error('A lock either skips locked rows or refuses to wait for them, not both.');
|
|
423
|
+
}
|
|
424
|
+
this._lock = { strength, opts };
|
|
425
|
+
return this;
|
|
426
|
+
}
|
|
408
427
|
// -------------------------------------------------------------------------
|
|
409
428
|
// RETURNING
|
|
410
429
|
// -------------------------------------------------------------------------
|
|
@@ -469,6 +488,7 @@ class QueryBuilder {
|
|
|
469
488
|
}
|
|
470
489
|
: undefined;
|
|
471
490
|
c._funcCall = this._funcCall ? { ...this._funcCall } : undefined;
|
|
491
|
+
c._lock = this._lock ? { ...this._lock, opts: this._lock.opts ? { ...this._lock.opts } : undefined } : undefined;
|
|
472
492
|
c._fromFunction = this._fromFunction ? { ...this._fromFunction } : undefined;
|
|
473
493
|
return c;
|
|
474
494
|
}
|
|
@@ -782,6 +802,7 @@ class QueryBuilder {
|
|
|
782
802
|
: undefined;
|
|
783
803
|
return pg_ast_1.nodes.selectStmt({
|
|
784
804
|
distinctClause: this._distinct ? [] : undefined,
|
|
805
|
+
lockingClause: this._buildLockingClause(),
|
|
785
806
|
targetList: targetList,
|
|
786
807
|
fromClause: this._buildFromClause(),
|
|
787
808
|
whereClause: whereClause,
|
|
@@ -794,6 +815,21 @@ class QueryBuilder {
|
|
|
794
815
|
op: 'SETOP_NONE',
|
|
795
816
|
});
|
|
796
817
|
}
|
|
818
|
+
_buildLockingClause() {
|
|
819
|
+
if (!this._lock)
|
|
820
|
+
return undefined;
|
|
821
|
+
const { strength, opts } = this._lock;
|
|
822
|
+
return [
|
|
823
|
+
pg_ast_1.nodes.lockingClause({
|
|
824
|
+
strength: LOCK_STRENGTHS[strength],
|
|
825
|
+
waitPolicy: (opts?.skipLocked
|
|
826
|
+
? 'LockWaitSkip'
|
|
827
|
+
: opts?.noWait
|
|
828
|
+
? 'LockWaitError'
|
|
829
|
+
: undefined),
|
|
830
|
+
}),
|
|
831
|
+
];
|
|
832
|
+
}
|
|
797
833
|
_buildSelect() {
|
|
798
834
|
this._resetParams();
|
|
799
835
|
const withClause = this._buildWithClause();
|