@stamhoofd/sql 2.83.5 → 2.84.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.
Files changed (176) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +9 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/QueryableModel.d.ts +1 -0
  6. package/dist/src/QueryableModel.d.ts.map +1 -1
  7. package/dist/src/QueryableModel.js +20 -2
  8. package/dist/src/QueryableModel.js.map +1 -1
  9. package/dist/src/SQL.d.ts +9 -16
  10. package/dist/src/SQL.d.ts.map +1 -1
  11. package/dist/src/SQL.js +16 -13
  12. package/dist/src/SQL.js.map +1 -1
  13. package/dist/src/SQLDelete.d.ts +2 -2
  14. package/dist/src/SQLDelete.d.ts.map +1 -1
  15. package/dist/src/SQLDelete.js +13 -3
  16. package/dist/src/SQLDelete.js.map +1 -1
  17. package/dist/src/SQLExpression.d.ts +3 -0
  18. package/dist/src/SQLExpression.d.ts.map +1 -1
  19. package/dist/src/SQLExpression.js.map +1 -1
  20. package/dist/src/SQLExpressions.d.ts +17 -6
  21. package/dist/src/SQLExpressions.d.ts.map +1 -1
  22. package/dist/src/SQLExpressions.js +20 -12
  23. package/dist/src/SQLExpressions.js.map +1 -1
  24. package/dist/src/SQLJoin.d.ts +3 -3
  25. package/dist/src/SQLJoin.d.ts.map +1 -1
  26. package/dist/src/SQLJoin.js +5 -1
  27. package/dist/src/SQLJoin.js.map +1 -1
  28. package/dist/src/SQLJsonExpressions.d.ts +22 -0
  29. package/dist/src/SQLJsonExpressions.d.ts.map +1 -1
  30. package/dist/src/SQLJsonExpressions.js +56 -4
  31. package/dist/src/SQLJsonExpressions.js.map +1 -1
  32. package/dist/src/SQLSelect.d.ts +10 -4
  33. package/dist/src/SQLSelect.d.ts.map +1 -1
  34. package/dist/src/SQLSelect.js +25 -11
  35. package/dist/src/SQLSelect.js.map +1 -1
  36. package/dist/src/SQLWhere.d.ts +29 -1
  37. package/dist/src/SQLWhere.d.ts.map +1 -1
  38. package/dist/src/SQLWhere.js +181 -12
  39. package/dist/src/SQLWhere.js.map +1 -1
  40. package/dist/src/filters/SQLFilter.d.ts +1 -0
  41. package/dist/src/filters/SQLFilter.d.ts.map +1 -1
  42. package/dist/src/filters/SQLFilter.js +8 -0
  43. package/dist/src/filters/SQLFilter.js.map +1 -1
  44. package/dist/src/filters/modern/SQLModernFilter.d.ts +73 -0
  45. package/dist/src/filters/modern/SQLModernFilter.d.ts.map +1 -0
  46. package/dist/src/filters/modern/SQLModernFilter.js +200 -0
  47. package/dist/src/filters/modern/SQLModernFilter.js.map +1 -0
  48. package/dist/src/filters/modern/compilers/contains.d.ts +4 -0
  49. package/dist/src/filters/modern/compilers/contains.d.ts.map +1 -0
  50. package/dist/src/filters/modern/compilers/contains.js +28 -0
  51. package/dist/src/filters/modern/compilers/contains.js.map +1 -0
  52. package/dist/src/filters/modern/compilers/equals.d.ts +4 -0
  53. package/dist/src/filters/modern/compilers/equals.d.ts.map +1 -0
  54. package/dist/src/filters/modern/compilers/equals.js +46 -0
  55. package/dist/src/filters/modern/compilers/equals.js.map +1 -0
  56. package/dist/src/filters/modern/compilers/greater.d.ts +4 -0
  57. package/dist/src/filters/modern/compilers/greater.d.ts.map +1 -0
  58. package/dist/src/filters/modern/compilers/greater.js +17 -0
  59. package/dist/src/filters/modern/compilers/greater.js.map +1 -0
  60. package/dist/src/filters/modern/compilers/in.d.ts +4 -0
  61. package/dist/src/filters/modern/compilers/in.d.ts.map +1 -0
  62. package/dist/src/filters/modern/compilers/in.js +50 -0
  63. package/dist/src/filters/modern/compilers/in.js.map +1 -0
  64. package/dist/src/filters/modern/compilers/index.d.ts +5 -0
  65. package/dist/src/filters/modern/compilers/index.d.ts.map +1 -0
  66. package/dist/src/filters/modern/compilers/index.js +8 -0
  67. package/dist/src/filters/modern/compilers/index.js.map +1 -0
  68. package/dist/src/filters/modern/compilers/less.d.ts +4 -0
  69. package/dist/src/filters/modern/compilers/less.d.ts.map +1 -0
  70. package/dist/src/filters/modern/compilers/less.js +17 -0
  71. package/dist/src/filters/modern/compilers/less.js.map +1 -0
  72. package/dist/src/filters/modern/helpers/isJSONColumn.d.ts +4 -0
  73. package/dist/src/filters/modern/helpers/isJSONColumn.d.ts.map +1 -0
  74. package/dist/src/filters/modern/helpers/isJSONColumn.js +16 -0
  75. package/dist/src/filters/modern/helpers/isJSONColumn.js.map +1 -0
  76. package/dist/src/filters/modern/helpers/normalizeCompareValue.d.ts +9 -0
  77. package/dist/src/filters/modern/helpers/normalizeCompareValue.d.ts.map +1 -0
  78. package/dist/src/filters/modern/helpers/normalizeCompareValue.js +82 -0
  79. package/dist/src/filters/modern/helpers/normalizeCompareValue.js.map +1 -0
  80. package/dist/tests/filters/$and.test.d.ts +2 -0
  81. package/dist/tests/filters/$and.test.d.ts.map +1 -0
  82. package/dist/tests/filters/$and.test.js +185 -0
  83. package/dist/tests/filters/$and.test.js.map +1 -0
  84. package/dist/tests/filters/$contains.test.d.ts +2 -0
  85. package/dist/tests/filters/$contains.test.d.ts.map +1 -0
  86. package/dist/tests/filters/$contains.test.js +701 -0
  87. package/dist/tests/filters/$contains.test.js.map +1 -0
  88. package/dist/tests/filters/$eq.test.d.ts +2 -0
  89. package/dist/tests/filters/$eq.test.d.ts.map +1 -0
  90. package/dist/tests/filters/$eq.test.js +986 -0
  91. package/dist/tests/filters/$eq.test.js.map +1 -0
  92. package/dist/tests/filters/$gt.test.d.ts +2 -0
  93. package/dist/tests/filters/$gt.test.d.ts.map +1 -0
  94. package/dist/tests/filters/$gt.test.js +463 -0
  95. package/dist/tests/filters/$gt.test.js.map +1 -0
  96. package/dist/tests/filters/$gte.test.d.ts +2 -0
  97. package/dist/tests/filters/$gte.test.d.ts.map +1 -0
  98. package/dist/tests/filters/$gte.test.js +433 -0
  99. package/dist/tests/filters/$gte.test.js.map +1 -0
  100. package/dist/tests/filters/$in.test.d.ts +2 -0
  101. package/dist/tests/filters/$in.test.d.ts.map +1 -0
  102. package/dist/tests/filters/$in.test.js +590 -0
  103. package/dist/tests/filters/$in.test.js.map +1 -0
  104. package/dist/tests/filters/$lt.test.d.ts +2 -0
  105. package/dist/tests/filters/$lt.test.d.ts.map +1 -0
  106. package/dist/tests/filters/$lt.test.js +433 -0
  107. package/dist/tests/filters/$lt.test.js.map +1 -0
  108. package/dist/tests/filters/$lte.test.d.ts +2 -0
  109. package/dist/tests/filters/$lte.test.d.ts.map +1 -0
  110. package/dist/tests/filters/$lte.test.js +472 -0
  111. package/dist/tests/filters/$lte.test.js.map +1 -0
  112. package/dist/tests/filters/$neq.test.d.ts +2 -0
  113. package/dist/tests/filters/$neq.test.d.ts.map +1 -0
  114. package/dist/tests/filters/$neq.test.js +32 -0
  115. package/dist/tests/filters/$neq.test.js.map +1 -0
  116. package/dist/tests/filters/$not.test.d.ts +2 -0
  117. package/dist/tests/filters/$not.test.d.ts.map +1 -0
  118. package/dist/tests/filters/$not.test.js +50 -0
  119. package/dist/tests/filters/$not.test.js.map +1 -0
  120. package/dist/tests/filters/$or.test.d.ts +2 -0
  121. package/dist/tests/filters/$or.test.d.ts.map +1 -0
  122. package/dist/tests/filters/$or.test.js +185 -0
  123. package/dist/tests/filters/$or.test.js.map +1 -0
  124. package/dist/tests/filters/dot-syntax.test.d.ts +2 -0
  125. package/dist/tests/filters/dot-syntax.test.d.ts.map +1 -0
  126. package/dist/tests/filters/dot-syntax.test.js +210 -0
  127. package/dist/tests/filters/dot-syntax.test.js.map +1 -0
  128. package/dist/tests/filters/exists.test.d.ts +2 -0
  129. package/dist/tests/filters/exists.test.d.ts.map +1 -0
  130. package/dist/tests/filters/exists.test.js +106 -0
  131. package/dist/tests/filters/exists.test.js.map +1 -0
  132. package/dist/tests/filters/joined-relations.test.d.ts +2 -0
  133. package/dist/tests/filters/joined-relations.test.d.ts.map +1 -0
  134. package/dist/tests/filters/joined-relations.test.js +167 -0
  135. package/dist/tests/filters/joined-relations.test.js.map +1 -0
  136. package/dist/tests/filters/special-cases.test.d.ts +2 -0
  137. package/dist/tests/filters/special-cases.test.d.ts.map +1 -0
  138. package/dist/tests/filters/special-cases.test.js +114 -0
  139. package/dist/tests/filters/special-cases.test.js.map +1 -0
  140. package/dist/tests/filters/wildcard.test.d.ts +2 -0
  141. package/dist/tests/filters/wildcard.test.d.ts.map +1 -0
  142. package/dist/tests/filters/wildcard.test.js +67 -0
  143. package/dist/tests/filters/wildcard.test.js.map +1 -0
  144. package/dist/tests/jest.global.setup.d.ts +3 -0
  145. package/dist/tests/jest.global.setup.d.ts.map +1 -0
  146. package/dist/tests/jest.global.setup.js +7 -0
  147. package/dist/tests/jest.global.setup.js.map +1 -0
  148. package/dist/tests/jest.setup.d.ts +2 -0
  149. package/dist/tests/jest.setup.d.ts.map +1 -0
  150. package/dist/tests/jest.setup.js +5 -0
  151. package/dist/tests/jest.setup.js.map +1 -0
  152. package/dist/tests/utils/index.d.ts +57 -0
  153. package/dist/tests/utils/index.d.ts.map +1 -0
  154. package/dist/tests/utils/index.js +206 -0
  155. package/dist/tests/utils/index.js.map +1 -0
  156. package/dist/tsconfig.tsbuildinfo +1 -1
  157. package/package.json +4 -3
  158. package/src/QueryableModel.ts +22 -2
  159. package/src/SQL.ts +21 -30
  160. package/src/SQLDelete.ts +26 -15
  161. package/src/SQLExpression.ts +4 -0
  162. package/src/SQLExpressions.ts +23 -14
  163. package/src/SQLJoin.ts +8 -4
  164. package/src/SQLJsonExpressions.ts +65 -4
  165. package/src/SQLSelect.ts +31 -15
  166. package/src/SQLWhere.ts +208 -13
  167. package/src/filters/SQLFilter.ts +8 -0
  168. package/src/filters/modern/SQLModernFilter.ts +256 -0
  169. package/src/filters/modern/compilers/contains.ts +43 -0
  170. package/src/filters/modern/compilers/equals.ts +72 -0
  171. package/src/filters/modern/compilers/greater.ts +20 -0
  172. package/src/filters/modern/compilers/in.ts +62 -0
  173. package/src/filters/modern/compilers/index.ts +4 -0
  174. package/src/filters/modern/compilers/less.ts +19 -0
  175. package/src/filters/modern/helpers/isJSONColumn.ts +13 -0
  176. package/src/filters/modern/helpers/normalizeCompareValue.ts +95 -0
package/src/SQLSelect.ts CHANGED
@@ -1,22 +1,19 @@
1
1
  import { Database, SQLResultNamespacedRow } from '@simonbackx/simple-database';
2
- import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery, normalizeSQLQuery } from './SQLExpression';
2
+ import { Formatter } from '@stamhoofd/utility';
3
+ import { SQLExpression, SQLExpressionOptions, SQLNamedExpression, SQLQuery, joinSQLQuery, normalizeSQLQuery } from './SQLExpression';
3
4
  import { SQLAlias, SQLColumnExpression, SQLCount, SQLSelectAs, SQLSum, SQLTableExpression } from './SQLExpressions';
4
5
  import { SQLJoin } from './SQLJoin';
5
6
  import { Orderable } from './SQLOrderBy';
6
7
  import { Whereable } from './SQLWhere';
7
- import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  class EmptyClass {}
10
10
 
11
- export function parseTable(tableOrExpressiongOrNamespace: SQLExpression | string, table?: string): SQLExpression {
12
- if (table !== undefined && typeof tableOrExpressiongOrNamespace === 'string') {
13
- return new SQLTableExpression(tableOrExpressiongOrNamespace, table);
14
- }
15
- else if (typeof tableOrExpressiongOrNamespace === 'string') {
16
- return new SQLTableExpression(tableOrExpressiongOrNamespace);
11
+ export function parseTable(tableOrExpression: SQLNamedExpression | string, asNamespace?: string): SQLNamedExpression {
12
+ if (typeof tableOrExpression === 'string') {
13
+ return new SQLTableExpression(tableOrExpression, asNamespace);
17
14
  }
18
15
  else {
19
- return tableOrExpressiongOrNamespace;
16
+ return tableOrExpression;
20
17
  }
21
18
  }
22
19
 
@@ -34,7 +31,7 @@ export type IterableSQLSelectOptions = {
34
31
 
35
32
  export class SQLSelect<T extends object = SQLResultNamespacedRow> extends Whereable(Orderable(EmptyClass)) implements SQLExpression {
36
33
  _columns: SQLExpression[];
37
- _from: SQLExpression;
34
+ _from: SQLNamedExpression;
38
35
 
39
36
  _limit: number | null = null;
40
37
  _offset: number | null = null;
@@ -63,8 +60,8 @@ export class SQLSelect<T extends object = SQLResultNamespacedRow> extends Wherea
63
60
 
64
61
  from(namespace: string, table: string): this;
65
62
  from(table: string): this;
66
- from(expression: SQLExpression): this;
67
- from(tableOrExpressiongOrNamespace: SQLExpression | string, table?: string): this {
63
+ from(expression: SQLNamedExpression): this;
64
+ from(tableOrExpressiongOrNamespace: SQLNamedExpression | string, table?: string): this {
68
65
  this._from = parseTable(tableOrExpressiongOrNamespace, table);
69
66
 
70
67
  return this;
@@ -102,7 +99,7 @@ export class SQLSelect<T extends object = SQLResultNamespacedRow> extends Wherea
102
99
  // Create a clone since we are mutating the default namespaces
103
100
  const parentOptions = options;
104
101
  options = options ? { ...options } : {};
105
- options.defaultNamespace = (this._from as any).namespace ?? (this._from as any).table ?? undefined;
102
+ options.defaultNamespace = this._from.getName();
106
103
 
107
104
  if (parentOptions?.defaultNamespace) {
108
105
  options.parentNamespace = parentOptions.defaultNamespace;
@@ -128,8 +125,14 @@ export class SQLSelect<T extends object = SQLResultNamespacedRow> extends Wherea
128
125
 
129
126
  // Where
130
127
  if (this._where) {
131
- query.push('WHERE');
132
- query.push(this._where.getSQL(options));
128
+ const always = this._where.isAlways;
129
+ if (always === false) {
130
+ throw new Error('Cannot use SQLSelect with a where that is not always true');
131
+ }
132
+ else if (always === null) {
133
+ query.push('WHERE');
134
+ query.push(this._where.getSQL(options));
135
+ }
133
136
  }
134
137
 
135
138
  if (this._groupBy.length > 0) {
@@ -156,6 +159,15 @@ export class SQLSelect<T extends object = SQLResultNamespacedRow> extends Wherea
156
159
  return joinSQLQuery(query, ' ');
157
160
  }
158
161
 
162
+ /**
163
+ * Returns true when it know all results will be included without filtering.
164
+ * Returns false when it knows no single result will be included.
165
+ * Null when it does not know.
166
+ */
167
+ get isAlways() {
168
+ return this._where ? this._where.isAlways : true;
169
+ }
170
+
159
171
  limit(limit: number | null, offset: number | null = null): this {
160
172
  this._limit = limit;
161
173
  this._offset = offset;
@@ -163,6 +175,10 @@ export class SQLSelect<T extends object = SQLResultNamespacedRow> extends Wherea
163
175
  }
164
176
 
165
177
  async fetch(): Promise<T[]> {
178
+ if (this._where && this._where.isAlways === false) {
179
+ return [];
180
+ }
181
+
166
182
  const { query, params } = normalizeSQLQuery(this.getSQL());
167
183
 
168
184
  // when debugging: log all queries
package/src/SQLWhere.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SQLExpression, SQLExpressionOptions, SQLQuery, joinSQLQuery, normalizeSQLQuery } from './SQLExpression';
2
2
  import { SQLArray, SQLColumnExpression, SQLDynamicExpression, SQLNull, readDynamicSQLExpression } from './SQLExpressions';
3
- import { SQLJoin } from './SQLJoin';
3
+ import { SQLJoin, SQLJoinType } from './SQLJoin';
4
+ import { SQLSelect } from './SQLSelect';
4
5
 
5
6
  type Constructor<T = {}> = new (...args: any[]) => T;
6
7
 
@@ -121,6 +122,18 @@ export abstract class SQLWhere implements SQLExpression {
121
122
  return false;
122
123
  }
123
124
 
125
+ get isAlways(): boolean | null {
126
+ return null;
127
+ }
128
+
129
+ get isAlwaysTrue(): boolean {
130
+ return this.isAlways === true;
131
+ }
132
+
133
+ get isAlwaysFalse(): boolean {
134
+ return this.isAlways === false;
135
+ }
136
+
124
137
  abstract getSQL(options?: SQLExpressionOptions): SQLQuery;
125
138
  getJoins(): SQLJoin[] {
126
139
  return [];
@@ -151,12 +164,17 @@ export class SQLEmptyWhere extends SQLWhere {
151
164
  getSQL(options?: SQLExpressionOptions): SQLQuery {
152
165
  throw new Error('Empty where');
153
166
  }
167
+
168
+ get isAlways() {
169
+ return true;
170
+ }
154
171
  }
155
172
 
156
173
  export class SQLWhereEqual extends SQLWhere {
157
174
  column: SQLExpression;
158
175
  sign = SQLWhereSign.Equal;
159
176
  value: SQLExpression;
177
+ nullable = false;
160
178
 
161
179
  static parseWhere(...parsed: ParseWhereArguments): SQLWhere {
162
180
  if (parsed[1] === undefined) {
@@ -199,7 +217,11 @@ export class SQLWhereEqual extends SQLWhere {
199
217
  }
200
218
 
201
219
  get isSingle(): boolean {
202
- return true;
220
+ return this.transformed?.isSingle ?? true;
221
+ }
222
+
223
+ get isAlways(): boolean | null {
224
+ return this.transformed?.isAlways ?? null;
203
225
  }
204
226
 
205
227
  inverted(): this {
@@ -230,7 +252,55 @@ export class SQLWhereEqual extends SQLWhere {
230
252
  return this;
231
253
  }
232
254
 
255
+ setNullable(nullable: boolean = true): this {
256
+ this.nullable = nullable;
257
+ return this;
258
+ }
259
+
260
+ get transformed() {
261
+ if (this.value instanceof SQLNull) {
262
+ // We'll do some transformations to make this query work as expected.
263
+ // < null = always false
264
+ // > null = (IS NOT null)
265
+ // <= null = (IS null)
266
+ // >= null = always true
267
+ if (this.sign === SQLWhereSign.Less) {
268
+ // always false
269
+ return new SQLWhereOr([]);
270
+ }
271
+ if (this.sign === SQLWhereSign.Greater) {
272
+ // > null = (IS NOT null)
273
+ return new SQLWhereEqual(this.column, SQLWhereSign.NotEqual, this.value);
274
+ }
275
+ if (this.sign === SQLWhereSign.LessEqual) {
276
+ // (IS null)
277
+ return new SQLWhereEqual(this.column, SQLWhereSign.Equal, this.value);
278
+ }
279
+ if (this.sign === SQLWhereSign.GreaterEqual) {
280
+ // always true
281
+ return new SQLWhereAnd([]);
282
+ }
283
+ }
284
+
285
+ // If the expression is nullable, we'll need to do some handling to make sure the query works as expected.
286
+ if (this.nullable && !(this.value instanceof SQLNull)) {
287
+ // <: should also include null values
288
+ // <=: should also include null values
289
+ if (this.sign === SQLWhereSign.Less || this.sign === SQLWhereSign.LessEqual) {
290
+ return new SQLWhereOr([
291
+ this.clone().setNullable(false),
292
+ new SQLWhereEqual(this.column, SQLWhereSign.Equal, new SQLNull()),
293
+ ]);
294
+ }
295
+ }
296
+
297
+ return null;
298
+ }
299
+
233
300
  getSQL(options?: SQLExpressionOptions): SQLQuery {
301
+ if (this.transformed) {
302
+ return this.transformed.getSQL(options);
303
+ }
234
304
  if (this.value instanceof SQLArray) {
235
305
  if (this.sign !== SQLWhereSign.Equal && this.sign !== SQLWhereSign.NotEqual) {
236
306
  throw new Error('Unsupported sign for array: ' + this.sign);
@@ -250,7 +320,7 @@ export class SQLWhereEqual extends SQLWhere {
250
320
 
251
321
  return joinSQLQuery([
252
322
  this.column.getSQL(options),
253
- ` IS ${(this.sign === SQLWhereSign.NotEqual) ? 'NOT ' : ''} `,
323
+ ` IS ${(this.sign === SQLWhereSign.NotEqual) ? 'NOT ' : ''}`,
254
324
  this.value.getSQL(options),
255
325
  ]);
256
326
  }
@@ -275,7 +345,7 @@ export class SQLWhereLike extends SQLWhere {
275
345
  }
276
346
 
277
347
  static escape(str: string) {
278
- return str.replace(/([%_])/g, '\\$1');
348
+ return str.replace(/([%_\\])/g, '\\$1');
279
349
  }
280
350
 
281
351
  clone(): this {
@@ -385,6 +455,24 @@ export class SQLWhereExists extends SQLWhere {
385
455
  return this;
386
456
  }
387
457
 
458
+ get isAlways(): boolean | null {
459
+ if (this.subquery instanceof SQLSelect) {
460
+ const value = this.subquery.isAlways;
461
+ if (this.notExists) {
462
+ if (value === true) {
463
+ // If the subquery is always true, then NOT EXISTS is always false
464
+ return false;
465
+ }
466
+ if (value === false) {
467
+ // If the subquery is always false, then NOT EXISTS is always true
468
+ return true;
469
+ }
470
+ }
471
+ return value;
472
+ }
473
+ return null;
474
+ }
475
+
388
476
  getSQL(options?: SQLExpressionOptions): SQLQuery {
389
477
  return joinSQLQuery([
390
478
  `${this.notExists ? 'NOT EXISTS' : 'EXISTS'} (`,
@@ -401,22 +489,45 @@ export class SQLWhereJoin extends SQLWhere {
401
489
  join: SQLJoin;
402
490
  where: SQLWhere;
403
491
 
404
- constructor(join: SQLJoin, where: SQLWhere) {
492
+ /**
493
+ * When this is true, this means we know this relation will always exist.
494
+ *
495
+ * This information will be used to optimize the query.
496
+ */
497
+ doesRelationAlwaysExist = false;
498
+
499
+ constructor(join: SQLJoin, where: SQLWhere, options?: { doesRelationAlwaysExist?: boolean }) {
405
500
  super();
406
501
  this.join = join;
407
502
  this.where = where;
503
+ this.doesRelationAlwaysExist = options?.doesRelationAlwaysExist ?? false;
408
504
  }
409
505
 
410
506
  get isSingle(): boolean {
411
507
  return this.where.isSingle;
412
508
  }
413
509
 
510
+ get isAlways(): boolean | null {
511
+ return this.where.isAlways;
512
+ }
513
+
414
514
  getSQL(options?: SQLExpressionOptions): SQLQuery {
415
- return this.where.getSQL(options);
515
+ if (this.where.isAlways !== null && (this.doesRelationAlwaysExist || this.join.type === SQLJoinType.Left)) {
516
+ throw new Error('SQLWhereJoin: should not be included in query if result is determined');
517
+ }
518
+
519
+ return this.where.getSQL({
520
+ ...options,
521
+ parentNamespace: options?.defaultNamespace,
522
+ defaultNamespace: this.join.table.getName(),
523
+ });
416
524
  }
417
525
 
418
526
  getJoins(): SQLJoin[] {
419
- return [this.join];
527
+ if (this.where.isAlways !== null && (this.doesRelationAlwaysExist || this.join.type === SQLJoinType.Left)) {
528
+ return [];
529
+ }
530
+ return [this.join, ...this.where.getJoins()];
420
531
  }
421
532
  }
422
533
 
@@ -429,9 +540,13 @@ export class SQLWhereAnd extends SQLWhere {
429
540
  }
430
541
 
431
542
  getSQL(options?: SQLExpressionOptions): SQLQuery {
543
+ if (this.isAlways === false) {
544
+ throw new Error('SQLWhereAnd: $and is always false and should be removed from the query');
545
+ }
546
+
432
547
  return joinSQLQuery(
433
- this.children.map((c) => {
434
- if (c.isSingle) {
548
+ this.filteredChildren.map((c) => {
549
+ if (c.isSingle || this.filteredChildren.length === 1) {
435
550
  return c.getSQL(options);
436
551
  }
437
552
  return joinSQLQuery(['(', c.getSQL(options), ')']);
@@ -440,8 +555,38 @@ export class SQLWhereAnd extends SQLWhere {
440
555
  );
441
556
  }
442
557
 
558
+ get filteredChildren(): SQLWhere[] {
559
+ // Children that always return true should not be included in the query (because the result only depends on the other children)
560
+ return this.children.filter(c => c.isAlways !== true).flatMap(c => c instanceof SQLWhereAnd ? c.filteredChildren : [c]);
561
+ }
562
+
443
563
  getJoins(): SQLJoin[] {
444
- return this.children.flatMap(c => c.getJoins());
564
+ return this.children.flatMap(c => c.getJoins()); // note: keep all joins
565
+ }
566
+
567
+ get isSingle(): boolean {
568
+ return this.filteredChildren.length === 1 && this.filteredChildren[0].isSingle;
569
+ }
570
+
571
+ get isAlways(): boolean | null {
572
+ let allTrue = true;
573
+ for (const c of this.children) {
574
+ const v = c.isAlways;
575
+ if (v === false) {
576
+ // If any child is always false, the whole AND is false
577
+ return false;
578
+ }
579
+ if (v === null) {
580
+ allTrue = false;
581
+ }
582
+ }
583
+
584
+ return allTrue ? true : null;
585
+ }
586
+
587
+ inverted(): SQLWhereOr {
588
+ // NOT (A AND B) is the same as (NOT A OR NOT B)
589
+ return new SQLWhereOr(this.children.map(c => new SQLWhereNot(c)));
445
590
  }
446
591
  }
447
592
 
@@ -454,9 +599,14 @@ export class SQLWhereOr extends SQLWhere {
454
599
  }
455
600
 
456
601
  getSQL(options?: SQLExpressionOptions): SQLQuery {
602
+ if (this.filteredChildren.length === 0) {
603
+ // Always false: throw an error (the parent should filter out this query)
604
+ throw new Error('SQLWhereOr: empty $or is always false and should be removed from the query');
605
+ }
606
+
457
607
  return joinSQLQuery(
458
- this.children.map((c) => {
459
- if (c.isSingle) {
608
+ this.filteredChildren.map((c) => {
609
+ if (c.isSingle || this.filteredChildren.length === 1) {
460
610
  return c.getSQL(options);
461
611
  }
462
612
  return joinSQLQuery(['(', c.getSQL(options), ')']);
@@ -468,6 +618,36 @@ export class SQLWhereOr extends SQLWhere {
468
618
  getJoins(): SQLJoin[] {
469
619
  return this.children.flatMap(c => c.getJoins());
470
620
  }
621
+
622
+ get filteredChildren(): SQLWhere[] {
623
+ // Children that always return false should not be included in the query (because the result only depends on the other children)
624
+ return this.children.filter(c => c.isAlways !== false).flatMap(c => c instanceof SQLWhereOr ? c.filteredChildren : [c]);
625
+ }
626
+
627
+ get isSingle(): boolean {
628
+ return this.filteredChildren.length === 1 && this.filteredChildren[0].isSingle;
629
+ }
630
+
631
+ get isAlways(): boolean | null {
632
+ let isAllFalse = true;
633
+ for (const c of this.children) {
634
+ const v = c.isAlways;
635
+ if (v === true) {
636
+ // If any child is always true, the whole OR is true
637
+ return true;
638
+ }
639
+ if (v === null) {
640
+ isAllFalse = false;
641
+ }
642
+ }
643
+
644
+ return isAllFalse ? false : null;
645
+ }
646
+
647
+ inverted(): SQLWhereOr {
648
+ // NOT (A OR B) is the same as (NOT A AND NOT B)
649
+ return new SQLWhereAnd(this.children.map(c => new SQLWhereNot(c)));
650
+ }
471
651
  }
472
652
 
473
653
  export class SQLWhereNot extends SQLWhere {
@@ -484,7 +664,7 @@ export class SQLWhereNot extends SQLWhere {
484
664
 
485
665
  getSQL(options?: SQLExpressionOptions): SQLQuery {
486
666
  // Optimize query
487
- if (this.a instanceof SQLWhereEqual) {
667
+ if (this.a instanceof SQLWhereEqual || this.a instanceof SQLWhereAnd || this.a instanceof SQLWhereOr || this.a instanceof SQLWhereNot) {
488
668
  return this.a.inverted().getSQL(options);
489
669
  }
490
670
 
@@ -499,4 +679,19 @@ export class SQLWhereNot extends SQLWhere {
499
679
  getJoins(): SQLJoin[] {
500
680
  return this.a.getJoins();
501
681
  }
682
+
683
+ get isAlways(): boolean | null {
684
+ const v = this.a.isAlways;
685
+ if (v === true) {
686
+ return false;
687
+ }
688
+ if (v === false) {
689
+ return true;
690
+ }
691
+ return null;
692
+ }
693
+
694
+ inverted(): SQLWhere {
695
+ return this.a; // NOT NOT A is just A
696
+ }
502
697
  }
@@ -137,6 +137,14 @@ export function createSQLRelationFilterCompiler(baseSelect: InstanceType<typeof
137
137
  };
138
138
  }
139
139
 
140
+ export function createSQLOneToOneRelationFilterCompiler(baseSelect: InstanceType<typeof SQLSelect> & SQLExpression, definitions: SQLFilterDefinitions): SQLFilterCompiler {
141
+ return async (filter: StamhoofdFilter) => {
142
+ const w = await compileToSQLFilter(filter, definitions);
143
+ const q = baseSelect.clone().where(w);
144
+ return new SQLWhereExists(q);
145
+ };
146
+ }
147
+
140
148
  // Already joined, but creates a namespace
141
149
  export function createSQLFilterNamespace(definitions: SQLFilterDefinitions): SQLFilterCompiler {
142
150
  return (filter: StamhoofdFilter) => {