sql-love 0.9.4 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## Change Log
2
2
 
3
+ ### v0.9.6 - 5 Dec 2023
4
+
5
+ - Utility functions (extractGrouped, extractIndexed, getPagination, runPrisma, runMysql)
6
+ - Added `SelectBuilder.setDefaultEngine(engine)`
7
+ - More documentation
8
+ - Fix `.sortField()` logic
9
+
3
10
  ### v0.9.4 - 30 Nov 2023
4
11
 
5
12
  - Support parsing and building "ILIKE" and "NOT ILIKE"
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
- <img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=0.9.4" width="250" height="208" />
1
+ <img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=0.9.6" width="250" height="208" />
2
2
 
3
3
  # sql-love
4
4
 
5
- [![NPM Link](https://badgen.net/npm/v/sql-love?v=0.9.4)](https://npmjs.com/package/sql-love)
6
- [![Dependencies](https://badgen.net/static/dependencies/0/green?v=0.9.4)](https://npmjs.com/package/sql-love)
7
- [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=0.9.4)](https://github.com/kensnyder/sql-love/actions)
8
- [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=0.9.4)](https://codecov.io/gh/kensnyder/sql-love)
9
- [![ISC License](https://badgen.net/static/license/ISC/green?v=0.9.4)](https://opensource.org/licenses/ISC)
5
+ [![NPM Link](https://badgen.net/npm/v/sql-love?v=0.9.6)](https://npmjs.com/package/sql-love)
6
+ [![Dependencies](https://badgen.net/static/dependencies/0/green?v=0.9.6)](https://npmjs.com/package/sql-love)
7
+ [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=0.9.6)](https://github.com/kensnyder/sql-love/actions)
8
+ [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=0.9.6)](https://codecov.io/gh/kensnyder/sql-love)
9
+ [![ISC License](https://badgen.net/static/license/ISC/green?v=0.9.6)](https://opensource.org/licenses/ISC)
10
10
 
11
11
  Classes for parsing and building SQL select queries in Node
12
12
 
@@ -20,12 +20,21 @@ npm install sql-love
20
20
 
21
21
  - [SelectBuilder](#selectbuilder)
22
22
  - [Parsing base SQL](#parsing-base-sql)
23
+ - [SQL injection](#sql-injection)
23
24
  - [Building the Query](#building-the-query)
24
- - [Counting Results](#counting-results)
25
- - [Other Methods](#other-methods)
26
- - [new SelectBuilder() Limitations](#selectparse-limitations)
27
- - [How to Contribute](./CONTRIBUTING.md)
28
- - [ISC License](./LICENSE.md)
25
+ - [More examples](#where-examples)
26
+ - [Counting results](#counting-results)
27
+ - [Other methods](#other-methods)
28
+ - [new SelectBuilder() limitations](#selectparse-limitations)
29
+ - [Utility functions](#utility-functions)
30
+ - [getPagination](#getpagination)
31
+ - [extractGrouped](#extractGrouped)
32
+ - [extractIndexed](#extractIndexed)
33
+ - [runMysql and runPrismaWithCount](#runPrisma-and-runPrismaWithCount)
34
+ - [runMysql and runMysqlWithCount](#runMysql-and-runMysqlWithCount)
35
+ - [runPg and runPgWithCount](#runPg-and-runPgWithCount)
36
+ - [How to contribute](./CONTRIBUTING.md)
37
+ - [ISC license](./LICENSE.md)
29
38
 
30
39
  ## SelectBuilder
31
40
 
@@ -37,7 +46,8 @@ clauses including JOIN, WHERE, ORDER BY, LIMIT, OFFSET.
37
46
  You can define a base query and pass it to the SelectBuilder constructor.
38
47
 
39
48
  ```js
40
- const { SelectBuilder } = require('sql-love');
49
+ import { SelectBuilder } from 'sql-love';
50
+
41
51
  const query = new SelectBuilder(`
42
52
  SELECT u.id, u.fname, u.lname, u.email, p.phone
43
53
  FROM users
@@ -60,11 +70,16 @@ const { sql, bindings } = query.compile();
60
70
  connection.query(sql, bindings, (err, results, fields) => {});
61
71
  // or Prisma
62
72
  const result = await prisma.$queryRawUnsafe(sql, ...bindings);
73
+
74
+ // Note that these are prepared statements so the values in the "bindings"
75
+ // array are safe from SQL injection.
63
76
  ```
64
77
 
65
78
  It is possible to add placeholders to the base query.
66
79
 
67
80
  ```js
81
+ import { SelectBuilder } from 'sql-love';
82
+
68
83
  const query = new SelectBuilder(
69
84
  `
70
85
  SELECT u.id, u.fname, u.lname, u.email, a.city, a.zip
@@ -95,27 +110,42 @@ And "bindings" equals:
95
110
  You may specify a compiler engine to use. The default is `"mysql"`.
96
111
 
97
112
  ```js
113
+ import { SelectBuilder } from 'sql-love';
114
+
98
115
  const query = new SelectBuilder('SELECT * FROM users')
99
116
  .limit(10)
100
117
  .page(3)
101
118
  .where('id', 1);
102
119
 
103
- query.compile('mysql').sql;
120
+ query.compile({ engine: 'mysql' }).sql;
104
121
  // SELECT * FROM users WHERE id = ? OFFSET 20 LIMIT 10
105
122
 
106
- query.compile('pg').sql;
123
+ query.compile({ engine: 'pg' }).sql;
107
124
  // SELECT * FROM users WHERE id = $1 OFFSET 20 LIMIT 10
108
125
 
109
- query.compile('mssql').sql;
126
+ query.compile({ engine: 'mssql' }).sql;
110
127
  // SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
111
128
 
112
- query.compile('oracle').sql;
129
+ query.compile({ engine: 'oracle' }).sql;
113
130
  // SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
131
+
132
+ // Or you can specify the default engine to use in the compile function
133
+ SelectBuilder.setDefaultEngine('pg');
114
134
  ```
115
135
 
116
- ### Building the Query
136
+ ### SQL injection
137
+
138
+ You should use prepared statements to run the sql and bindings returned by
139
+ `SelectBuilder.compile()`. This will protect you from SQL injection.
140
+
141
+ Even though `prisma.$queryRawUnsafe()` implies that the query is unsafe,
142
+ execute with the "sql", and "bindings" is actually safe because it uses prepared
143
+ statements.
144
+
145
+ ### Building the query
117
146
 
118
- Use the following methods to build queries.
147
+ Use the following methods to build queries. And see
148
+ [more examples below](#where-examples).
119
149
 
120
150
  - `query.where(column, operator, value)` - Require column satisfy operator
121
151
  - `query.where(column, value)` - Require column equal a value
@@ -148,7 +178,94 @@ Use the following methods to build queries.
148
178
  - `query.rightOuterJoin(expression)` - Add a RIGHT OUTER JOIN expression
149
179
  - `query.groupBy(column)` - Group by a column or expression
150
180
 
151
- ### Counting Results
181
+ ### More examples
182
+
183
+ ```js
184
+ // The following are equivalent
185
+ query.where('deleted_at', '=', null);
186
+ query.where('deleted_at', '=', undefined);
187
+ query.where('deleted_at', null);
188
+ query.where('deleted_at', undefined);
189
+ query.where('deleted_at IS NULL');
190
+ query.where({ deleted_at: null });
191
+ query.where({ deleted_at: undefined });
192
+
193
+ // the following demonstrate ways to use LIKE
194
+ query.where('name', 'LIKE', 'son');
195
+ query.where('name', 'LIKE ?', 'son');
196
+ query.where('name', 'LIKE ?%', 'son');
197
+ query.where('name', 'LIKE %?', 'son');
198
+ query.where('name', 'LIKE %?%', 'son');
199
+ query.where('name', 'LIKE', '%son%');
200
+
201
+ // The following demonstrate ways to use various operators
202
+ query.where('price', '=', 100);
203
+ query.where('price', '!=', 100);
204
+ query.where('price', '<>', 100);
205
+ query.where('price', '>', 100);
206
+ query.where({ 'price >': 100 });
207
+ query.where('price', 'BETWEEN', [100, 200]);
208
+ query.where('price', 'NOT BETWEEN', [100, 200]);
209
+ query.whereBetween('price', [100, 200]); // price BETWEEN 100 AND 200
210
+ query.whereBetween('price', [100, null]); // price > 100
211
+ query.whereBetween('price', [null, 200]); // price < 200
212
+ query.whereBetween('price', [null, null]); // clause is ignored
213
+
214
+ // The following demonstrate ways to use IN and NOT IN
215
+ query.where('status', 'IN', ['pending', 'approved']);
216
+ query.where('status', ['pending', 'approved']);
217
+ query.where('status', '=', ['pending', 'approved']);
218
+ query.where('status', '!=', ['pending', 'approved']);
219
+ query.where('status', 'NOT IN', ['pending', 'approved']);
220
+ query.where({ status: ['pending', 'approved'] });
221
+ query.where({ 'status NOT IN': ['pending', 'approved'] });
222
+ query.where({ 'status !=': ['pending', 'approved'] });
223
+ query.where({ 'status <>': ['pending', 'approved'] });
224
+ // Note: .having() supports the same signatures as .where()
225
+
226
+ // The following demonstrates using objects to specify multiple conditions
227
+ query.where({
228
+ deleted_at: null,
229
+ 'price >': 100,
230
+ 'price <': 200,
231
+ color: ['blue', 'black'],
232
+ 'stars BETWEEN': [4, 5],
233
+ conditions: ['new', 'used'],
234
+ });
235
+
236
+ // The following demonstrate equivalent ways to use OR
237
+ query.orWhere([{ approved_at: null }, { denied_at: null }]);
238
+ query.orWhere({ approved_at: null, denied_at: null });
239
+ query.orWhere(['approved_at IS NULL', 'denied_at IS NULL']);
240
+ query.orWhere([
241
+ ['approved_at', null],
242
+ ['denied_at =', null],
243
+ ]);
244
+ query.where('(approved_at IS NULL OR denied_at IS NULL)');
245
+ // Note: .orHaving() supports the same signatures as .orWhere()
246
+
247
+ // The following demonstrate joins
248
+ query.innerJoin('phone_numbers p ON p.user_id = u.id');
249
+ query.leftJoin('phone_numbers p ON p.user_id = u.id AND p.type = ?', ['main']);
250
+ query.outerJoin('phone_numbers p ON p.user_id = u.id AND p.type NOT IN(?, ?)', [
251
+ 'home',
252
+ 'cell',
253
+ ]);
254
+
255
+ // The following demonstrates pagination
256
+ query.limit(10).page(3); // LIMIT 10 OFFSET 20
257
+
258
+ // The following demonstrates cloning
259
+ const query1 = new SelectBuilder();
260
+ query1.where('email', 'john@example.com');
261
+ const query2 = query1.getClone();
262
+ query1.table('users');
263
+ query2.table('contacts');
264
+ query1.compile().sql; // SELECT * FROM users WHERE email = ?
265
+ query2.compile().sql; // SELECT * FROM contacts WHERE email = ?
266
+ ```
267
+
268
+ ### Counting results
152
269
 
153
270
  One powerful feature of SelectBuilder is that it can construct a count query
154
271
  to determine the number of results that would have been returned if there were
@@ -159,6 +276,9 @@ const query = new SelectBuilder('SELECT id, name FROM users LIMIT 5');
159
276
 
160
277
  const { sql } = query.compileCount();
161
278
  // SELECT COUNT(*) AS found_rows FROM users
279
+
280
+ const { sql } = query.compileCount({ countExpr: 'DISTINCT externalId' });
281
+ // SELECT COUNT(DISTINCT externalId) AS found_rows FROM users
162
282
  ```
163
283
 
164
284
  With queries that have a "HAVING" clause, the main query will be wrapped in a
@@ -193,14 +313,14 @@ SelectBuilder has a few other useful methods.
193
313
  - `query.reset(fields)` - Reset a few particular aspects of the query (e.g. \['where', 'having'\])
194
314
  - `query.reset()` - Reset query to an empty state
195
315
 
196
- ### Parser Limitations
316
+ ### Parser limitations
197
317
 
198
318
  `new SelectBuilder(sql)` uses regular expressions and is not a true parser.
199
319
  The goal is to be fast and useful for 99% of situations.
200
320
 
201
321
  Below are some limitations illustrated by example.
202
322
 
203
- #### Nested Subqueries
323
+ #### Nested subqueries
204
324
 
205
325
  Most subqueries can be parsed but sub-subqueries don't work.
206
326
 
@@ -215,14 +335,13 @@ SELECT * FROM categories_posts WHERE category_id IN(
215
335
 
216
336
  // ✅ WORKING EQUIVALENT
217
337
  const query = new SelectBuilder(`SELECT * FROM categories_posts`);
218
- const subquery =
219
- new SelectBuilder(`SELECT id FROM categories WHERE client_id IN(
220
- SELECT client_id FROM affiliations WHERE name LIKE 'test'
221
- )`);
338
+ const subquery = `SELECT id FROM categories WHERE client_id IN(
339
+ SELECT client_id FROM affiliations WHERE name LIKE 'test'
340
+ )`;
222
341
  query.where(`category_id IN(${subquery})`);
223
342
  ```
224
343
 
225
- #### Keywords in Strings
344
+ #### Keywords in strings
226
345
 
227
346
  If you need to use SQL keywords in strings, use bindings.
228
347
 
@@ -236,7 +355,7 @@ new SelectBuilder(`SELECT id, CONCAT(:prefix, expr) FROM users`, {
236
355
  });
237
356
  ```
238
357
 
239
- #### Nested OR and AND Clauses
358
+ #### Nested OR and AND clauses
240
359
 
241
360
  Nested logic can't be parsed properly.
242
361
 
@@ -260,3 +379,110 @@ query.orWhere([
260
379
  'id > 0 AND is_active IS NOT NULL',
261
380
  ]);
262
381
  ```
382
+
383
+ ## Utility functions
384
+
385
+ There are several utility functions bundled with this library.
386
+
387
+ ### getPagination
388
+
389
+ You can get detailed page and limit information from a query.
390
+
391
+ ```js
392
+ import { SelectBuilder, getPagination } from 'sql-love';
393
+
394
+ const query = new SelectBuilder('SELECT * FROM users').limit(10).page(1);
395
+ // ...
396
+ // run a count query to determine that there are 42 results
397
+ const pagination = getPagination(query, 42);
398
+ expect(pagination).toEqual({
399
+ page: 1,
400
+ prevPage: null,
401
+ nextPage: 2,
402
+ perPage: 10,
403
+ numPages: 5,
404
+ total: 42,
405
+ isFirst: true,
406
+ isLast: false,
407
+ });
408
+ ```
409
+
410
+ ### extractIndexed
411
+
412
+ You can index a results set by a particular field.
413
+
414
+ ```js
415
+ import { extractIndexed } from 'sql-love';
416
+
417
+ const records = [
418
+ { id: 1, name: 'John' },
419
+ { id: 2, name: 'Jane' },
420
+ ];
421
+ const indexed = extractIndexed('id', records);
422
+ /*
423
+ {
424
+ 1: { id: 1, name: 'John' },
425
+ 2: { id: 2, name: 'Jane' },
426
+ });
427
+ */
428
+ ```
429
+
430
+ ### extractGrouped
431
+
432
+ You can divide a results set into groups based on a particular field.
433
+
434
+ ```js
435
+ import { extractGrouped } from 'sql-love';
436
+
437
+ const records = [
438
+ { id: 1, name: 'John', dept: 'Marketing' },
439
+ { id: 2, name: 'Jane', dept: 'Finance' },
440
+ { id: 3, name: 'Tim', dept: 'Marketing' },
441
+ ];
442
+ const grouped = extractGrouped('dept', records);
443
+ /*
444
+ {
445
+ Marketing: [
446
+ { id: 1, name: 'John', dept: 'Marketing' },
447
+ { id: 3, name: 'Tim', dept: 'Marketing' },
448
+ ],
449
+ Finance: [{ id: 2, name: 'Jane', dept: 'Finance' }],
450
+ }
451
+ */
452
+ ```
453
+
454
+ ### runPrisma and runPrismaWithCount
455
+
456
+ `runPrisma()` will run a query using Prisma and return the results.
457
+
458
+ ```js
459
+ import { SelectBuilder, runPrisma, runPrismaWithCount } from 'sql-love';
460
+ import { prisma } from '~/db.server';
461
+
462
+ const query = new SelectBuilder('SELECT * FROM users');
463
+ query.where('dept', 'Marketing');
464
+
465
+ const rows = runPrisma(prisma, query, { engine: 'pg' });
466
+ ```
467
+
468
+ `runPrismaWithCount()` will run a query using Prisma and return the results,
469
+ found rows and pagination.
470
+
471
+ ```js
472
+ import { SelectBuilder, runPrisma, runPrismaWithCount } from 'sql-love';
473
+ import { prisma } from '~/db.server';
474
+
475
+ const query = new SelectBuilder('SELECT * FROM users')
476
+ .where('dept', 'Marketing')
477
+ .limit(2)
478
+ .page(5);
479
+ const { records, total, pagination } = await runPrismaWithCount(prisma, query, {
480
+ engine: 'pg',
481
+ });
482
+ ```
483
+
484
+ See [getPagination](#getpagination) for details on the pagination object.
485
+
486
+ ### runMysql and runMysqlWithCount
487
+
488
+ ### runPg and runPgWithCount
package/dist/index.d.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  export { default as Parser } from './src/Parser/Parser';
2
2
  export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
3
+ export { default as extractGrouped } from './src/extractGrouped/extractGrouped';
4
+ export { default as extractIndexed } from './src/extractIndexed/extractIndexed';
5
+ export { default as getPagination } from './src/getPagination/getPagination';
6
+ export { runPrisma, runPrismaWithCount } from './src/runPrisma/runPrisma';
package/dist/index.js CHANGED
@@ -1,2 +1,6 @@
1
1
  export { default as Parser } from './src/Parser/Parser';
2
2
  export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
3
+ export { default as extractGrouped } from './src/extractGrouped/extractGrouped';
4
+ export { default as extractIndexed } from './src/extractIndexed/extractIndexed';
5
+ export { default as getPagination } from './src/getPagination/getPagination';
6
+ export { runPrisma, runPrismaWithCount } from './src/runPrisma/runPrisma';
@@ -73,12 +73,20 @@ export default class SelectBuilder {
73
73
  * @param [bindings] Placeholders to bind to the query
74
74
  */
75
75
  constructor(sql?: string, bindings?: Record<string, any>);
76
+ /**
77
+ * Set the default database engine style (mssql, mysql, oracle, pg)
78
+ * @param engine
79
+ */
80
+ static setDefaultEngine(engine: EngineStyle): typeof SelectBuilder;
76
81
  /**
77
82
  * Get the SQL as a pretty-printed string alongside an array of bindings
78
- * @param engineStyle The database engine style (mssql, mysql, oracle, pg)
83
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
79
84
  * @return The SQL query as a string
80
85
  */
81
- compile(engineStyle?: EngineStyle): {
86
+ compile({ engine, bindingIndex, }?: {
87
+ engine?: EngineStyle;
88
+ bindingIndex?: number;
89
+ }): {
82
90
  sql: string;
83
91
  bindings: any;
84
92
  };
@@ -89,10 +97,26 @@ export default class SelectBuilder {
89
97
  * @param engineStyle The database engine style (mssql, mysql, oracle, pg)
90
98
  * @param countExpr The expression to use inside the COUNT()
91
99
  */
92
- compileCount(engineStyle?: EngineStyle, countExpr?: string): {
100
+ compileCount({ countExpr, engine, bindingIndex, }?: {
101
+ countExpr?: string;
102
+ engine?: EngineStyle;
103
+ bindingIndex?: number;
104
+ }): {
93
105
  sql: string;
94
106
  bindings: any;
95
107
  };
108
+ /**
109
+ * Get SQL needed to union this query with another
110
+ * @param query The query to union
111
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
112
+ */
113
+ compileUnion(query: SelectBuilder, { engine, bindingIndex, }?: {
114
+ engine?: EngineStyle;
115
+ bindingIndex?: number;
116
+ }): {
117
+ sql: string;
118
+ bindings: any[];
119
+ };
96
120
  /**
97
121
  * @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
98
122
  * Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
@@ -290,17 +314,17 @@ export default class SelectBuilder {
290
314
  * Add a series of WHERE clauses joined by OR (See _conditions for usage)
291
315
  * @param conditions
292
316
  */
293
- _orConditions(conditions: WhereCriteria[] | Record<string, any>[]): string;
317
+ _orConditions(conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>): string;
294
318
  /**
295
319
  * Add WHERE conditions to place inside an OR block (See _conditions for usage)
296
320
  * @param conditions A list or object where each item is an array with parameters that would be taken by where()
297
321
  */
298
- orWhere(conditions: WhereCriteria[] | Record<string, any>[]): this;
322
+ orWhere(conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>): this;
299
323
  /**
300
324
  * Add an OR with conditions under the HAVING clause
301
325
  * @param conditions A list or object where each item is an array with parameters that would be taken by having()
302
326
  */
303
- orHaving(conditions: WhereCriteria[] | Record<string, any>[]): this;
327
+ orHaving(conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>): this;
304
328
  /**
305
329
  * Add a HAVING condition (See _conditions for usage)
306
330
  */
@@ -318,13 +342,13 @@ export default class SelectBuilder {
318
342
  orderBy(column: string, direction?: 'ASC' | 'DESC'): this;
319
343
  /**
320
344
  * Sort by the given column, with a map of columns to translate
321
- * @param column The column name such as "created_at" or "-created_at" for descending
322
- * @param [mapNames] Column names to translate from one name to another
345
+ * @param fieldName The file name such as "created_at" or "-created_at" for descending
346
+ * @param [mapNames] Column names to translate from field to column expression
323
347
  * @example
324
348
  * query.sortField('-modified_at'); // ORDER BY modified_at DESC
325
- * query.sortField('created_at', {'created_at': 'created'}); // ORDER BY created
349
+ * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
326
350
  */
327
- sortField(column: string, mapNames?: Record<string, string>): this;
351
+ sortField(fieldName: string, mapNames?: Record<string, string>): this;
328
352
  /**
329
353
  * Check to see if the given string is 0 or all digits not starting with 0
330
354
  * @param string The input string
@@ -352,5 +376,4 @@ export default class SelectBuilder {
352
376
  * @param number The page number (integer greater than 0)
353
377
  */
354
378
  page(number: number | string): this;
355
- fetch(engine: any): void;
356
379
  }
@@ -1,6 +1,7 @@
1
1
  import Parser from '../Parser/Parser';
2
2
  import substrCount from '../substrCount/substrCount';
3
3
  const isNullish = v => v === undefined || v === null || v === false || isNaN(v);
4
+ let defaultEngine = 'mysql';
4
5
  /**
5
6
  * Build a select query
6
7
  * Class Select
@@ -85,12 +86,20 @@ export default class SelectBuilder {
85
86
  new Parser(this).parse(sql);
86
87
  }
87
88
  }
89
+ /**
90
+ * Set the default database engine style (mssql, mysql, oracle, pg)
91
+ * @param engine
92
+ */
93
+ static setDefaultEngine(engine) {
94
+ defaultEngine = engine;
95
+ return SelectBuilder;
96
+ }
88
97
  /**
89
98
  * Get the SQL as a pretty-printed string alongside an array of bindings
90
- * @param engineStyle The database engine style (mssql, mysql, oracle, pg)
99
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
91
100
  * @return The SQL query as a string
92
101
  */
93
- compile(engineStyle = 'mysql') {
102
+ compile({ engine = defaultEngine, bindingIndex = 1, } = {}) {
94
103
  // build all the lines
95
104
  const lines = [
96
105
  'SELECT',
@@ -105,29 +114,29 @@ export default class SelectBuilder {
105
114
  ];
106
115
  if (this._page > 0) {
107
116
  const offset = (this._page - 1) * this._limit;
108
- lines.push(this._compileOFFSET(engineStyle, offset));
109
- lines.push(this._compileLIMIT(engineStyle, this._limit));
117
+ lines.push(this._compileOFFSET(engine, offset));
118
+ lines.push(this._compileLIMIT(engine, this._limit));
110
119
  }
111
120
  else {
112
121
  if (this._offset) {
113
- lines.push(this._compileOFFSET(engineStyle, this._offset));
122
+ lines.push(this._compileOFFSET(engine, this._offset));
114
123
  }
115
124
  if (this._limit) {
116
- lines.push(this._compileLIMIT(engineStyle, this._limit));
125
+ lines.push(this._compileLIMIT(engine, this._limit));
117
126
  }
118
127
  }
119
128
  const pretty = lines.filter(Boolean).join('\n').trim();
120
129
  let replacer;
121
130
  const bindings = [];
122
- if (engineStyle === 'pg') {
123
- let idx = 1;
124
- replacer = ($0, $1) => {
131
+ if (engine === 'pg') {
132
+ let idx = bindingIndex;
133
+ replacer = (_, $1) => {
125
134
  bindings.push(this._bindings[$1]);
126
135
  return '$' + idx++;
127
136
  };
128
137
  }
129
138
  else {
130
- replacer = ($0, $1) => {
139
+ replacer = (_, $1) => {
131
140
  bindings.push(this._bindings[$1]);
132
141
  return '?';
133
142
  };
@@ -156,9 +165,9 @@ export default class SelectBuilder {
156
165
  * @param engineStyle The database engine style (mssql, mysql, oracle, pg)
157
166
  * @param countExpr The expression to use inside the COUNT()
158
167
  */
159
- compileCount(engineStyle = 'mysql', countExpr = '*') {
168
+ compileCount({ countExpr = '*', engine = defaultEngine, bindingIndex = 1, } = {}) {
160
169
  const query = this.getFoundRowsQuery(countExpr);
161
- const { sql, bindings } = query.compile(engineStyle);
170
+ const { sql, bindings } = query.compile({ engine, bindingIndex });
162
171
  if (this._havings.length === 0) {
163
172
  return { sql, bindings };
164
173
  }
@@ -170,6 +179,22 @@ export default class SelectBuilder {
170
179
  };
171
180
  }
172
181
  }
182
+ /**
183
+ * Get SQL needed to union this query with another
184
+ * @param query The query to union
185
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
186
+ */
187
+ compileUnion(query, { engine = defaultEngine, bindingIndex = 1, } = {}) {
188
+ const first = this.compile({ engine, bindingIndex });
189
+ const second = query.compile({
190
+ engine,
191
+ bindingIndex: first.bindings.length + bindingIndex,
192
+ });
193
+ return {
194
+ sql: `(${first.sql})\nUNION\n(${second.sql})`,
195
+ bindings: [...first.bindings, ...second.bindings],
196
+ };
197
+ }
173
198
  /**
174
199
  * @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
175
200
  * Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
@@ -618,12 +643,21 @@ export default class SelectBuilder {
618
643
  */
619
644
  _orConditions(conditions) {
620
645
  const items = [];
621
- for (const condition of conditions) {
622
- if (Array.isArray(condition)) {
623
- this._conditions(items, condition);
646
+ if (Array.isArray(conditions)) {
647
+ for (const condition of conditions) {
648
+ if (Array.isArray(condition)) {
649
+ this._conditions(items, condition);
650
+ }
651
+ else {
652
+ this._conditions(items, [condition]);
653
+ }
624
654
  }
625
- else {
626
- this._conditions(items, [condition]);
655
+ }
656
+ else {
657
+ for (const [key, value] of Object.entries(conditions)) {
658
+ const one = [];
659
+ this._conditions(one, [key, value]);
660
+ items.push(one[0]);
627
661
  }
628
662
  }
629
663
  const joined = items.join(' OR ');
@@ -682,16 +716,20 @@ export default class SelectBuilder {
682
716
  }
683
717
  /**
684
718
  * Sort by the given column, with a map of columns to translate
685
- * @param column The column name such as "created_at" or "-created_at" for descending
686
- * @param [mapNames] Column names to translate from one name to another
719
+ * @param fieldName The file name such as "created_at" or "-created_at" for descending
720
+ * @param [mapNames] Column names to translate from field to column expression
687
721
  * @example
688
722
  * query.sortField('-modified_at'); // ORDER BY modified_at DESC
689
- * query.sortField('created_at', {'created_at': 'created'}); // ORDER BY created
723
+ * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
690
724
  */
691
- sortField(column, mapNames = {}) {
692
- column.replace(/^-(.+)/, '$1 DESC');
693
- column = mapNames[column] || column;
694
- this.orderBy(column);
725
+ sortField(fieldName, mapNames = {}) {
726
+ let direction = 'ASC';
727
+ if (fieldName.startsWith('-')) {
728
+ direction = 'DESC';
729
+ fieldName = fieldName.slice(1);
730
+ }
731
+ const column = mapNames[fieldName] || fieldName;
732
+ this.orderBy(column, direction);
695
733
  return this;
696
734
  }
697
735
  /**
@@ -769,9 +807,4 @@ export default class SelectBuilder {
769
807
  }
770
808
  return this;
771
809
  }
772
- fetch(engine) {
773
- if (engine.$queryRawUnsafe) {
774
- //
775
- }
776
- }
777
810
  }