squirreling 0.4.7 → 0.5.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.
@@ -1,8 +1,18 @@
1
+ import { unknownFunctionError } from '../parseErrors.js'
2
+ import { invalidContextError } from '../executionErrors.js'
3
+ import {
4
+ argCountError,
5
+ argValueError,
6
+ castError,
7
+ } from '../validationErrors.js'
8
+ import { isMathFunc } from '../validation.js'
9
+ import { applyIntervalToDate } from './date.js'
1
10
  import { executeSelect } from './execute.js'
11
+ import { evaluateMathFunc } from './math.js'
2
12
  import { applyBinaryOp, stringify } from './utils.js'
3
13
 
4
14
  /**
5
- * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource } from '../types.js'
15
+ * @import { ExprNode, AsyncRow, SqlPrimitive, AsyncDataSource, IntervalUnit } from '../types.js'
6
16
  */
7
17
 
8
18
  /**
@@ -12,9 +22,10 @@ import { applyBinaryOp, stringify } from './utils.js'
12
22
  * @param {ExprNode} params.node - The expression node to evaluate
13
23
  * @param {AsyncRow} params.row - The data row to evaluate against
14
24
  * @param {Record<string, AsyncDataSource>} params.tables
25
+ * @param {number} [params.rowIndex] - 1-based row index for error reporting
15
26
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
16
27
  */
17
- export async function evaluateExpr({ node, row, tables }) {
28
+ export async function evaluateExpr({ node, row, tables, rowIndex }) {
18
29
  if (node.type === 'literal') {
19
30
  return node.value
20
31
  }
@@ -31,7 +42,7 @@ export async function evaluateExpr({ node, row, tables }) {
31
42
  return row[colName]()
32
43
  }
33
44
  }
34
- return undefined
45
+ return null
35
46
  }
36
47
 
37
48
  // Scalar subquery - returns a single value
@@ -49,16 +60,16 @@ export async function evaluateExpr({ node, row, tables }) {
49
60
  // Unary operators
50
61
  if (node.type === 'unary') {
51
62
  if (node.op === 'NOT') {
52
- return !await evaluateExpr({ node: node.argument, row, tables })
63
+ return !await evaluateExpr({ node: node.argument, row, tables, rowIndex })
53
64
  }
54
65
  if (node.op === 'IS NULL') {
55
- return await evaluateExpr({ node: node.argument, row, tables }) == null
66
+ return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) == null
56
67
  }
57
68
  if (node.op === 'IS NOT NULL') {
58
- return await evaluateExpr({ node: node.argument, row, tables }) != null
69
+ return await evaluateExpr({ node: node.argument, row, tables, rowIndex }) != null
59
70
  }
60
71
  if (node.op === '-') {
61
- const val = await evaluateExpr({ node: node.argument, row, tables })
72
+ const val = await evaluateExpr({ node: node.argument, row, tables, rowIndex })
62
73
  if (val == null) return null
63
74
  return -val
64
75
  }
@@ -66,7 +77,17 @@ export async function evaluateExpr({ node, row, tables }) {
66
77
 
67
78
  // Binary operators
68
79
  if (node.type === 'binary') {
69
- const left = await evaluateExpr({ node: node.left, row, tables })
80
+ // Handle date +/- interval at AST level
81
+ if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
82
+ const dateVal = await evaluateExpr({ node: node.left, row, tables, rowIndex })
83
+ return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
84
+ }
85
+ if (node.op === '+' && node.left.type === 'interval') {
86
+ const dateVal = await evaluateExpr({ node: node.right, row, tables, rowIndex })
87
+ return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
88
+ }
89
+
90
+ const left = await evaluateExpr({ node: node.left, row, tables, rowIndex })
70
91
 
71
92
  // Short-circuit evaluation for AND and OR
72
93
  if (node.op === 'AND') {
@@ -76,7 +97,7 @@ export async function evaluateExpr({ node, row, tables }) {
76
97
  if (left) return true
77
98
  }
78
99
 
79
- const right = await evaluateExpr({ node: node.right, row, tables })
100
+ const right = await evaluateExpr({ node: node.right, row, tables, rowIndex })
80
101
  return applyBinaryOp(node.op, left, right)
81
102
  }
82
103
 
@@ -84,34 +105,77 @@ export async function evaluateExpr({ node, row, tables }) {
84
105
  if (node.type === 'function') {
85
106
  const funcName = node.name.toUpperCase()
86
107
  /** @type {SqlPrimitive[]} */
87
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables })))
108
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, rowIndex })))
88
109
 
89
110
  if (funcName === 'UPPER') {
90
- if (args.length !== 1) throw new Error('UPPER requires exactly 1 argument')
111
+ if (args.length !== 1) {
112
+ throw argCountError({
113
+ funcName: 'UPPER',
114
+ expected: 1,
115
+ received: args.length,
116
+ positionStart: node.positionStart,
117
+ positionEnd: node.positionEnd,
118
+ rowNumber: rowIndex,
119
+ })
120
+ }
91
121
  const val = args[0]
92
122
  if (val == null) return null
93
123
  return String(val).toUpperCase()
94
124
  }
95
125
 
96
126
  if (funcName === 'LOWER') {
97
- if (args.length !== 1) throw new Error('LOWER requires exactly 1 argument')
127
+ if (args.length !== 1) {
128
+ throw argCountError({
129
+ funcName: 'LOWER',
130
+ expected: 1,
131
+ received: args.length,
132
+ positionStart: node.positionStart,
133
+ positionEnd: node.positionEnd,
134
+ rowNumber: rowIndex,
135
+ })
136
+ }
98
137
  const val = args[0]
99
138
  if (val == null) return null
100
139
  return String(val).toLowerCase()
101
140
  }
102
141
 
103
142
  if (funcName === 'CONCAT') {
104
- if (args.length < 1) throw new Error('CONCAT requires at least 1 argument')
143
+ if (args.length < 1) {
144
+ throw argCountError({
145
+ funcName: 'CONCAT',
146
+ expected: 'at least 1',
147
+ received: args.length,
148
+ positionStart: node.positionStart,
149
+ positionEnd: node.positionEnd,
150
+ rowNumber: rowIndex,
151
+ })
152
+ }
105
153
  // SQL CONCAT returns NULL if any argument is NULL
106
154
  if (args.some(a => a == null)) return null
107
155
  if (args.some(a => typeof a === 'object')) {
108
- throw new Error('CONCAT does not support object arguments')
156
+ throw argValueError({
157
+ funcName: 'CONCAT',
158
+ message: 'does not support object arguments',
159
+ positionStart: node.positionStart,
160
+ positionEnd: node.positionEnd,
161
+ hint: 'Use CAST to convert objects to strings first.',
162
+ rowNumber: rowIndex,
163
+ })
109
164
  }
110
165
  return args.map(a => String(a)).join('')
111
166
  }
112
167
 
113
168
  if (funcName === 'LENGTH') {
114
- if (args.length !== 1) throw new Error('LENGTH requires exactly 1 argument')
169
+ if (args.length !== 1) {
170
+ throw argCountError({
171
+ funcName: 'LENGTH',
172
+ expected: 1,
173
+ received: args.length,
174
+ positionStart: node.positionStart,
175
+ positionEnd: node.positionEnd,
176
+ rowNumber: rowIndex,
177
+ })
178
+ }
115
179
  const val = args[0]
116
180
  if (val == null) return null
117
181
  return String(val).length
@@ -119,21 +183,41 @@ export async function evaluateExpr({ node, row, tables }) {
119
183
 
120
184
  if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
121
185
  if (args.length < 2 || args.length > 3) {
122
- throw new Error(`${funcName} requires 2 or 3 arguments`)
186
+ throw argCountError({
187
+ funcName,
188
+ expected: '2 or 3',
189
+ received: args.length,
190
+ positionStart: node.positionStart,
191
+ positionEnd: node.positionEnd,
192
+ rowNumber: rowIndex,
193
+ })
123
194
  }
124
195
  const str = args[0]
125
196
  if (str == null) return null
126
197
  const strVal = String(str)
127
198
  const start = Number(args[1])
128
199
  if (!Number.isInteger(start) || start < 1) {
129
- throw new Error(`${funcName} start position must be a positive integer`)
200
+ throw argValueError({
201
+ funcName,
202
+ message: `start position must be a positive integer, got ${args[1]}`,
203
+ positionStart: node.positionStart,
204
+ positionEnd: node.positionEnd,
205
+ hint: 'SQL uses 1-based indexing.',
206
+ rowNumber: rowIndex,
207
+ })
130
208
  }
131
209
  // SQL uses 1-based indexing
132
210
  const startIdx = start - 1
133
211
  if (args.length === 3) {
134
212
  const len = Number(args[2])
135
213
  if (!Number.isInteger(len) || len < 0) {
136
- throw new Error(`${funcName} length must be a non-negative integer`)
214
+ throw argValueError({
215
+ funcName,
216
+ message: `length must be a non-negative integer, got ${args[2]}`,
217
+ positionStart: node.positionStart,
218
+ positionEnd: node.positionEnd,
219
+ rowNumber: rowIndex,
220
+ })
137
221
  }
138
222
  return strVal.substring(startIdx, startIdx + len)
139
223
  }
@@ -141,14 +225,32 @@ export async function evaluateExpr({ node, row, tables }) {
141
225
  }
142
226
 
143
227
  if (funcName === 'TRIM') {
144
- if (args.length !== 1) throw new Error('TRIM requires exactly 1 argument')
228
+ if (args.length !== 1) {
229
+ throw argCountError({
230
+ funcName: 'TRIM',
231
+ expected: 1,
232
+ received: args.length,
233
+ positionStart: node.positionStart,
234
+ positionEnd: node.positionEnd,
235
+ rowNumber: rowIndex,
236
+ })
237
+ }
145
238
  const val = args[0]
146
239
  if (val == null) return null
147
240
  return String(val).trim()
148
241
  }
149
242
 
150
243
  if (funcName === 'REPLACE') {
151
- if (args.length !== 3) throw new Error('REPLACE requires exactly 3 arguments')
244
+ if (args.length !== 3) {
245
+ throw argCountError({
246
+ funcName: 'REPLACE',
247
+ expected: 3,
248
+ received: args.length,
249
+ positionStart: node.positionStart,
250
+ positionEnd: node.positionEnd,
251
+ rowNumber: rowIndex,
252
+ })
253
+ }
152
254
  const str = args[0]
153
255
  const searchStr = args[1]
154
256
  const replaceStr = args[2]
@@ -158,13 +260,71 @@ export async function evaluateExpr({ node, row, tables }) {
158
260
  }
159
261
 
160
262
  if (funcName === 'RANDOM' || funcName === 'RAND') {
161
- if (args.length !== 0) throw new Error(`${funcName} takes no arguments`)
263
+ if (args.length !== 0) {
264
+ throw argCountError({
265
+ funcName,
266
+ expected: 0,
267
+ received: args.length,
268
+ positionStart: node.positionStart,
269
+ positionEnd: node.positionEnd,
270
+ rowNumber: rowIndex,
271
+ })
272
+ }
162
273
  return Math.random()
163
274
  }
164
275
 
276
+ if (funcName === 'CURRENT_DATE') {
277
+ if (args.length !== 0) {
278
+ throw argCountError({
279
+ funcName: 'CURRENT_DATE',
280
+ expected: 0,
281
+ received: args.length,
282
+ positionStart: node.positionStart,
283
+ positionEnd: node.positionEnd,
284
+ rowNumber: rowIndex,
285
+ })
286
+ }
287
+ return new Date().toISOString().split('T')[0]
288
+ }
289
+
290
+ if (funcName === 'CURRENT_TIME') {
291
+ if (args.length !== 0) {
292
+ throw argCountError({
293
+ funcName: 'CURRENT_TIME',
294
+ expected: 0,
295
+ received: args.length,
296
+ positionStart: node.positionStart,
297
+ positionEnd: node.positionEnd,
298
+ rowNumber: rowIndex,
299
+ })
300
+ }
301
+ return new Date().toISOString().split('T')[1].replace('Z', '')
302
+ }
303
+
304
+ if (funcName === 'CURRENT_TIMESTAMP') {
305
+ if (args.length !== 0) {
306
+ throw argCountError({
307
+ funcName: 'CURRENT_TIMESTAMP',
308
+ expected: 0,
309
+ received: args.length,
310
+ positionStart: node.positionStart,
311
+ positionEnd: node.positionEnd,
312
+ rowNumber: rowIndex,
313
+ })
314
+ }
315
+ return new Date().toISOString()
316
+ }
317
+
165
318
  if (funcName === 'JSON_OBJECT') {
166
319
  if (args.length % 2 !== 0) {
167
- throw new Error('JSON_OBJECT requires an even number of arguments (key-value pairs)')
320
+ throw argCountError({
321
+ funcName: 'JSON_OBJECT',
322
+ expected: 'even number',
323
+ received: args.length,
324
+ positionStart: node.positionStart,
325
+ positionEnd: node.positionEnd,
326
+ rowNumber: rowIndex,
327
+ })
168
328
  }
169
329
  /** @type {Record<string, SqlPrimitive>} */
170
330
  const result = {}
@@ -172,7 +332,14 @@ export async function evaluateExpr({ node, row, tables }) {
172
332
  const key = args[i]
173
333
  const value = args[i + 1]
174
334
  if (key == null) {
175
- throw new Error('JSON_OBJECT: key cannot be null')
335
+ throw argValueError({
336
+ funcName: 'JSON_OBJECT',
337
+ message: 'key cannot be null',
338
+ positionStart: node.positionStart,
339
+ positionEnd: node.positionEnd,
340
+ hint: 'All keys must be non-null values.',
341
+ rowNumber: rowIndex,
342
+ })
176
343
  }
177
344
  result[String(key)] = value
178
345
  }
@@ -180,7 +347,16 @@ export async function evaluateExpr({ node, row, tables }) {
180
347
  }
181
348
 
182
349
  if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY') {
183
- if (args.length !== 2) throw new Error(`${funcName} requires exactly 2 arguments`)
350
+ if (args.length !== 2) {
351
+ throw argCountError({
352
+ funcName,
353
+ expected: 2,
354
+ received: args.length,
355
+ positionStart: node.positionStart,
356
+ positionEnd: node.positionEnd,
357
+ rowNumber: rowIndex,
358
+ })
359
+ }
184
360
  let jsonArg = args[0]
185
361
  const pathArg = args[1]
186
362
  if (jsonArg == null || pathArg == null) return null
@@ -190,11 +366,24 @@ export async function evaluateExpr({ node, row, tables }) {
190
366
  try {
191
367
  jsonArg = JSON.parse(jsonArg)
192
368
  } catch {
193
- throw new Error(`${funcName}: invalid JSON string`)
369
+ throw argValueError({
370
+ funcName,
371
+ message: 'invalid JSON string',
372
+ positionStart: node.positionStart,
373
+ positionEnd: node.positionEnd,
374
+ hint: 'First argument must be valid JSON.',
375
+ rowNumber: rowIndex,
376
+ })
194
377
  }
195
378
  }
196
- if (typeof jsonArg !== 'object') {
197
- throw new Error(`${funcName}: first argument must be JSON string or object`)
379
+ if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
380
+ throw argValueError({
381
+ funcName,
382
+ message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
383
+ positionStart: node.positionStart,
384
+ positionEnd: node.positionEnd,
385
+ rowNumber: rowIndex,
386
+ })
198
387
  }
199
388
 
200
389
  // Parse path ("$.foo.bar[0].baz" or "foo.bar[0]")
@@ -223,11 +412,25 @@ export async function evaluateExpr({ node, row, tables }) {
223
412
  return current
224
413
  }
225
414
 
226
- throw new Error('Unsupported function ' + funcName)
415
+ if (isMathFunc(funcName)) {
416
+ return evaluateMathFunc({
417
+ funcName,
418
+ args,
419
+ positionStart: node.positionStart,
420
+ positionEnd: node.positionEnd,
421
+ rowNumber: rowIndex,
422
+ })
423
+ }
424
+
425
+ throw unknownFunctionError({
426
+ funcName,
427
+ positionStart: node.positionStart,
428
+ positionEnd: node.positionEnd,
429
+ })
227
430
  }
228
431
 
229
432
  if (node.type === 'cast') {
230
- const val = await evaluateExpr({ node: node.expr, row, tables })
433
+ const val = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
231
434
  if (val == null) return null
232
435
  const toType = node.toType.toUpperCase()
233
436
  if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
@@ -235,7 +438,15 @@ export async function evaluateExpr({ node, row, tables }) {
235
438
  return String(val)
236
439
  }
237
440
  // Can only cast primitives to other primitive types
238
- if (typeof val === 'object') throw new Error(`Cannot CAST object to type ${node.toType}`)
441
+ if (typeof val === 'object') {
442
+ throw castError({
443
+ toType: node.toType,
444
+ positionStart: node.positionStart,
445
+ positionEnd: node.positionEnd,
446
+ fromType: 'object',
447
+ rowNumber: rowIndex,
448
+ })
449
+ }
239
450
  if (toType === 'INTEGER' || toType === 'INT') {
240
451
  const num = Number(val)
241
452
  if (isNaN(num)) return null
@@ -252,21 +463,26 @@ export async function evaluateExpr({ node, row, tables }) {
252
463
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
253
464
  return Boolean(val)
254
465
  }
255
- throw new Error('Unsupported CAST to type ' + node.toType)
466
+ throw castError({
467
+ toType: node.toType,
468
+ positionStart: node.positionStart,
469
+ positionEnd: node.positionEnd,
470
+ rowNumber: rowIndex,
471
+ })
256
472
  }
257
473
 
258
474
  // IN and NOT IN with value lists
259
475
  if (node.type === 'in valuelist') {
260
- const exprVal = await evaluateExpr({ node: node.expr, row, tables })
476
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
261
477
  for (const valueNode of node.values) {
262
- const val = await evaluateExpr({ node: valueNode, row, tables })
478
+ const val = await evaluateExpr({ node: valueNode, row, tables, rowIndex })
263
479
  if (exprVal === val) return true
264
480
  }
265
481
  return false
266
482
  }
267
483
  // IN with subqueries
268
484
  if (node.type === 'in') {
269
- const exprVal = await evaluateExpr({ node: node.expr, row, tables })
485
+ const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
270
486
  const results = executeSelect(node.subquery, tables)
271
487
  /** @type {SqlPrimitive[]} */
272
488
  const values = []
@@ -291,31 +507,43 @@ export async function evaluateExpr({ node, row, tables }) {
291
507
  // CASE expressions
292
508
  if (node.type === 'case') {
293
509
  // For simple CASE: evaluate the case expression once
294
- const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
510
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables, rowIndex })
295
511
 
296
512
  // Iterate through WHEN clauses
297
513
  for (const whenClause of node.whenClauses) {
298
514
  let conditionResult
299
515
  if (caseValue !== undefined) {
300
516
  // Simple CASE: compare caseValue with condition
301
- const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables })
517
+ const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
302
518
  conditionResult = caseValue === whenValue
303
519
  } else {
304
520
  // Searched CASE: evaluate condition as boolean
305
- conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables })
521
+ conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, rowIndex })
306
522
  }
307
523
 
308
524
  if (conditionResult) {
309
- return evaluateExpr({ node: whenClause.result, row, tables })
525
+ return evaluateExpr({ node: whenClause.result, row, tables, rowIndex })
310
526
  }
311
527
  }
312
528
 
313
529
  // No WHEN clause matched, return ELSE result or NULL
314
530
  if (node.elseResult) {
315
- return evaluateExpr({ node: node.elseResult, row, tables })
531
+ return evaluateExpr({ node: node.elseResult, row, tables, rowIndex })
316
532
  }
317
533
  return null
318
534
  }
319
535
 
320
- throw new Error('Unknown expression node type ' + node.type)
536
+ // INTERVAL expressions should only appear as part of binary +/- operations
537
+ // which are handled above. A standalone interval is an error.
538
+ if (node.type === 'interval') {
539
+ throw invalidContextError({
540
+ item: 'INTERVAL',
541
+ validContext: 'date arithmetic (+ or -)',
542
+ positionStart: node.positionStart,
543
+ positionEnd: node.positionEnd,
544
+ rowNumber: rowIndex,
545
+ })
546
+ }
547
+
548
+ throw new Error(`Unknown expression node type: ${node.type}. This is an internal error - the query may contain unsupported syntax.`)
321
549
  }
@@ -1,3 +1,4 @@
1
+ import { unknownFunctionError } from '../parseErrors.js'
1
2
  import { isAggregateFunc } from '../validation.js'
2
3
  import { evaluateExpr } from './expression.js'
3
4
  import { applyBinaryOp } from './utils.js'
@@ -152,5 +153,10 @@ async function evaluateAggregateFunction(funcName, args, group, tables) {
152
153
  return max
153
154
  }
154
155
 
155
- throw new Error('Unsupported aggregate function: ' + funcName)
156
+ throw unknownFunctionError({
157
+ funcName,
158
+ positionStart: 0,
159
+ positionEnd: 0,
160
+ validFunctions: 'COUNT, SUM, AVG, MIN, MAX',
161
+ })
156
162
  }
@@ -1,3 +1,5 @@
1
+ import { missingClauseError } from '../parseErrors.js'
2
+ import { tableNotFoundError } from '../executionErrors.js'
1
3
  import { evaluateExpr } from './expression.js'
2
4
  import { stringify } from './utils.js'
3
5
 
@@ -22,7 +24,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
22
24
  const join = joins[0]
23
25
  const rightSource = tables[join.table]
24
26
  if (rightSource === undefined) {
25
- throw new Error(`Table "${join.table}" not found`)
27
+ throw tableNotFoundError({ tableName: join.table })
26
28
  }
27
29
 
28
30
  // Buffer right rows for hash index (required for hash join)
@@ -62,7 +64,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
62
64
  const join = joins[i]
63
65
  const rightSource = tables[join.table]
64
66
  if (rightSource === undefined) {
65
- throw new Error(`Table "${join.table}" not found`)
67
+ throw tableNotFoundError({ tableName: join.table })
66
68
  }
67
69
 
68
70
  /** @type {AsyncRow[]} */
@@ -98,7 +100,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
98
100
  const lastJoin = joins[joins.length - 1]
99
101
  const rightSource = tables[lastJoin.table]
100
102
  if (rightSource === undefined) {
101
- throw new Error(`Table "${lastJoin.table}" not found`)
103
+ throw tableNotFoundError({ tableName: lastJoin.table })
102
104
  }
103
105
 
104
106
  /** @type {AsyncRow[]} */
@@ -234,7 +236,10 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
234
236
  const { joinType, on: onCondition } = join
235
237
 
236
238
  if (!onCondition) {
237
- throw new Error('JOIN requires ON condition')
239
+ throw missingClauseError({
240
+ missing: 'ON condition',
241
+ context: 'JOIN',
242
+ })
238
243
  }
239
244
 
240
245
  const keys = extractJoinKeys(onCondition, leftTable, rightTable)