@tanstack/db 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/errors.cjs +18 -6
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +9 -3
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +4 -1
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +38 -21
- package/dist/cjs/query/builder/index.cjs +25 -16
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +8 -8
- package/dist/cjs/query/builder/ref-proxy.cjs +12 -8
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +2 -1
- package/dist/cjs/query/builder/types.d.cts +493 -28
- package/dist/cjs/query/compiler/evaluators.cjs +29 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +4 -2
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +13 -4
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs +70 -60
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/select.cjs +131 -42
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/compiler/select.d.cts +1 -5
- package/dist/cjs/query/ir.cjs +4 -0
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +6 -1
- package/dist/cjs/query/optimizer.cjs +61 -20
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/esm/errors.d.ts +9 -3
- package/dist/esm/errors.js +18 -6
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/query/builder/functions.d.ts +38 -21
- package/dist/esm/query/builder/functions.js +4 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +8 -8
- package/dist/esm/query/builder/index.js +27 -18
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +2 -1
- package/dist/esm/query/builder/ref-proxy.js +12 -8
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +493 -28
- package/dist/esm/query/compiler/evaluators.js +29 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +4 -2
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +15 -6
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +71 -61
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/select.d.ts +1 -5
- package/dist/esm/query/compiler/select.js +131 -42
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/ir.d.ts +6 -1
- package/dist/esm/query/ir.js +4 -0
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/optimizer.js +62 -21
- package/dist/esm/query/optimizer.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +17 -10
- package/src/query/builder/functions.ts +176 -108
- package/src/query/builder/index.ts +68 -48
- package/src/query/builder/ref-proxy.ts +14 -20
- package/src/query/builder/types.ts +622 -110
- package/src/query/compiler/evaluators.ts +30 -0
- package/src/query/compiler/group-by.ts +6 -1
- package/src/query/compiler/index.ts +23 -6
- package/src/query/compiler/joins.ts +132 -101
- package/src/query/compiler/select.ts +206 -113
- package/src/query/ir.ts +14 -1
- package/src/query/optimizer.ts +131 -59
|
@@ -337,6 +337,36 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
// Null/undefined checking functions
|
|
341
|
+
case `isUndefined`: {
|
|
342
|
+
const arg = compiledArgs[0]!
|
|
343
|
+
return (data) => {
|
|
344
|
+
const value = arg(data)
|
|
345
|
+
return value === undefined
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
case `isNotUndefined`: {
|
|
349
|
+
const arg = compiledArgs[0]!
|
|
350
|
+
return (data) => {
|
|
351
|
+
const value = arg(data)
|
|
352
|
+
return value !== undefined
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
case `isNull`: {
|
|
356
|
+
const arg = compiledArgs[0]!
|
|
357
|
+
return (data) => {
|
|
358
|
+
const value = arg(data)
|
|
359
|
+
return value === null
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
case `isNotNull`: {
|
|
363
|
+
const arg = compiledArgs[0]!
|
|
364
|
+
return (data) => {
|
|
365
|
+
const value = arg(data)
|
|
366
|
+
return value !== null
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
340
370
|
default:
|
|
341
371
|
throw new UnknownFunctionError(func.name)
|
|
342
372
|
}
|
|
@@ -349,12 +349,17 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
349
349
|
return typeof value === `number` ? value : value != null ? Number(value) : 0
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
// Create a raw value extractor function for the expression to aggregate
|
|
353
|
+
const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {
|
|
354
|
+
return compiledExpr(namespacedRow)
|
|
355
|
+
}
|
|
356
|
+
|
|
352
357
|
// Return the appropriate aggregate function
|
|
353
358
|
switch (aggExpr.name.toLowerCase()) {
|
|
354
359
|
case `sum`:
|
|
355
360
|
return sum(valueExtractor)
|
|
356
361
|
case `count`:
|
|
357
|
-
return count()
|
|
362
|
+
return count(rawValueExtractor)
|
|
358
363
|
case `avg`:
|
|
359
364
|
return avg(valueExtractor)
|
|
360
365
|
case `min`:
|
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
LimitOffsetRequireOrderByError,
|
|
8
8
|
UnsupportedFromTypeError,
|
|
9
9
|
} from "../../errors.js"
|
|
10
|
-
import { PropRef, getWhereExpression } from "../ir.js"
|
|
10
|
+
import { PropRef, Value as ValClass, getWhereExpression } from "../ir.js"
|
|
11
11
|
import { compileExpression } from "./evaluators.js"
|
|
12
12
|
import { processJoins } from "./joins.js"
|
|
13
13
|
import { processGroupBy } from "./group-by.js"
|
|
14
14
|
import { processOrderBy } from "./order-by.js"
|
|
15
|
-
import {
|
|
15
|
+
import { processSelect } from "./select.js"
|
|
16
16
|
import type { OrderByOptimizationInfo } from "./order-by.js"
|
|
17
17
|
import type {
|
|
18
18
|
BasicExpression,
|
|
@@ -173,7 +173,7 @@ export function compileQuery(
|
|
|
173
173
|
})
|
|
174
174
|
)
|
|
175
175
|
} else if (query.select) {
|
|
176
|
-
pipeline =
|
|
176
|
+
pipeline = processSelect(pipeline, query.select, allInputs)
|
|
177
177
|
} else {
|
|
178
178
|
// If no SELECT clause, create __select_results with the main table data
|
|
179
179
|
pipeline = pipeline.pipe(
|
|
@@ -270,7 +270,8 @@ export function compileQuery(
|
|
|
270
270
|
const resultPipeline = orderedPipeline.pipe(
|
|
271
271
|
map(([key, [row, orderByIndex]]) => {
|
|
272
272
|
// Extract the final results from __select_results and include orderBy index
|
|
273
|
-
const
|
|
273
|
+
const raw = (row as any).__select_results
|
|
274
|
+
const finalResults = unwrapValue(raw)
|
|
274
275
|
return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]
|
|
275
276
|
})
|
|
276
277
|
)
|
|
@@ -294,7 +295,8 @@ export function compileQuery(
|
|
|
294
295
|
const resultPipeline: ResultStream = pipeline.pipe(
|
|
295
296
|
map(([key, row]) => {
|
|
296
297
|
// Extract the final results from __select_results and return [key, [results, undefined]]
|
|
297
|
-
const
|
|
298
|
+
const raw = (row as any).__select_results
|
|
299
|
+
const finalResults = unwrapValue(raw)
|
|
298
300
|
return [key, [finalResults, undefined]] as [
|
|
299
301
|
unknown,
|
|
300
302
|
[any, string | undefined],
|
|
@@ -359,7 +361,9 @@ function processFrom(
|
|
|
359
361
|
const extractedInput = subQueryInput.pipe(
|
|
360
362
|
map((data: any) => {
|
|
361
363
|
const [key, [value, _orderByIndex]] = data
|
|
362
|
-
|
|
364
|
+
// Unwrap Value expressions that might have leaked through as the entire row
|
|
365
|
+
const unwrapped = unwrapValue(value)
|
|
366
|
+
return [key, unwrapped] as [unknown, any]
|
|
363
367
|
})
|
|
364
368
|
)
|
|
365
369
|
|
|
@@ -374,6 +378,19 @@ function processFrom(
|
|
|
374
378
|
}
|
|
375
379
|
}
|
|
376
380
|
|
|
381
|
+
// Helper to check if a value is a Value expression
|
|
382
|
+
function isValue(raw: any): boolean {
|
|
383
|
+
return (
|
|
384
|
+
raw instanceof ValClass ||
|
|
385
|
+
(raw && typeof raw === `object` && `type` in raw && raw.type === `val`)
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Helper to unwrap a Value expression or return the value itself
|
|
390
|
+
function unwrapValue(value: any): any {
|
|
391
|
+
return isValue(value) ? value.value : value
|
|
392
|
+
}
|
|
393
|
+
|
|
377
394
|
/**
|
|
378
395
|
* Recursively maps optimized subqueries to their original queries for proper caching.
|
|
379
396
|
* This ensures that when we encounter the same QueryRef object in different contexts,
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
} from "@tanstack/db-ivm"
|
|
8
8
|
import {
|
|
9
9
|
CollectionInputNotFoundError,
|
|
10
|
+
InvalidJoinCondition,
|
|
11
|
+
InvalidJoinConditionLeftTableError,
|
|
12
|
+
InvalidJoinConditionRightTableError,
|
|
10
13
|
InvalidJoinConditionSameTableError,
|
|
11
14
|
InvalidJoinConditionTableMismatchError,
|
|
12
|
-
InvalidJoinConditionWrongTablesError,
|
|
13
15
|
JoinCollectionNotFoundError,
|
|
14
16
|
UnsupportedJoinSourceTypeError,
|
|
15
17
|
UnsupportedJoinTypeError,
|
|
@@ -139,10 +141,11 @@ function processJoin(
|
|
|
139
141
|
)
|
|
140
142
|
|
|
141
143
|
// Analyze which table each expression refers to and swap if necessary
|
|
144
|
+
const availableTableAliases = Object.keys(tables)
|
|
142
145
|
const { mainExpr, joinedExpr } = analyzeJoinExpressions(
|
|
143
146
|
joinClause.left,
|
|
144
147
|
joinClause.right,
|
|
145
|
-
|
|
148
|
+
availableTableAliases,
|
|
146
149
|
joinedTableAlias
|
|
147
150
|
)
|
|
148
151
|
|
|
@@ -187,90 +190,106 @@ function processJoin(
|
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
if (activeCollection) {
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
// because the indexes are only available after the initial sync
|
|
236
|
-
// so we can't fetch it during compilation
|
|
237
|
-
index ??= findIndexForField(
|
|
238
|
-
followRefCollection.indexes,
|
|
239
|
-
followRefResult.path
|
|
193
|
+
// If the lazy collection comes from a subquery that has a limit and/or an offset clause
|
|
194
|
+
// then we need to deoptimize the join because we don't know which rows are in the result set
|
|
195
|
+
// since we simply lookup matching keys in the index but the index contains all rows
|
|
196
|
+
// (not just the ones that pass the limit and offset clauses)
|
|
197
|
+
const lazyFrom =
|
|
198
|
+
activeCollection === `main` ? joinClause.from : rawQuery.from
|
|
199
|
+
const limitedSubquery =
|
|
200
|
+
lazyFrom.type === `queryRef` &&
|
|
201
|
+
(lazyFrom.query.limit || lazyFrom.query.offset)
|
|
202
|
+
|
|
203
|
+
if (!limitedSubquery) {
|
|
204
|
+
// This join can be optimized by having the active collection
|
|
205
|
+
// dynamically load keys into the lazy collection
|
|
206
|
+
// based on the value of the joinKey and by looking up
|
|
207
|
+
// matching rows in the index of the lazy collection
|
|
208
|
+
|
|
209
|
+
// Mark the lazy collection as lazy
|
|
210
|
+
// this Set is passed by the liveQueryCollection to the compiler
|
|
211
|
+
// such that the liveQueryCollection can check it after compilation
|
|
212
|
+
// to know which collections are lazy collections
|
|
213
|
+
lazyCollections.add(lazyCollection.id)
|
|
214
|
+
|
|
215
|
+
const activePipeline =
|
|
216
|
+
activeCollection === `main` ? mainPipeline : joinedPipeline
|
|
217
|
+
|
|
218
|
+
let index: BaseIndex<string | number> | undefined
|
|
219
|
+
|
|
220
|
+
const lazyCollectionJoinExpr =
|
|
221
|
+
activeCollection === `main`
|
|
222
|
+
? (joinedExpr as PropRef)
|
|
223
|
+
: (mainExpr as PropRef)
|
|
224
|
+
|
|
225
|
+
const followRefResult = followRef(
|
|
226
|
+
rawQuery,
|
|
227
|
+
lazyCollectionJoinExpr,
|
|
228
|
+
lazyCollection
|
|
229
|
+
)!
|
|
230
|
+
const followRefCollection = followRefResult.collection
|
|
231
|
+
|
|
232
|
+
const fieldName = followRefResult.path[0]
|
|
233
|
+
if (fieldName) {
|
|
234
|
+
ensureIndexForField(
|
|
235
|
+
fieldName,
|
|
236
|
+
followRefResult.path,
|
|
237
|
+
followRefCollection
|
|
240
238
|
)
|
|
239
|
+
}
|
|
241
240
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
241
|
+
let deoptimized = false
|
|
242
|
+
|
|
243
|
+
const activePipelineWithLoading: IStreamBuilder<
|
|
244
|
+
[key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]
|
|
245
|
+
> = activePipeline.pipe(
|
|
246
|
+
tap(([joinKey, _]) => {
|
|
247
|
+
if (deoptimized) {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Find the index for the path we join on
|
|
252
|
+
// we need to find the index inside the map operator
|
|
253
|
+
// because the indexes are only available after the initial sync
|
|
254
|
+
// so we can't fetch it during compilation
|
|
255
|
+
index ??= findIndexForField(
|
|
256
|
+
followRefCollection.indexes,
|
|
257
|
+
followRefResult.path
|
|
250
258
|
)
|
|
251
|
-
}
|
|
252
259
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
// The `callbacks` object is passed by the liveQueryCollection to the compiler.
|
|
261
|
+
// It contains a function to lazy load keys for each lazy collection
|
|
262
|
+
// as well as a function to switch back to a regular collection
|
|
263
|
+
// (useful when there's no index for available for lazily loading the collection)
|
|
264
|
+
const collectionCallbacks = callbacks[lazyCollection.id]
|
|
265
|
+
if (!collectionCallbacks) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Internal error: callbacks for collection are missing in join pipeline. Make sure the live query collection sets them before running the pipeline.`
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { loadKeys, loadInitialState } = collectionCallbacks
|
|
272
|
+
|
|
273
|
+
if (index && index.supports(`eq`)) {
|
|
274
|
+
// Use the index to fetch the PKs of the rows in the lazy collection
|
|
275
|
+
// that match this row from the active collection based on the value of the joinKey
|
|
276
|
+
const matchingKeys = index.lookup(`eq`, joinKey)
|
|
277
|
+
// Inform the lazy collection that those keys need to be loaded
|
|
278
|
+
loadKeys(matchingKeys)
|
|
279
|
+
} else {
|
|
280
|
+
// We can't optimize the join because there is no index for the join key
|
|
281
|
+
// on the lazy collection, so we load the initial state
|
|
282
|
+
deoptimized = true
|
|
283
|
+
loadInitialState()
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
)
|
|
269
287
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
288
|
+
if (activeCollection === `main`) {
|
|
289
|
+
mainPipeline = activePipelineWithLoading
|
|
290
|
+
} else {
|
|
291
|
+
joinedPipeline = activePipelineWithLoading
|
|
292
|
+
}
|
|
274
293
|
}
|
|
275
294
|
}
|
|
276
295
|
|
|
@@ -283,53 +302,65 @@ function processJoin(
|
|
|
283
302
|
|
|
284
303
|
/**
|
|
285
304
|
* Analyzes join expressions to determine which refers to which table
|
|
286
|
-
* and returns them in the correct order (
|
|
305
|
+
* and returns them in the correct order (available table expression first, joined table expression second)
|
|
287
306
|
*/
|
|
288
307
|
function analyzeJoinExpressions(
|
|
289
308
|
left: BasicExpression,
|
|
290
309
|
right: BasicExpression,
|
|
291
|
-
|
|
310
|
+
allAvailableTableAliases: Array<string>,
|
|
292
311
|
joinedTableAlias: string
|
|
293
312
|
): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {
|
|
313
|
+
// Filter out the joined table alias from the available table aliases
|
|
314
|
+
const availableTableAliases = allAvailableTableAliases.filter(
|
|
315
|
+
(alias) => alias !== joinedTableAlias
|
|
316
|
+
)
|
|
317
|
+
|
|
294
318
|
const leftTableAlias = getTableAliasFromExpression(left)
|
|
295
319
|
const rightTableAlias = getTableAliasFromExpression(right)
|
|
296
320
|
|
|
297
|
-
// If left expression refers to
|
|
321
|
+
// If left expression refers to an available table and right refers to joined table, keep as is
|
|
298
322
|
if (
|
|
299
|
-
leftTableAlias
|
|
323
|
+
leftTableAlias &&
|
|
324
|
+
availableTableAliases.includes(leftTableAlias) &&
|
|
300
325
|
rightTableAlias === joinedTableAlias
|
|
301
326
|
) {
|
|
302
327
|
return { mainExpr: left, joinedExpr: right }
|
|
303
328
|
}
|
|
304
329
|
|
|
305
|
-
// If left expression refers to joined table and right refers to
|
|
330
|
+
// If left expression refers to joined table and right refers to an available table, swap them
|
|
306
331
|
if (
|
|
307
332
|
leftTableAlias === joinedTableAlias &&
|
|
308
|
-
rightTableAlias
|
|
333
|
+
rightTableAlias &&
|
|
334
|
+
availableTableAliases.includes(rightTableAlias)
|
|
309
335
|
) {
|
|
310
336
|
return { mainExpr: right, joinedExpr: left }
|
|
311
337
|
}
|
|
312
338
|
|
|
339
|
+
// If one expression doesn't refer to any table, this is an invalid join
|
|
340
|
+
if (!leftTableAlias || !rightTableAlias) {
|
|
341
|
+
// For backward compatibility, use the first available table alias in error message
|
|
342
|
+
throw new InvalidJoinConditionTableMismatchError()
|
|
343
|
+
}
|
|
344
|
+
|
|
313
345
|
// If both expressions refer to the same alias, this is an invalid join
|
|
314
346
|
if (leftTableAlias === rightTableAlias) {
|
|
315
|
-
throw new InvalidJoinConditionSameTableError(leftTableAlias
|
|
347
|
+
throw new InvalidJoinConditionSameTableError(leftTableAlias)
|
|
316
348
|
}
|
|
317
349
|
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
350
|
+
// Left side must refer to an available table
|
|
351
|
+
// This cannot happen with the query builder as there is no way to build a ref
|
|
352
|
+
// to an unavailable table, but just in case, but could happen with the IR
|
|
353
|
+
if (!availableTableAliases.includes(leftTableAlias)) {
|
|
354
|
+
throw new InvalidJoinConditionLeftTableError(leftTableAlias)
|
|
324
355
|
}
|
|
325
356
|
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
)
|
|
357
|
+
// Right side must refer to the joined table
|
|
358
|
+
if (rightTableAlias !== joinedTableAlias) {
|
|
359
|
+
throw new InvalidJoinConditionRightTableError(joinedTableAlias)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// This should not be reachable given the logic above, but just in case
|
|
363
|
+
throw new InvalidJoinCondition()
|
|
333
364
|
}
|
|
334
365
|
|
|
335
366
|
/**
|