joist-core 2.1.0 → 2.2.0-next.10

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 (217) hide show
  1. package/build/BaseEntity.js +1 -1
  2. package/build/BaseEntity.js.map +1 -1
  3. package/build/ConditionBuilder.d.ts +1 -22
  4. package/build/ConditionBuilder.d.ts.map +1 -1
  5. package/build/ConditionBuilder.js +6 -72
  6. package/build/ConditionBuilder.js.map +1 -1
  7. package/build/EntityFilter.d.ts +4 -2
  8. package/build/EntityFilter.d.ts.map +1 -1
  9. package/build/EntityGraphQLFilter.d.ts +2 -0
  10. package/build/EntityGraphQLFilter.d.ts.map +1 -1
  11. package/build/EntityGraphQLFilter.js.map +1 -1
  12. package/build/EntityManager.d.ts +27 -39
  13. package/build/EntityManager.d.ts.map +1 -1
  14. package/build/EntityManager.js +243 -122
  15. package/build/EntityManager.js.map +1 -1
  16. package/build/EntityMetadata.d.ts +19 -1
  17. package/build/EntityMetadata.d.ts.map +1 -1
  18. package/build/EntityMetadata.js.map +1 -1
  19. package/build/IndexManager.d.ts +2 -2
  20. package/build/IndexManager.d.ts.map +1 -1
  21. package/build/IndexManager.js +22 -16
  22. package/build/IndexManager.js.map +1 -1
  23. package/build/InstanceData.d.ts +21 -3
  24. package/build/InstanceData.d.ts.map +1 -1
  25. package/build/InstanceData.js +70 -10
  26. package/build/InstanceData.js.map +1 -1
  27. package/build/IsLoadedCache.d.ts +1 -1
  28. package/build/IsLoadedCache.d.ts.map +1 -1
  29. package/build/IsLoadedCache.js +37 -26
  30. package/build/IsLoadedCache.js.map +1 -1
  31. package/build/JoinRows.d.ts +1 -1
  32. package/build/JoinRows.js +6 -1
  33. package/build/JoinRows.js.map +1 -1
  34. package/build/PluginManager.d.ts +12 -0
  35. package/build/PluginManager.d.ts.map +1 -1
  36. package/build/PluginManager.js +18 -2
  37. package/build/PluginManager.js.map +1 -1
  38. package/build/QueryParser.collectionJoins.d.ts +27 -0
  39. package/build/QueryParser.collectionJoins.d.ts.map +1 -0
  40. package/build/QueryParser.collectionJoins.js +466 -0
  41. package/build/QueryParser.collectionJoins.js.map +1 -0
  42. package/build/QueryParser.collectionJoins.test.d.ts +2 -0
  43. package/build/QueryParser.collectionJoins.test.d.ts.map +1 -0
  44. package/build/QueryParser.collectionJoins.test.js +772 -0
  45. package/build/QueryParser.collectionJoins.test.js.map +1 -0
  46. package/build/QueryParser.d.ts +71 -11
  47. package/build/QueryParser.d.ts.map +1 -1
  48. package/build/QueryParser.js +39 -33
  49. package/build/QueryParser.js.map +1 -1
  50. package/build/QueryParser.pruning.d.ts +4 -2
  51. package/build/QueryParser.pruning.d.ts.map +1 -1
  52. package/build/QueryParser.pruning.js +87 -10
  53. package/build/QueryParser.pruning.js.map +1 -1
  54. package/build/QueryParser.pruning.test.d.ts +2 -0
  55. package/build/QueryParser.pruning.test.d.ts.map +1 -0
  56. package/build/QueryParser.pruning.test.js +106 -0
  57. package/build/QueryParser.pruning.test.js.map +1 -0
  58. package/build/QueryVisitor.d.ts.map +1 -1
  59. package/build/QueryVisitor.js +22 -0
  60. package/build/QueryVisitor.js.map +1 -1
  61. package/build/ReactionsManager.d.ts +2 -1
  62. package/build/ReactionsManager.d.ts.map +1 -1
  63. package/build/ReactionsManager.js +55 -51
  64. package/build/ReactionsManager.js.map +1 -1
  65. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  66. package/build/batchloaders/BatchLoader.js +9 -1
  67. package/build/batchloaders/BatchLoader.js.map +1 -1
  68. package/build/batchloaders/manyToManyBatchLoader.js +3 -1
  69. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  70. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  71. package/build/batchloaders/populateBatchLoader.js +2 -1
  72. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  73. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  74. package/build/batchloaders/recursiveM2mBatchLoader.js +3 -1
  75. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  76. package/build/changes.d.ts.map +1 -1
  77. package/build/changes.js +1 -4
  78. package/build/changes.js.map +1 -1
  79. package/build/config.d.ts.map +1 -1
  80. package/build/config.js +18 -10
  81. package/build/config.js.map +1 -1
  82. package/build/configure.d.ts +3 -3
  83. package/build/configure.d.ts.map +1 -1
  84. package/build/configure.js +66 -2
  85. package/build/configure.js.map +1 -1
  86. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  87. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  88. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  89. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  90. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  91. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  92. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  93. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  94. package/build/dataloaders/findCountDataLoader.d.ts +1 -2
  95. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  96. package/build/dataloaders/findCountDataLoader.js +16 -13
  97. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  98. package/build/dataloaders/findDataLoader.d.ts +7 -3
  99. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  100. package/build/dataloaders/findDataLoader.js +105 -91
  101. package/build/dataloaders/findDataLoader.js.map +1 -1
  102. package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
  103. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  104. package/build/dataloaders/findIdsDataLoader.js +16 -15
  105. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  106. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  107. package/build/dataloaders/findOrCreateDataLoader.js +7 -2
  108. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  109. package/build/dataloaders/findPaginatedDataLoader.d.ts +7 -0
  110. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -0
  111. package/build/dataloaders/findPaginatedDataLoader.js +79 -0
  112. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -0
  113. package/build/defaults.d.ts.map +1 -1
  114. package/build/defaults.js +49 -42
  115. package/build/defaults.js.map +1 -1
  116. package/build/drivers/EntityWriter.js +13 -7
  117. package/build/drivers/EntityWriter.js.map +1 -1
  118. package/build/drivers/buildRawQuery.d.ts +6 -4
  119. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  120. package/build/drivers/buildRawQuery.js +11 -6
  121. package/build/drivers/buildRawQuery.js.map +1 -1
  122. package/build/drivers/buildUtils.d.ts +7 -2
  123. package/build/drivers/buildUtils.d.ts.map +1 -1
  124. package/build/drivers/buildUtils.js +14 -5
  125. package/build/drivers/buildUtils.js.map +1 -1
  126. package/build/fields.d.ts.map +1 -1
  127. package/build/fields.js +31 -12
  128. package/build/fields.js.map +1 -1
  129. package/build/index.d.ts +2 -1
  130. package/build/index.d.ts.map +1 -1
  131. package/build/index.js +7 -5
  132. package/build/index.js.map +1 -1
  133. package/build/json.d.ts +3 -3
  134. package/build/json.d.ts.map +1 -1
  135. package/build/json.js +4 -4
  136. package/build/json.js.map +1 -1
  137. package/build/keys.d.ts.map +1 -1
  138. package/build/keys.js +8 -6
  139. package/build/keys.js.map +1 -1
  140. package/build/loadHints.d.ts +13 -4
  141. package/build/loadHints.d.ts.map +1 -1
  142. package/build/loadHints.js +63 -9
  143. package/build/loadHints.js.map +1 -1
  144. package/build/loadLens.d.ts +11 -0
  145. package/build/loadLens.d.ts.map +1 -1
  146. package/build/loadLens.js +25 -8
  147. package/build/loadLens.js.map +1 -1
  148. package/build/normalizeHints.d.ts +7 -0
  149. package/build/normalizeHints.d.ts.map +1 -1
  150. package/build/normalizeHints.js +40 -2
  151. package/build/normalizeHints.js.map +1 -1
  152. package/build/preloading/JsonAggregatePreloader.js +6 -2
  153. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  154. package/build/reactiveHints.d.ts +24 -7
  155. package/build/reactiveHints.d.ts.map +1 -1
  156. package/build/reactiveHints.js +45 -28
  157. package/build/reactiveHints.js.map +1 -1
  158. package/build/relations/AbstractRelationImpl.d.ts +7 -2
  159. package/build/relations/AbstractRelationImpl.d.ts.map +1 -1
  160. package/build/relations/AbstractRelationImpl.js.map +1 -1
  161. package/build/relations/AsyncProperty.d.ts +36 -0
  162. package/build/relations/AsyncProperty.d.ts.map +1 -0
  163. package/build/relations/AsyncProperty.js +80 -0
  164. package/build/relations/AsyncProperty.js.map +1 -0
  165. package/build/relations/{ReactiveQueryField.d.ts → AsyncReactiveField.d.ts} +10 -10
  166. package/build/relations/AsyncReactiveField.d.ts.map +1 -0
  167. package/build/relations/{ReactiveQueryField.js → AsyncReactiveField.js} +19 -19
  168. package/build/relations/{ReactiveQueryField.js.map → AsyncReactiveField.js.map} +1 -1
  169. package/build/relations/ReactiveField.d.ts +7 -9
  170. package/build/relations/ReactiveField.d.ts.map +1 -1
  171. package/build/relations/ReactiveField.js +5 -10
  172. package/build/relations/ReactiveField.js.map +1 -1
  173. package/build/relations/ReactiveGetter.d.ts +5 -5
  174. package/build/relations/ReactiveGetter.d.ts.map +1 -1
  175. package/build/relations/ReactiveGetter.js +3 -3
  176. package/build/relations/ReactiveGetter.js.map +1 -1
  177. package/build/relations/ReactiveReference.d.ts +2 -2
  178. package/build/relations/ReactiveReference.d.ts.map +1 -1
  179. package/build/relations/ReactiveReference.js +100 -36
  180. package/build/relations/ReactiveReference.js.map +1 -1
  181. package/build/relations/hasOneThrough.d.ts.map +1 -1
  182. package/build/relations/hasOneThrough.js +6 -4
  183. package/build/relations/hasOneThrough.js.map +1 -1
  184. package/build/relations/{hasAsyncProperty.d.ts → hasProperty.d.ts} +12 -12
  185. package/build/relations/hasProperty.d.ts.map +1 -0
  186. package/build/relations/{hasAsyncProperty.js → hasProperty.js} +20 -20
  187. package/build/relations/hasProperty.js.map +1 -0
  188. package/build/relations/index.d.ts +3 -2
  189. package/build/relations/index.d.ts.map +1 -1
  190. package/build/relations/index.js +16 -11
  191. package/build/relations/index.js.map +1 -1
  192. package/build/resurrection.d.ts +10 -0
  193. package/build/resurrection.d.ts.map +1 -0
  194. package/build/resurrection.js +93 -0
  195. package/build/resurrection.js.map +1 -0
  196. package/build/rules.js +3 -3
  197. package/build/trusted.d.ts +1 -1
  198. package/build/trusted.d.ts.map +1 -1
  199. package/build/trusted.js +1 -1
  200. package/build/trusted.js.map +1 -1
  201. package/build/upsert.d.ts.map +1 -1
  202. package/build/upsert.js +26 -10
  203. package/build/upsert.js.map +1 -1
  204. package/build/utils.d.ts +2 -0
  205. package/build/utils.d.ts.map +1 -1
  206. package/build/utils.js +12 -0
  207. package/build/utils.js.map +1 -1
  208. package/build/withLoaded.js +5 -5
  209. package/build/withLoaded.js.map +1 -1
  210. package/package.json +10 -12
  211. package/build/caches.d.ts +0 -6
  212. package/build/caches.d.ts.map +0 -1
  213. package/build/caches.js +0 -42
  214. package/build/caches.js.map +0 -1
  215. package/build/relations/ReactiveQueryField.d.ts.map +0 -1
  216. package/build/relations/hasAsyncProperty.d.ts.map +0 -1
  217. package/build/relations/hasAsyncProperty.js.map +0 -1
@@ -0,0 +1,772 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const QueryParser_collectionJoins_1 = require("./QueryParser.collectionJoins");
4
+ describe("QueryParser.collectionJoins", () => {
5
+ it("allows unblocked multiple collection left joins before pruning", () => {
6
+ const query = {
7
+ selects: ["a.*"],
8
+ tables: [
9
+ { alias: "a", table: "authors", join: "primary" },
10
+ {
11
+ alias: "b",
12
+ table: "books",
13
+ join: "outer",
14
+ col1: "a.id",
15
+ col2: "b.author_id",
16
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
17
+ },
18
+ {
19
+ alias: "c",
20
+ table: "comments",
21
+ join: "outer",
22
+ col1: "a.id",
23
+ col2: "c.parent_author_id",
24
+ collection: { parentAlias: "a", rootAlias: "c", kind: "o2m" },
25
+ },
26
+ ],
27
+ orderBys: [],
28
+ };
29
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query, { pruneJoins: false });
30
+ expect(query.tables).toMatchObject([
31
+ { alias: "a", table: "authors", join: "primary" },
32
+ { alias: "b", table: "books", join: "outer" },
33
+ { alias: "c", table: "comments", join: "outer" },
34
+ ]);
35
+ });
36
+ it("prunes unblocked multiple collection left joins after checking fanout safety", () => {
37
+ const query = {
38
+ selects: ["a.*"],
39
+ tables: [
40
+ { alias: "a", table: "authors", join: "primary" },
41
+ {
42
+ alias: "b",
43
+ table: "books",
44
+ join: "outer",
45
+ col1: "a.id",
46
+ col2: "b.author_id",
47
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
48
+ },
49
+ {
50
+ alias: "c",
51
+ table: "comments",
52
+ join: "outer",
53
+ col1: "a.id",
54
+ col2: "c.parent_author_id",
55
+ collection: { parentAlias: "a", rootAlias: "c", kind: "o2m" },
56
+ },
57
+ ],
58
+ orderBys: [],
59
+ };
60
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
61
+ expect(query.tables).toMatchObject([{ alias: "a", table: "authors", join: "primary" }]);
62
+ expect(query.condition).toBeUndefined();
63
+ });
64
+ it("skips join-to-exists optimization when disabled", () => {
65
+ const query = {
66
+ selects: ["a.*"],
67
+ tables: [
68
+ { alias: "a", table: "authors", join: "primary" },
69
+ {
70
+ alias: "b",
71
+ table: "books",
72
+ join: "outer",
73
+ col1: "a.id",
74
+ col2: "b.author_id",
75
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
76
+ },
77
+ ],
78
+ condition: {
79
+ kind: "exp",
80
+ op: "and",
81
+ conditions: [{ kind: "column", alias: "b", column: "title", dbType: "text", cond: { kind: "eq", value: "b1" } }],
82
+ },
83
+ orderBys: [],
84
+ };
85
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query, { optimizeJoinsToExists: false });
86
+ expect(query.tables).toMatchObject([
87
+ { alias: "a", table: "authors", join: "primary" },
88
+ { alias: "b", table: "books", join: "outer" },
89
+ ]);
90
+ expect(query.condition).toMatchObject({
91
+ op: "and",
92
+ conditions: [{ kind: "column", alias: "b", column: "title" }],
93
+ });
94
+ });
95
+ it("does not rewrite sibling OR when an alias is selected outside the OR", () => {
96
+ const query = {
97
+ selects: ["a.*", "b.title as book_title"],
98
+ tables: [
99
+ { alias: "a", table: "authors", join: "primary" },
100
+ {
101
+ alias: "b",
102
+ table: "books",
103
+ join: "outer",
104
+ col1: "a.id",
105
+ col2: "b.author_id",
106
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
107
+ },
108
+ {
109
+ alias: "c",
110
+ table: "comments",
111
+ join: "outer",
112
+ col1: "a.id",
113
+ col2: "c.parent_author_id",
114
+ collection: { parentAlias: "a", rootAlias: "c", kind: "o2m" },
115
+ },
116
+ ],
117
+ condition: {
118
+ kind: "exp",
119
+ op: "or",
120
+ conditions: [
121
+ { kind: "column", alias: "b", column: "id", dbType: "int", cond: { kind: "not-null" } },
122
+ { kind: "column", alias: "c", column: "id", dbType: "int", cond: { kind: "not-null" } },
123
+ ],
124
+ },
125
+ orderBys: [],
126
+ };
127
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query, { allowMultipleLeftJoins: true });
128
+ expect(query.tables).toMatchObject([
129
+ { alias: "a", table: "authors", join: "primary" },
130
+ { alias: "b", table: "books", join: "outer" },
131
+ { alias: "c", table: "comments", join: "outer" },
132
+ ]);
133
+ expect(query.condition).toMatchObject({
134
+ op: "or",
135
+ conditions: [
136
+ { kind: "column", alias: "b", column: "id" },
137
+ { kind: "column", alias: "c", column: "id" },
138
+ ],
139
+ });
140
+ });
141
+ it("does not rewrite collection joins when selected aliases must remain in the outer query", () => {
142
+ const query = {
143
+ selects: ["b.id::text as id", "array_agg(br.rating::text) as review_ratings"],
144
+ tables: [
145
+ { alias: "a", table: "authors", join: "primary" },
146
+ {
147
+ alias: "b",
148
+ table: "books",
149
+ join: "outer",
150
+ col1: "a.id",
151
+ col2: "b.author_id",
152
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
153
+ },
154
+ {
155
+ alias: "br",
156
+ table: "book_reviews",
157
+ join: "outer",
158
+ col1: "b.id",
159
+ col2: "br.book_id",
160
+ collection: { parentAlias: "b", rootAlias: "br", kind: "o2m" },
161
+ },
162
+ ],
163
+ condition: {
164
+ kind: "exp",
165
+ op: "and",
166
+ conditions: [
167
+ {
168
+ kind: "column",
169
+ alias: "br",
170
+ column: "rating",
171
+ dbType: "int",
172
+ cond: { kind: "not-null" },
173
+ },
174
+ ],
175
+ },
176
+ groupBys: [{ alias: "b", column: "id" }],
177
+ orderBys: [],
178
+ };
179
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
180
+ expect(query.tables).toMatchObject([
181
+ { alias: "a", table: "authors", join: "primary" },
182
+ { alias: "b", table: "books", join: "outer" },
183
+ { alias: "br", table: "book_reviews", join: "outer" },
184
+ ]);
185
+ expect(JSON.stringify(query.condition).includes('"kind":"exists"')).toEqual(false);
186
+ });
187
+ it("does not rewrite nested collection joins referenced by raw aggregate selects", () => {
188
+ const query = {
189
+ selects: ["a.id::text as id", { sql: "array_agg(br.rating::text) as review_ratings", bindings: [], aliases: [] }],
190
+ tables: [
191
+ { alias: "a", table: "authors", join: "primary" },
192
+ {
193
+ alias: "b",
194
+ table: "books",
195
+ join: "outer",
196
+ col1: "a.id",
197
+ col2: "b.author_id",
198
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
199
+ },
200
+ {
201
+ alias: "br",
202
+ table: "book_reviews",
203
+ join: "outer",
204
+ col1: "b.id",
205
+ col2: "br.book_id",
206
+ collection: { parentAlias: "b", rootAlias: "br", kind: "o2m" },
207
+ },
208
+ ],
209
+ condition: {
210
+ kind: "exp",
211
+ op: "and",
212
+ conditions: [
213
+ {
214
+ kind: "column",
215
+ alias: "br",
216
+ column: "rating",
217
+ dbType: "int",
218
+ cond: { kind: "not-null" },
219
+ },
220
+ ],
221
+ },
222
+ groupBys: [{ alias: "a", column: "id" }],
223
+ orderBys: [{ alias: "a", column: "id", order: "ASC" }],
224
+ };
225
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
226
+ // I.e. even if the raw select's `aliases` metadata is empty, `array_agg(br.rating)` keeps `br` in the
227
+ // outer query; moving `br` under EXISTS would leave the aggregate with a dangling FROM reference.
228
+ expect(query.tables).toMatchObject([
229
+ { alias: "a", table: "authors", join: "primary" },
230
+ { alias: "b", table: "books", join: "outer" },
231
+ { alias: "br", table: "book_reviews", join: "outer" },
232
+ ]);
233
+ expect(JSON.stringify(query.condition).includes('"kind":"exists"')).toEqual(false);
234
+ });
235
+ it("does not rewrite m2m target joins referenced by raw aggregate selects", () => {
236
+ const query = {
237
+ selects: ["a.id::text as id", { sql: "array_agg(t.name::text) as tag_names", bindings: [], aliases: [] }],
238
+ tables: [
239
+ { alias: "a", table: "authors", join: "primary" },
240
+ {
241
+ alias: "att",
242
+ table: "authors_to_tags",
243
+ join: "outer",
244
+ col1: "a.id",
245
+ col2: "att.author_id",
246
+ collection: { parentAlias: "a", rootAlias: "att", kind: "m2m" },
247
+ },
248
+ {
249
+ alias: "t",
250
+ table: "tags",
251
+ join: "outer",
252
+ col1: "att.tag_id",
253
+ col2: "t.id",
254
+ collection: { parentAlias: "att", rootAlias: "att", kind: "m2m" },
255
+ },
256
+ ],
257
+ condition: {
258
+ kind: "exp",
259
+ op: "and",
260
+ conditions: [
261
+ {
262
+ kind: "column",
263
+ alias: "t",
264
+ column: "name",
265
+ dbType: "text",
266
+ cond: { kind: "not-null" },
267
+ },
268
+ ],
269
+ },
270
+ groupBys: [{ alias: "a", column: "id" }],
271
+ orderBys: [{ alias: "a", column: "id", order: "ASC" }],
272
+ };
273
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
274
+ // I.e. `t` is not the m2m collection root, but `array_agg(t.name)` still requires the `att -> t` join chain to
275
+ // stay in the outer query.
276
+ expect(query.tables).toMatchObject([
277
+ { alias: "a", table: "authors", join: "primary" },
278
+ { alias: "att", table: "authors_to_tags", join: "outer" },
279
+ { alias: "t", table: "tags", join: "outer" },
280
+ ]);
281
+ expect(JSON.stringify(query.condition).includes('"kind":"exists"')).toEqual(false);
282
+ });
283
+ it("keeps non-parent correlation aliases required by collection EXISTS", () => {
284
+ const query = {
285
+ selects: ["pp.*"],
286
+ tables: [
287
+ { alias: "pp", table: "plan_packages", join: "primary" },
288
+ {
289
+ alias: "pp_b0",
290
+ table: "ready_plans",
291
+ join: "outer",
292
+ col1: "pp.id",
293
+ col2: "pp_b0.id",
294
+ distinct: false,
295
+ },
296
+ {
297
+ alias: "_pp_b0_version",
298
+ table: "ready_plan_versions",
299
+ join: "outer",
300
+ col1: "pp_b0.id",
301
+ col2: "_pp_b0_version.identity_id",
302
+ },
303
+ {
304
+ alias: "_pp_version",
305
+ table: "plan_package_versions",
306
+ join: "outer",
307
+ col1: "_pp_b0_version.id",
308
+ col2: "_pp_version.id",
309
+ },
310
+ {
311
+ alias: "rpm",
312
+ table: "ready_plan_version_to_markets",
313
+ join: "outer",
314
+ col1: "_pp_version.id",
315
+ col2: "rpm.ready_plan_version_id",
316
+ collection: { parentAlias: "pp", rootAlias: "rpm", kind: "m2m" },
317
+ },
318
+ ],
319
+ condition: {
320
+ kind: "exp",
321
+ op: "and",
322
+ conditions: [{ kind: "column", alias: "rpm", column: "market_id", dbType: "int", cond: { kind: "in", value: [2] } }],
323
+ },
324
+ orderBys: [{ alias: "pp", column: "id", order: "ASC" }],
325
+ };
326
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
327
+ // I.e. the collection metadata points at parent `pp`, but the actual EXISTS correlation uses `_pp_version`.
328
+ // Pruning must keep the whole `pp_b0 -> _pp_b0_version -> _pp_version` dependency chain in the outer query.
329
+ expect(query.tables).toMatchObject([
330
+ { alias: "pp", table: "plan_packages", join: "primary" },
331
+ { alias: "pp_b0", table: "ready_plans", join: "outer" },
332
+ { alias: "_pp_b0_version", table: "ready_plan_versions", join: "outer" },
333
+ { alias: "_pp_version", table: "plan_package_versions", join: "outer" },
334
+ ]);
335
+ expect(query.condition).toMatchObject({
336
+ kind: "exp",
337
+ op: "and",
338
+ conditions: [
339
+ {
340
+ kind: "exists",
341
+ outerAliases: ["_pp_version"],
342
+ subquery: {
343
+ tables: [{ alias: "rpm", table: "ready_plan_version_to_markets", join: "primary" }],
344
+ condition: {
345
+ op: "and",
346
+ conditions: [
347
+ // I.e. `_pp_version` must be tracked as the outer alias, not just the declared collection parent `pp`.
348
+ { kind: "raw", aliases: ["rpm", "_pp_version"], condition: "_pp_version.id = rpm.ready_plan_version_id" },
349
+ { kind: "column", alias: "rpm", column: "market_id", cond: { kind: "in", value: [2] } },
350
+ ],
351
+ },
352
+ },
353
+ },
354
+ ],
355
+ });
356
+ });
357
+ it("rewrites collection branches inside mixed ordinary and collection ORs", () => {
358
+ const query = {
359
+ selects: ["a.*"],
360
+ tables: [
361
+ { alias: "a", table: "authors", join: "primary" },
362
+ { alias: "p", table: "publishers", join: "outer", col1: "a.publisher_id", col2: "p.id" },
363
+ {
364
+ alias: "b",
365
+ table: "books",
366
+ join: "outer",
367
+ col1: "a.id",
368
+ col2: "b.author_id",
369
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
370
+ },
371
+ {
372
+ alias: "c",
373
+ table: "comments",
374
+ join: "outer",
375
+ col1: "a.id",
376
+ col2: "c.parent_author_id",
377
+ collection: { parentAlias: "a", rootAlias: "c", kind: "o2m" },
378
+ },
379
+ ],
380
+ condition: {
381
+ kind: "exp",
382
+ op: "or",
383
+ conditions: [
384
+ { kind: "column", alias: "p", column: "name", dbType: "text", cond: { kind: "ilike", value: "%foo%" } },
385
+ { kind: "column", alias: "b", column: "title", dbType: "text", cond: { kind: "ilike", value: "%foo%" } },
386
+ { kind: "column", alias: "c", column: "text", dbType: "text", cond: { kind: "ilike", value: "%foo%" } },
387
+ ],
388
+ },
389
+ orderBys: [{ alias: "a", column: "id", order: "ASC" }],
390
+ };
391
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
392
+ // I.e. the ordinary `p` branch stays joined, while independent collection branches become scoped EXISTS clauses.
393
+ expect(query.tables).toMatchObject([
394
+ { alias: "a", table: "authors", join: "primary" },
395
+ { alias: "p", table: "publishers", join: "outer" },
396
+ ]);
397
+ expect(query.condition).toMatchObject({
398
+ kind: "exp",
399
+ op: "or",
400
+ conditions: [
401
+ { kind: "column", alias: "p", column: "name", cond: { kind: "ilike", value: "%foo%" } },
402
+ {
403
+ kind: "exists",
404
+ subquery: {
405
+ tables: [{ alias: "b", table: "books", join: "primary" }],
406
+ condition: {
407
+ op: "and",
408
+ conditions: [
409
+ { kind: "raw", condition: "a.id = b.author_id" },
410
+ { kind: "column", alias: "b", column: "title", cond: { kind: "ilike", value: "%foo%" } },
411
+ ],
412
+ },
413
+ },
414
+ },
415
+ {
416
+ kind: "exists",
417
+ subquery: {
418
+ tables: [{ alias: "c", table: "comments", join: "primary" }],
419
+ condition: {
420
+ op: "and",
421
+ conditions: [
422
+ { kind: "raw", condition: "a.id = c.parent_author_id" },
423
+ { kind: "column", alias: "c", column: "text", cond: { kind: "ilike", value: "%foo%" } },
424
+ ],
425
+ },
426
+ },
427
+ },
428
+ ],
429
+ });
430
+ });
431
+ it("preserves optional joins under collection EXISTS for nullable OR branches", () => {
432
+ const query = {
433
+ selects: ["a.*"],
434
+ tables: [
435
+ { alias: "a", table: "authors", join: "primary" },
436
+ {
437
+ alias: "b",
438
+ table: "books",
439
+ join: "outer",
440
+ col1: "a.id",
441
+ col2: "b.author_id",
442
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
443
+ },
444
+ { alias: "br", table: "book_reviews", join: "outer", col1: "b.id", col2: "br.book_id" },
445
+ { alias: "c", table: "comments", join: "outer", col1: "b.id", col2: "c.parent_book_id" },
446
+ ],
447
+ condition: {
448
+ kind: "exp",
449
+ op: "and",
450
+ conditions: [
451
+ {
452
+ kind: "exp",
453
+ op: "or",
454
+ conditions: [
455
+ { kind: "column", alias: "br", column: "rating", dbType: "int", cond: { kind: "eq", value: 5 } },
456
+ { kind: "column", alias: "c", column: "text", dbType: "text", cond: { kind: "eq", value: "match" } },
457
+ ],
458
+ },
459
+ ],
460
+ },
461
+ orderBys: [],
462
+ };
463
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
464
+ // I.e. an OR across optional descendants must be evaluated after LEFT JOINs, not after INNER JOINs that require
465
+ // every nullable branch to exist before any branch can match.
466
+ // I.e. only the parent table remains in the outer query; `b`/`br`/`c` moved under EXISTS.
467
+ expect(query.tables).toMatchObject([{ alias: "a", table: "authors", join: "primary" }]);
468
+ expect(query.condition).toMatchObject({
469
+ kind: "exp",
470
+ op: "and",
471
+ conditions: [
472
+ {
473
+ kind: "exists",
474
+ subquery: {
475
+ tables: [
476
+ // I.e. `b` is the collection root and becomes the EXISTS subquery's FROM table.
477
+ { alias: "b", table: "books", join: "primary" },
478
+ // I.e. `br` must stay nullable so the `c.text = match` OR branch can match without a review row.
479
+ { alias: "br", table: "book_reviews", join: "outer", distinct: false },
480
+ // I.e. `c` must stay nullable so the `br.rating = 5` OR branch can match without a comment row.
481
+ { alias: "c", table: "comments", join: "outer", distinct: false },
482
+ ],
483
+ condition: {
484
+ op: "and",
485
+ conditions: [
486
+ // I.e. this correlation is required after moving `b` out of the outer query.
487
+ { kind: "raw", condition: "a.id = b.author_id" },
488
+ {
489
+ kind: "exp",
490
+ op: "or",
491
+ conditions: [
492
+ // I.e. these branch aliases must remain in one OR under the LEFT JOINs, not become required joins.
493
+ { kind: "column", alias: "br", column: "rating", cond: { kind: "eq", value: 5 } },
494
+ { kind: "column", alias: "c", column: "text", cond: { kind: "eq", value: "match" } },
495
+ ],
496
+ },
497
+ ],
498
+ },
499
+ },
500
+ },
501
+ ],
502
+ });
503
+ });
504
+ it("splits same-root anti-join ORs into NOT EXISTS OR EXISTS", () => {
505
+ const query = {
506
+ selects: ["a.*"],
507
+ tables: [
508
+ { alias: "a", table: "authors", join: "primary" },
509
+ {
510
+ alias: "b",
511
+ table: "books",
512
+ join: "outer",
513
+ col1: "a.id",
514
+ col2: "b.author_id",
515
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
516
+ },
517
+ {
518
+ alias: "br",
519
+ table: "book_reviews",
520
+ join: "outer",
521
+ col1: "b.id",
522
+ col2: "br.book_id",
523
+ collection: { parentAlias: "b", rootAlias: "br", kind: "o2m" },
524
+ },
525
+ ],
526
+ condition: {
527
+ kind: "exp",
528
+ op: "or",
529
+ conditions: [
530
+ { kind: "column", alias: "b", column: "id", dbType: "int", cond: { kind: "is-null" } },
531
+ { kind: "column", alias: "br", column: "rating", dbType: "int", cond: { kind: "eq", value: 5 } },
532
+ ],
533
+ },
534
+ orderBys: [],
535
+ };
536
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
537
+ // I.e. the anti-join branch must become `NOT EXISTS (books)`, while the positive review branch can become its
538
+ // own correlated `EXISTS`; keeping `b.id IS NULL` inside an `EXISTS (books ...)` would make it impossible.
539
+ expect(query.tables).toMatchObject([{ alias: "a", table: "authors", join: "primary" }]);
540
+ expect(query.condition).toMatchObject({
541
+ kind: "exp",
542
+ op: "or",
543
+ conditions: [
544
+ {
545
+ kind: "exists",
546
+ negate: true,
547
+ subquery: {
548
+ tables: [{ alias: "b", table: "books", join: "primary" }],
549
+ condition: { op: "and", conditions: [{ kind: "raw", condition: "a.id = b.author_id" }] },
550
+ },
551
+ },
552
+ {
553
+ kind: "exists",
554
+ negate: false,
555
+ subquery: {
556
+ tables: [{ alias: "b", table: "books", join: "primary" }],
557
+ condition: {
558
+ op: "and",
559
+ conditions: [
560
+ { kind: "raw", condition: "a.id = b.author_id" },
561
+ {
562
+ kind: "exists",
563
+ negate: false,
564
+ subquery: { tables: [{ alias: "br", table: "book_reviews", join: "primary" }] },
565
+ },
566
+ ],
567
+ },
568
+ },
569
+ },
570
+ ],
571
+ });
572
+ });
573
+ it("splits same-root id IS NULL OR id IN list into NOT EXISTS OR EXISTS", () => {
574
+ const query = {
575
+ selects: ["a.*"],
576
+ tables: [
577
+ { alias: "a", table: "authors", join: "primary" },
578
+ {
579
+ alias: "b",
580
+ table: "books",
581
+ join: "outer",
582
+ col1: "a.id",
583
+ col2: "b.author_id",
584
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
585
+ },
586
+ ],
587
+ condition: {
588
+ kind: "exp",
589
+ op: "or",
590
+ conditions: [
591
+ { kind: "column", alias: "b", column: "id", dbType: "int", cond: { kind: "is-null" } },
592
+ { kind: "column", alias: "b", column: "id", dbType: "int", cond: { kind: "in", value: [1, 2] } },
593
+ ],
594
+ },
595
+ orderBys: [],
596
+ };
597
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
598
+ // I.e. `b.id IS NULL OR b.id IN (...)` means "no books OR one of these books"; the null branch must not be
599
+ // evaluated inside an `EXISTS (books ...)`, where `b.id IS NULL` can never match a real child row.
600
+ expect(query.tables).toMatchObject([{ alias: "a", table: "authors", join: "primary" }]);
601
+ expect(query.condition).toMatchObject({
602
+ kind: "exp",
603
+ op: "or",
604
+ conditions: [
605
+ {
606
+ kind: "exists",
607
+ negate: true,
608
+ subquery: {
609
+ tables: [{ alias: "b", table: "books", join: "primary" }],
610
+ condition: { op: "and", conditions: [{ kind: "raw", condition: "a.id = b.author_id" }] },
611
+ },
612
+ },
613
+ {
614
+ kind: "exists",
615
+ negate: false,
616
+ subquery: {
617
+ tables: [{ alias: "b", table: "books", join: "primary" }],
618
+ condition: {
619
+ op: "and",
620
+ conditions: [
621
+ { kind: "raw", condition: "a.id = b.author_id" },
622
+ { kind: "column", alias: "b", column: "id", cond: { kind: "in", value: [1, 2] } },
623
+ ],
624
+ },
625
+ },
626
+ },
627
+ ],
628
+ });
629
+ });
630
+ it("splits m2m target id IS NULL OR id IN list into NOT EXISTS OR EXISTS", () => {
631
+ const query = {
632
+ selects: ["a.*"],
633
+ tables: [
634
+ { alias: "a", table: "authors", join: "primary" },
635
+ {
636
+ alias: "att",
637
+ table: "authors_to_tags",
638
+ join: "outer",
639
+ col1: "a.id",
640
+ col2: "att.author_id",
641
+ collection: { parentAlias: "a", rootAlias: "att", kind: "m2m" },
642
+ },
643
+ {
644
+ alias: "t",
645
+ table: "tags",
646
+ join: "outer",
647
+ col1: "att.tag_id",
648
+ col2: "t.id",
649
+ collection: { parentAlias: "att", rootAlias: "att", kind: "m2m" },
650
+ },
651
+ ],
652
+ condition: {
653
+ kind: "exp",
654
+ op: "or",
655
+ conditions: [
656
+ { kind: "column", alias: "t", column: "id", dbType: "int", cond: { kind: "is-null" } },
657
+ { kind: "column", alias: "t", column: "id", dbType: "int", cond: { kind: "in", value: [1, 2] } },
658
+ ],
659
+ },
660
+ orderBys: [],
661
+ };
662
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
663
+ // I.e. m2m target `t.id IS NULL` means no matching membership row, so it must split against root `att`.
664
+ expect(query.tables).toMatchObject([{ alias: "a", table: "authors", join: "primary" }]);
665
+ expect(query.condition).toMatchObject({
666
+ kind: "exp",
667
+ op: "or",
668
+ conditions: [
669
+ {
670
+ kind: "exists",
671
+ negate: true,
672
+ subquery: {
673
+ tables: [{ alias: "att", table: "authors_to_tags", join: "primary" }],
674
+ condition: { op: "and", conditions: [{ kind: "raw", condition: "a.id = att.author_id" }] },
675
+ },
676
+ },
677
+ {
678
+ kind: "exists",
679
+ negate: false,
680
+ subquery: {
681
+ tables: [
682
+ { alias: "att", table: "authors_to_tags", join: "primary" },
683
+ { alias: "t", table: "tags", join: "outer", distinct: false },
684
+ ],
685
+ condition: {
686
+ op: "and",
687
+ conditions: [
688
+ { kind: "raw", condition: "a.id = att.author_id" },
689
+ { kind: "column", alias: "t", column: "id", cond: { kind: "in", value: [1, 2] } },
690
+ ],
691
+ },
692
+ },
693
+ },
694
+ ],
695
+ });
696
+ });
697
+ it("does not split same-row positive branches into unrelated EXISTS clauses", () => {
698
+ const query = {
699
+ selects: ["a.*"],
700
+ tables: [
701
+ { alias: "a", table: "authors", join: "primary" },
702
+ {
703
+ alias: "b",
704
+ table: "books",
705
+ join: "outer",
706
+ col1: "a.id",
707
+ col2: "b.author_id",
708
+ collection: { parentAlias: "a", rootAlias: "b", kind: "o2m" },
709
+ },
710
+ {
711
+ alias: "br",
712
+ table: "book_reviews",
713
+ join: "outer",
714
+ col1: "b.id",
715
+ col2: "br.book_id",
716
+ collection: { parentAlias: "b", rootAlias: "br", kind: "o2m" },
717
+ },
718
+ ],
719
+ condition: {
720
+ kind: "exp",
721
+ op: "or",
722
+ conditions: [
723
+ { kind: "column", alias: "b", column: "id", dbType: "int", cond: { kind: "is-null" } },
724
+ {
725
+ kind: "exp",
726
+ op: "and",
727
+ conditions: [
728
+ { kind: "column", alias: "b", column: "title", dbType: "text", cond: { kind: "eq", value: "b1" } },
729
+ { kind: "column", alias: "br", column: "rating", dbType: "int", cond: { kind: "eq", value: 5 } },
730
+ ],
731
+ },
732
+ ],
733
+ },
734
+ orderBys: [],
735
+ };
736
+ (0, QueryParser_collectionJoins_1.optimizeCollectionJoins)(query);
737
+ // I.e. the positive branch must stay one `EXISTS (books WHERE b.title = ... AND EXISTS reviews ...)`, not split
738
+ // into independent `EXISTS (books title = ...) AND EXISTS (reviews rating = ...)` clauses that can match different rows.
739
+ expect(query.tables).toMatchObject([{ alias: "a", table: "authors", join: "primary" }]);
740
+ expect(query.condition).toMatchObject({
741
+ kind: "exp",
742
+ op: "or",
743
+ conditions: [
744
+ { kind: "exists", negate: true, subquery: { tables: [{ alias: "b", join: "primary" }] } },
745
+ {
746
+ kind: "exists",
747
+ negate: false,
748
+ subquery: {
749
+ tables: [{ alias: "b", table: "books", join: "primary" }],
750
+ condition: {
751
+ op: "and",
752
+ conditions: [
753
+ { kind: "raw", condition: "a.id = b.author_id" },
754
+ {
755
+ kind: "exp",
756
+ op: "and",
757
+ conditions: [{ kind: "column", alias: "b", column: "title", cond: { kind: "eq", value: "b1" } }],
758
+ },
759
+ {
760
+ kind: "exists",
761
+ negate: false,
762
+ subquery: { tables: [{ alias: "br", table: "book_reviews", join: "primary" }] },
763
+ },
764
+ ],
765
+ },
766
+ },
767
+ },
768
+ ],
769
+ });
770
+ });
771
+ });
772
+ //# sourceMappingURL=QueryParser.collectionJoins.test.js.map