@sqb/builder 5.0.6 → 6.0.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
@@ -6,16 +6,40 @@
6
6
 
7
7
  [![NPM Version][npm-image]][npm-url]
8
8
  [![NPM Downloads][downloads-image]][downloads-url]
9
- [![Build Status][travis-image]][travis-url]
9
+ [![CI Tests][ci-test-image]][ci-test-url]
10
10
  [![Test Coverage][coveralls-image]][coveralls-url]
11
- [![Dependencies][dependencies-image]][dependencies-url]
12
- [![DevDependencies][devdependencies-image]][devdependencies-url]
13
- [![Package Quality][quality-image]][quality-url]
14
11
 
15
12
  ## About SQB
16
13
 
17
14
  SQB is an extensible, multi-dialect SQL query builder and Database connection wrapper for NodeJS.
18
15
 
16
+ ## About @sqb/builder
17
+
18
+ `@sqb/builder` is the query construction and serialization core the whole SQB stack is built on.
19
+ It lets you compose `Select`, `Insert`, `Update` and `Delete` statements as plain JavaScript/
20
+ TypeScript objects — with type-checked columns, joins, conditions and parameters — and turns them
21
+ into the correct SQL text for whichever database you're targeting, without depending on any driver
22
+ or network connection itself.
23
+
24
+ Serialization is dialect-driven: each target database (Postgres, MySQL, MariaDB, Oracle, SQL
25
+ Server, SQLite, ...) is a pluggable `SerializerExtension` that can override how any part of a query
26
+ is rendered — identifier quoting, `LIMIT`/`OFFSET` syntax, boolean literals, `RETURNING` support,
27
+ and more — while everything it doesn't override falls through to sensible defaults. This is what
28
+ lets [`@sqb/connect`](../connect) and the various dialect/adapter packages share one query-building
29
+ API across every supported database.
30
+
31
+ ```ts
32
+ import { Select, Eq } from '@sqb/builder';
33
+
34
+ const query = Select('id', 'given_name', 'family_name')
35
+ .from('customers')
36
+ .where(Eq('active', true))
37
+ .orderBy('id')
38
+ .limit(10);
39
+
40
+ const { sql, params } = query.generate({ dialect: 'postgres' });
41
+ ```
42
+
19
43
  ## Main goals
20
44
 
21
45
  - Single code base for any sql based database
@@ -39,7 +63,7 @@ $ npm install @sqb/builder --save
39
63
 
40
64
  ## Node Compatibility
41
65
 
42
- - node >= 16.x
66
+ - node >= 20.x
43
67
 
44
68
  ### License
45
69
 
@@ -47,17 +71,9 @@ SQB is available under [MIT](LICENSE) license.
47
71
 
48
72
  [npm-image]: https://img.shields.io/npm/v/@sqb/builder.svg
49
73
  [npm-url]: https://npmjs.org/package/@sqb/builder
50
- [travis-image]: https://img.shields.io/travis/sqbjs/@sqb/builder/master.svg
51
- [travis-url]: https://travis-ci.org/sqbjs/@sqb/builder
52
- [coveralls-image]: https://img.shields.io/coveralls/sqbjs/@sqb/builder/master.svg
53
- [coveralls-url]: https://coveralls.io/r/sqbjs/@sqb/builder
54
74
  [downloads-image]: https://img.shields.io/npm/dm/@sqb/builder.svg
55
75
  [downloads-url]: https://npmjs.org/package/@sqb/builder
56
- [gitter-image]: https://badges.gitter.im/sqbjs/@sqb/builder.svg
57
- [gitter-url]: https://gitter.im/sqbjs/@sqb/builder?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
58
- [dependencies-image]: https://david-dm.org/sqbjs/@sqb/builder/status.svg
59
- [dependencies-url]: https://david-dm.org/sqbjs/@sqb/builder
60
- [devdependencies-image]: https://david-dm.org/sqbjs/@sqb/builder/dev-status.svg
61
- [devdependencies-url]: https://david-dm.org/sqbjs/@sqb/builder?type=dev
62
- [quality-image]: http://npm.packagequality.com/shield/@sqb/builder.png
63
- [quality-url]: http://packagequality.com/#?package=@sqb/builder
76
+ [ci-test-image]: https://github.com/panates/sqb/actions/workflows/test.yml/badge.svg
77
+ [ci-test-url]: https://github.com/panates/sqb/actions/workflows/test.yml
78
+ [coveralls-image]: https://coveralls.io/repos/github/sqbjs/sqb/badge.svg?branch=master
79
+ [coveralls-url]: https://coveralls.io/github/sqbjs/sqb?branch=master
package/enums.d.ts CHANGED
@@ -72,7 +72,8 @@ export declare enum OperatorType {
72
72
  isNot = "isNot",
73
73
  exists = "exists",
74
74
  notExists = "notExists",
75
- not = "not"
75
+ not = "not",
76
+ match = "match"
76
77
  }
77
78
  export declare enum DataType {
78
79
  BOOL = "BOOL",
package/enums.js CHANGED
@@ -76,6 +76,7 @@ export var OperatorType;
76
76
  OperatorType["exists"] = "exists";
77
77
  OperatorType["notExists"] = "notExists";
78
78
  OperatorType["not"] = "not";
79
+ OperatorType["match"] = "match";
79
80
  })(OperatorType || (OperatorType = {}));
80
81
  export var DataType;
81
82
  (function (DataType) {
package/op.ns.d.ts CHANGED
@@ -11,6 +11,7 @@ import { IsNot } from './sql/operators/is-not.js';
11
11
  import { Like } from './sql/operators/like.js';
12
12
  import { Lt } from './sql/operators/lt.js';
13
13
  import { Lte } from './sql/operators/lte.js';
14
+ import { Match } from './sql/operators/match.js';
14
15
  import { Ne } from './sql/operators/ne.js';
15
16
  import { Not } from './sql/operators/not.js';
16
17
  import { NotBetween } from './sql/operators/not-between.js';
@@ -59,6 +60,7 @@ export interface OperatorsMap {
59
60
  exists: typeof Exists;
60
61
  notExists: typeof NotExists;
61
62
  '!exists': typeof NotExists;
63
+ match: typeof Match;
62
64
  }
63
65
  declare const Operators: OperatorsMap;
64
66
  export { Operators };
package/op.ns.js CHANGED
@@ -12,6 +12,7 @@ import { IsNot } from './sql/operators/is-not.js';
12
12
  import { Like } from './sql/operators/like.js';
13
13
  import { Lt } from './sql/operators/lt.js';
14
14
  import { Lte } from './sql/operators/lte.js';
15
+ import { Match } from './sql/operators/match.js';
15
16
  import { Ne } from './sql/operators/ne.js';
16
17
  import { Not } from './sql/operators/not.js';
17
18
  import { NotBetween } from './sql/operators/not-between.js';
@@ -60,6 +61,7 @@ const Operators = {
60
61
  exists: Exists,
61
62
  notExists: NotExists,
62
63
  '!exists': NotExists,
64
+ match: Match,
63
65
  };
64
66
  LogicalOperator.Operators = Operators;
65
67
  export { Operators };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sqb/builder",
3
3
  "description": "Extensible multi-dialect SQL query builder written with TypeScript",
4
- "version": "5.0.6",
4
+ "version": "6.0.0",
5
5
  "author": "Panates",
6
6
  "license": "Apache-2.0",
7
7
  "dependencies": {
@@ -2,7 +2,7 @@ import { Query } from './sql/index.js';
2
2
  import type { DefaultSerializeFunction, GenerateOptions, ParamOptions } from './types.js';
3
3
  export declare class SerializeContext implements GenerateOptions {
4
4
  readonly rootQuery: Query;
5
- readonly reservedWords: string[];
5
+ readonly reservedWords: Set<string>;
6
6
  dialect?: string;
7
7
  prettyPrint?: boolean;
8
8
  params?: Record<string, any>;
@@ -4,7 +4,7 @@ import { Query } from './sql/index.js';
4
4
  import { isLogicalOperator, isQuery, isSqlElement } from './type-guards.js';
5
5
  export class SerializeContext {
6
6
  rootQuery;
7
- reservedWords = [
7
+ reservedWords = new Set([
8
8
  'schema',
9
9
  'table',
10
10
  'field',
@@ -61,7 +61,27 @@ export class SerializeContext {
61
61
  'foreign',
62
62
  'user',
63
63
  'password',
64
- ];
64
+ 'all',
65
+ 'as',
66
+ 'asc',
67
+ 'case',
68
+ 'cast',
69
+ 'check',
70
+ 'column',
71
+ 'desc',
72
+ 'else',
73
+ 'end',
74
+ 'for',
75
+ 'in',
76
+ 'into',
77
+ 'is',
78
+ 'on',
79
+ 'then',
80
+ 'to',
81
+ 'union',
82
+ 'unique',
83
+ 'when',
84
+ ]);
65
85
  dialect;
66
86
  prettyPrint;
67
87
  params;
@@ -182,7 +202,7 @@ export class SerializeContext {
182
202
  isReservedWord(s) {
183
203
  if (!s)
184
204
  return false;
185
- if (this.reservedWords.includes(s.toLowerCase()))
205
+ if (this.reservedWords.has(s.toLowerCase()))
186
206
  return true;
187
207
  for (const ext of SerializerRegistry.items()) {
188
208
  if (ext.dialect === this.dialect && ext.isReservedWord) {
@@ -19,7 +19,7 @@ class FieldClass extends BaseField {
19
19
  const prefix = ctx.escapeReserved(this._schema ? this._schema + '.' : '') +
20
20
  (this._table ? this._table + '.' : '');
21
21
  return (prefix +
22
- (!prefix && o.isReservedWord ? '"' + this._field + '"' : this._field) +
22
+ (o.isReservedWord ? '"' + this._field + '"' : this._field) +
23
23
  (this._alias ? ' as ' + this._alias : ''));
24
24
  });
25
25
  }
@@ -4,7 +4,6 @@ import { SerializeContext } from '../../serialize-context.js';
4
4
  declare class MinClass extends SqlElement {
5
5
  _expression: any;
6
6
  _alias?: string;
7
- constructor(expression: any);
8
7
  get _type(): SerializationType;
9
8
  /**
10
9
  * Sets alias to case expression.
@@ -4,10 +4,6 @@ import { SerializeContext } from '../../serialize-context.js';
4
4
  class MinClass extends SqlElement {
5
5
  _expression;
6
6
  _alias;
7
- constructor(expression) {
8
- super();
9
- this._expression = expression;
10
- }
11
7
  get _type() {
12
8
  return SerializationType.MIN_STATEMENT;
13
9
  }
@@ -38,6 +34,7 @@ class MinClass extends SqlElement {
38
34
  export const Min = function (expression) {
39
35
  if (!(this instanceof Min))
40
36
  return new Min(expression);
37
+ SqlElement.call(this);
41
38
  this._expression = expression;
42
39
  };
43
40
  Min.prototype = MinClass.prototype;
package/sql/insert.d.ts CHANGED
@@ -12,6 +12,7 @@ declare class InsertClass extends ReturningQuery {
12
12
  * Performs serialization
13
13
  */
14
14
  _serialize(ctx: SerializeContext): string;
15
+ protected __defaultSerialize(ctx: SerializeContext, o: any): string;
15
16
  /**
16
17
  *
17
18
  */
package/sql/insert.js CHANGED
@@ -23,6 +23,9 @@ class InsertClass extends ReturningQuery {
23
23
  values: this.__serializeValues(ctx),
24
24
  returning: this.__serializeReturning(ctx),
25
25
  };
26
+ return ctx.serialize(this._type, o, () => this.__defaultSerialize(ctx, o));
27
+ }
28
+ __defaultSerialize(ctx, o) {
26
29
  let out = 'insert into ' +
27
30
  o.table +
28
31
  '\n\t(' +
@@ -53,7 +56,7 @@ class InsertClass extends ReturningQuery {
53
56
  }
54
57
  else
55
58
  arr = Object.keys(this._input);
56
- return ctx.serialize(SerializationType.INSERT_QUERY_COLUMNS, arr, () => printArray(arr));
59
+ return ctx.serialize(SerializationType.INSERT_QUERY_COLUMNS, arr, () => printArray(arr.map(c => ctx.escapeReserved(c))));
57
60
  }
58
61
  /**
59
62
  *
@@ -4,8 +4,13 @@ import { isSqlElement } from '../../type-guards.js';
4
4
  import { CompOperator } from './comp-operator.js';
5
5
  class InClass extends CompOperator {
6
6
  _serialize(ctx) {
7
- if (Array.isArray(this._right) && !this._right.length)
8
- return '';
7
+ if (Array.isArray(this._right) && !this._right.length) {
8
+ // Nothing can be IN an empty list (always false), and everything is
9
+ // NOT IN an empty list (always true). Emit a self-contained literal
10
+ // instead of dropping the condition, which would silently invert its
11
+ // meaning into "no filter at all".
12
+ return this._operatorType === OperatorType.notIn ? '1=1' : '1=0';
13
+ }
9
14
  return super._serialize(ctx);
10
15
  }
11
16
  }
@@ -13,6 +13,7 @@ export * from './like.js';
13
13
  export * from './logical-operator.js';
14
14
  export * from './lt.js';
15
15
  export * from './lte.js';
16
+ export * from './match.js';
16
17
  export * from './ne.js';
17
18
  export * from './not.js';
18
19
  export * from './not-between.js';
@@ -13,6 +13,7 @@ export * from './like.js';
13
13
  export * from './logical-operator.js';
14
14
  export * from './lt.js';
15
15
  export * from './lte.js';
16
+ export * from './match.js';
16
17
  export * from './ne.js';
17
18
  export * from './not.js';
18
19
  export * from './not-between.js';
@@ -29,10 +29,13 @@ function wrapObject(obj) {
29
29
  const m = n.match(COMPARE_LEFT_PATTERN);
30
30
  if (!m)
31
31
  throw new TypeError(`"${n}" is not a valid expression definition`);
32
- fn = registeredOperators[m[2] || 'eq'];
32
+ // A bare array value with no explicit operator (e.g. {status: [...]})
33
+ // means "one of these values", not literal equality to an array.
34
+ const opKey = m[2] || (Array.isArray(v) ? 'in' : 'eq');
35
+ fn = registeredOperators[opKey];
33
36
  if (!fn)
34
- throw new Error(`Unknown operator "${m[2]}"`);
35
- const inst = fn(m[1], obj[n]);
37
+ throw new Error(`Unknown operator "${opKey}"`);
38
+ const inst = fn(m[1], v);
36
39
  result.push(inst);
37
40
  }
38
41
  }
@@ -0,0 +1,16 @@
1
+ import { SqlElement } from '../../serializable.js';
2
+ import { SerializeContext } from '../../serialize-context.js';
3
+ import { CompOperator } from './comp-operator.js';
4
+ declare class MatchClass extends CompOperator {
5
+ customArgs?: any;
6
+ __serialize(ctx: SerializeContext, o: any): string;
7
+ }
8
+ interface MatchCtor {
9
+ new (left: string | SqlElement, right?: string | SqlElement, customArgs?: any): Match;
10
+ (left: string | SqlElement, right?: string | SqlElement, customArgs?: any): Match;
11
+ prototype: Match;
12
+ }
13
+ export declare const Match: MatchCtor;
14
+ export interface Match extends MatchClass {
15
+ }
16
+ export {};
@@ -0,0 +1,32 @@
1
+ import { OperatorType } from '../../enums.js';
2
+ import { SqlElement } from '../../serializable.js';
3
+ import { SerializeContext } from '../../serialize-context.js';
4
+ import { CompOperator } from './comp-operator.js';
5
+ class MatchClass extends CompOperator {
6
+ customArgs;
7
+ __serialize(ctx, o) {
8
+ if (!o.right.expression)
9
+ return '';
10
+ if (o.right && typeof o.right.expression !== 'string')
11
+ o.right.expression = String(o.right.expression);
12
+ o.customArgs = this.customArgs;
13
+ return ctx.serialize(this._type, o, (_ctx, _o) => this.__defaultSerialize(_ctx, _o));
14
+ }
15
+ }
16
+ export const Match = function (left, right, customArgs) {
17
+ if (!(this instanceof Match))
18
+ return new Match(left, right, customArgs);
19
+ CompOperator.call(this, left, right);
20
+ this._operatorType = OperatorType.match;
21
+ this._symbol = '=';
22
+ this.customArgs = customArgs;
23
+ if (typeof left === 'string') {
24
+ const m = left.match(/^([\w\\.$]+)(\[])?/);
25
+ if (!m)
26
+ throw new TypeError(`"${left}" is not a valid expression definition`);
27
+ this._left = m[1];
28
+ this._isArray = !!m[2];
29
+ }
30
+ };
31
+ Match.prototype = MatchClass.prototype;
32
+ Match.prototype.constructor = Match;
package/sql/update.d.ts CHANGED
@@ -18,6 +18,7 @@ declare class UpdateClass extends ReturningQuery {
18
18
  * Performs serialization
19
19
  */
20
20
  _serialize(ctx: SerializeContext): string;
21
+ protected __defaultSerialize(ctx: SerializeContext, o: any): string;
21
22
  /**
22
23
  *
23
24
  */
package/sql/update.js CHANGED
@@ -33,6 +33,9 @@ class UpdateClass extends ReturningQuery {
33
33
  where: this.__serializeWhere(ctx),
34
34
  returning: this.__serializeReturning(ctx),
35
35
  };
36
+ return ctx.serialize(this._type, o, () => this.__defaultSerialize(ctx, o));
37
+ }
38
+ __defaultSerialize(ctx, o) {
36
39
  let out = 'update ' + o.table + ' set \n\t' + o.values + '\b';
37
40
  if (o.where)
38
41
  out += '\n' + o.where;
@@ -54,7 +57,7 @@ class UpdateClass extends ReturningQuery {
54
57
  });
55
58
  }
56
59
  return ctx.serialize(SerializationType.UPDATE_QUERY_VALUES, arr, () => {
57
- const a = arr.map(o => o.field + ' = ' + o.value);
60
+ const a = arr.map(o => ctx.escapeReserved(o.field) + ' = ' + o.value);
58
61
  return printArray(a, ',');
59
62
  });
60
63
  }