@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 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) |
@@ -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.10.6",
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": "ce544846eafd96280fac2607f873960392ae2c89"
48
+ "gitHead": "6880388c8cd5fb21745decff8bd23c3a430338b3"
49
49
  }
@@ -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();