squirreling 0.4.2 → 0.4.4

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/README.md CHANGED
@@ -29,7 +29,7 @@ Squirreling is a streaming async SQL engine for JavaScript. It is designed to pr
29
29
 
30
30
  Squirreling returns an async generator, allowing you to process rows one at a time without loading everything into memory.
31
31
 
32
- ```javascript
32
+ ```typescript
33
33
  import { executeSql } from 'squirreling'
34
34
 
35
35
  // In-memory table
@@ -40,12 +40,18 @@ const users = [
40
40
  // ...more rows
41
41
  ]
42
42
 
43
- // Process rows as they arrive (streaming)
44
- for await (const { cnt } of executeSql({
43
+ type AsyncRow = Record<string, AsyncCell>
44
+ type AsyncCell = () => Promise<SqlPrimitive>
45
+
46
+ // Returns an async iterable of rows with async cells
47
+ const asyncRows: AsyncIterable<AsyncRow> = executeSql({
45
48
  tables: { users },
46
49
  query: 'SELECT count(*) as cnt FROM users WHERE active = TRUE LIMIT 10',
47
- })) {
48
- console.log('Count', cnt)
50
+ })
51
+
52
+ // Process rows as they arrive (streaming)
53
+ for await (const { cnt } of asyncRows) {
54
+ console.log('Count', await cnt())
49
55
  }
50
56
  ```
51
57
 
@@ -54,7 +60,8 @@ There is an exported helper function `collect` to gather all rows into an array
54
60
  ```javascript
55
61
  import { collect, executeSql } from 'squirreling'
56
62
 
57
- const allUsers = await collect(executeSql({
63
+ // Collect all rows and cells into a materialized array
64
+ const allUsers: Record<string, SqlPrimitive>[] = await collect(executeSql({
58
65
  tables: { users },
59
66
  query: 'SELECT * FROM users',
60
67
  }))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -16,6 +16,16 @@ export async function evaluateAggregate({ col, rows, tables }) {
16
16
 
17
17
  if (func === 'COUNT') {
18
18
  if (arg.kind === 'star') return rows.length
19
+ if (arg.quantifier === 'distinct') {
20
+ const seen = new Set()
21
+ for (const row of rows) {
22
+ const v = await evaluateExpr({ node: arg.expr, row, tables })
23
+ if (v !== null && v !== undefined) {
24
+ seen.add(v)
25
+ }
26
+ }
27
+ return seen.size
28
+ }
19
29
  let count = 0
20
30
  for (const row of rows) {
21
31
  const v = await evaluateExpr({ node: arg.expr, row, tables })
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @import { ExprNode, SelectStatement, SelectColumn } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * Extracts column names needed from a SELECT statement.
7
+ *
8
+ * @param {SelectStatement} select
9
+ * @returns {string[] | undefined} array of column names, or undefined if all columns needed
10
+ */
11
+ export function extractColumns(select) {
12
+ // If any column is SELECT *, we need all columns
13
+ if (select.columns.some(col => col.kind === 'star')) {
14
+ return undefined
15
+ }
16
+
17
+ /** @type {Set<string>} */
18
+ const columns = new Set()
19
+
20
+ // Columns from SELECT list
21
+ for (const col of select.columns) {
22
+ collectColumnsFromSelectColumn(col, columns)
23
+ }
24
+
25
+ // Columns from WHERE
26
+ collectColumnsFromExpr(select.where, columns)
27
+
28
+ // Columns from ORDER BY
29
+ for (const item of select.orderBy) {
30
+ collectColumnsFromExpr(item.expr, columns)
31
+ }
32
+
33
+ // Columns from GROUP BY
34
+ for (const expr of select.groupBy) {
35
+ collectColumnsFromExpr(expr, columns)
36
+ }
37
+
38
+ // Columns from HAVING
39
+ collectColumnsFromExpr(select.having, columns)
40
+
41
+ return [...columns]
42
+ }
43
+
44
+ /**
45
+ * Collects column names from a SELECT column
46
+ *
47
+ * @param {SelectColumn} col
48
+ * @param {Set<string>} columns
49
+ */
50
+ function collectColumnsFromSelectColumn(col, columns) {
51
+ if (col.kind === 'derived') {
52
+ collectColumnsFromExpr(col.expr, columns)
53
+ } else if (col.kind === 'aggregate') {
54
+ if (col.arg.kind === 'expression') {
55
+ collectColumnsFromExpr(col.arg.expr, columns)
56
+ }
57
+ // 'star' aggregate (COUNT(*)) doesn't reference specific columns
58
+ }
59
+ // 'star' columns handled separately (returns undefined for all columns)
60
+ }
61
+
62
+ /**
63
+ * Recursively collects column names (identifiers) from an expression
64
+ *
65
+ * @param {ExprNode | undefined} expr
66
+ * @param {Set<string>} columns
67
+ */
68
+ function collectColumnsFromExpr(expr, columns) {
69
+ if (!expr) return
70
+ if (expr.type === 'identifier') {
71
+ columns.add(expr.name)
72
+ } else if (expr.type === 'literal') {
73
+ // No columns
74
+ } else if (expr.type === 'binary') {
75
+ collectColumnsFromExpr(expr.left, columns)
76
+ collectColumnsFromExpr(expr.right, columns)
77
+ } else if (expr.type === 'unary') {
78
+ collectColumnsFromExpr(expr.argument, columns)
79
+ } else if (expr.type === 'function') {
80
+ for (const arg of expr.args) {
81
+ collectColumnsFromExpr(arg, columns)
82
+ }
83
+ } else if (expr.type === 'cast') {
84
+ collectColumnsFromExpr(expr.expr, columns)
85
+ } else if (expr.type === 'in valuelist') {
86
+ collectColumnsFromExpr(expr.expr, columns)
87
+ for (const val of expr.values) {
88
+ collectColumnsFromExpr(val, columns)
89
+ }
90
+ } else if (expr.type === 'in') {
91
+ collectColumnsFromExpr(expr.expr, columns)
92
+ // Subquery columns are from a different scope, don't collect
93
+ } else if (expr.type === 'exists' || expr.type === 'not exists') {
94
+ // Subquery columns are from a different scope, don't collect
95
+ } else if (expr.type === 'case') {
96
+ if (expr.caseExpr) {
97
+ collectColumnsFromExpr(expr.caseExpr, columns)
98
+ }
99
+ for (const when of expr.whenClauses) {
100
+ collectColumnsFromExpr(when.condition, columns)
101
+ collectColumnsFromExpr(when.result, columns)
102
+ }
103
+ if (expr.elseResult) {
104
+ collectColumnsFromExpr(expr.elseResult, columns)
105
+ }
106
+ }
107
+ }
@@ -5,9 +5,10 @@ import { evaluateExpr } from './expression.js'
5
5
  import { evaluateHavingExpr } from './having.js'
6
6
  import { executeJoins } from './join.js'
7
7
  import { compareForTerm, defaultDerivedAlias } from './utils.js'
8
+ import { extractColumns } from './columns.js'
8
9
 
9
10
  /**
10
- * @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
11
+ * @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, ExprNode, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
11
12
  */
12
13
 
13
14
  /**
@@ -239,7 +240,16 @@ async function* evaluateStreaming(select, dataSource, tables) {
239
240
  /** @type {Set<string> | undefined} */
240
241
  const seen = select.distinct ? new Set() : undefined
241
242
 
242
- for await (const row of dataSource.getRows()) {
243
+ // hints for data source optimization
244
+ /** @type {QueryHints} */
245
+ const hints = {
246
+ columns: extractColumns(select),
247
+ where: select.where,
248
+ limit: select.limit,
249
+ offset: select.offset,
250
+ }
251
+
252
+ for await (const row of dataSource.getRows(hints)) {
243
253
  // WHERE filter
244
254
  if (select.where) {
245
255
  const pass = await evaluateExpr({ node: select.where, row, tables })
@@ -301,10 +311,18 @@ async function* evaluateStreaming(select, dataSource, tables) {
301
311
  * @yields {AsyncRow}
302
312
  */
303
313
  async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
314
+ // Build hints for data source optimization
315
+ // Note: limit/offset not passed here since buffering needs all rows for sorting/grouping
316
+ /** @type {QueryHints} */
317
+ const hints = {
318
+ where: select.where,
319
+ columns: extractColumns(select),
320
+ }
321
+
304
322
  // Step 1: Collect all rows from data source
305
323
  /** @type {AsyncRow[]} */
306
324
  const working = []
307
- for await (const row of dataSource.getRows()) {
325
+ for await (const row of dataSource.getRows(hints)) {
308
326
  working.push(row)
309
327
  }
310
328
 
@@ -210,7 +210,7 @@ export async function evaluateExpr({ node, row, tables }) {
210
210
  if (isNaN(num)) return null
211
211
  return num
212
212
  }
213
- if (toType === 'TEXT' || toType === 'STRING') {
213
+ if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
214
214
  return String(val)
215
215
  }
216
216
  if (toType === 'BOOLEAN' || toType === 'BOOL') {
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AsyncRow, ExecuteSqlOptions, SelectStatement, SqlPrimitive } from './types.js'
1
+ import type { AsyncDataSource, AsyncRow, ExecuteSqlOptions, SelectStatement, SqlPrimitive } from './types.js'
2
2
  export type { AsyncDataSource, AsyncRow, SqlPrimitive } from './types.js'
3
3
 
4
4
  /**
@@ -26,3 +26,5 @@ export function parseSql(query: string): SelectStatement
26
26
  * @returns array of all yielded values
27
27
  */
28
28
  export function collect<T>(asyncGen: AsyncGenerator<AsyncRow>): Promise<Record<string, SqlPrimitive>[]>
29
+
30
+ export function cachedDataSource(source: AsyncDataSource): AsyncDataSource
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
3
  export { collect } from './execute/utils.js'
4
+ export { cachedDataSource } from './backend/dataSource.js'
@@ -0,0 +1,198 @@
1
+ import { isBinaryOp } from '../validation.js'
2
+ import { parseExpression, parsePrimary } from './expression.js'
3
+
4
+ /**
5
+ * @import { ExprCursor, ExprNode } from '../types.js'
6
+ */
7
+
8
+ /**
9
+ * @param {ExprCursor} c
10
+ * @returns {ExprNode}
11
+ */
12
+ export function parseComparison(c) {
13
+ const left = parsePrimary(c)
14
+ const tok = c.current()
15
+
16
+ // IS [NOT] NULL
17
+ if (tok.type === 'keyword' && tok.value === 'IS') {
18
+ c.consume()
19
+ const notToken = c.current()
20
+ if (notToken.type === 'keyword' && notToken.value === 'NOT') {
21
+ c.consume()
22
+ c.expect('keyword', 'NULL')
23
+ return {
24
+ type: 'unary',
25
+ op: 'IS NOT NULL',
26
+ argument: left,
27
+ }
28
+ }
29
+ c.expect('keyword', 'NULL')
30
+ return {
31
+ type: 'unary',
32
+ op: 'IS NULL',
33
+ argument: left,
34
+ }
35
+ }
36
+
37
+ // [NOT] LIKE
38
+ if (tok.type === 'keyword' && tok.value === 'NOT') {
39
+ const nextTok = c.peek(1)
40
+ if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
41
+ c.consume() // NOT
42
+ c.consume() // LIKE
43
+ const right = parsePrimary(c)
44
+ return {
45
+ type: 'unary',
46
+ op: 'NOT',
47
+ argument: {
48
+ type: 'binary',
49
+ op: 'LIKE',
50
+ left,
51
+ right,
52
+ },
53
+ }
54
+ }
55
+ }
56
+
57
+ if (tok.type === 'keyword' && tok.value === 'LIKE') {
58
+ c.consume()
59
+ const right = parsePrimary(c)
60
+ return {
61
+ type: 'binary',
62
+ op: 'LIKE',
63
+ left,
64
+ right,
65
+ }
66
+ }
67
+
68
+ // [NOT] BETWEEN - convert to range comparison
69
+ if (tok.type === 'keyword' && tok.value === 'NOT') {
70
+ const nextTok = c.peek(1)
71
+ if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
72
+ c.consume() // NOT
73
+ c.consume() // BETWEEN
74
+ const lower = parsePrimary(c)
75
+ c.expect('keyword', 'AND')
76
+ const upper = parsePrimary(c)
77
+ // NOT BETWEEN -> expr < lower OR expr > upper
78
+ return {
79
+ type: 'binary',
80
+ op: 'OR',
81
+ left: { type: 'binary', op: '<', left, right: lower },
82
+ right: { type: 'binary', op: '>', left, right: upper },
83
+ }
84
+ }
85
+ }
86
+
87
+ if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
88
+ c.consume()
89
+ const lower = parsePrimary(c)
90
+ c.expect('keyword', 'AND')
91
+ const upper = parsePrimary(c)
92
+ // BETWEEN -> expr >= lower AND expr <= upper
93
+ return {
94
+ type: 'binary',
95
+ op: 'AND',
96
+ left: { type: 'binary', op: '>=', left, right: lower },
97
+ right: { type: 'binary', op: '<=', left, right: upper },
98
+ }
99
+ }
100
+
101
+ // [NOT] IN
102
+ if (tok.type === 'keyword' && tok.value === 'NOT') {
103
+ const nextTok = c.peek(1)
104
+ if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
105
+ c.consume() // NOT
106
+ c.consume() // IN
107
+
108
+ // Check if it's a subquery or a list of values by peeking ahead
109
+ // parseSubquery expects to consume the opening paren itself
110
+ const parenTok = c.current()
111
+ if (parenTok.type !== 'paren' || parenTok.value !== '(') {
112
+ throw new Error('Expected ( after IN')
113
+ }
114
+ const peekTok = c.peek(1)
115
+ if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
116
+ // Subquery - let parseSubquery handle the parens
117
+ const subquery = c.parseSubquery()
118
+ return {
119
+ type: 'unary',
120
+ op: 'NOT',
121
+ argument: {
122
+ type: 'in',
123
+ expr: left,
124
+ subquery,
125
+ },
126
+ }
127
+ } else {
128
+ // Parse list of values - we handle the parens
129
+ c.consume() // '('
130
+ /** @type {ExprNode[]} */
131
+ const values = []
132
+ while (true) {
133
+ values.push(parseExpression(c))
134
+ if (!c.match('comma')) break
135
+ }
136
+ c.expect('paren', ')')
137
+ return {
138
+ type: 'unary',
139
+ op: 'NOT',
140
+ argument: {
141
+ type: 'in valuelist',
142
+ expr: left,
143
+ values,
144
+ },
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ if (tok.type === 'keyword' && tok.value === 'IN') {
151
+ c.consume() // IN
152
+
153
+ // Check if it's a subquery or a list of values by peeking ahead
154
+ // parseSubquery expects to consume the opening paren itself
155
+ const parenTok = c.current()
156
+ if (parenTok.type !== 'paren' || parenTok.value !== '(') {
157
+ throw new Error('Expected ( after IN')
158
+ }
159
+ const peekTok = c.peek(1)
160
+ if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
161
+ // Subquery - let parseSubquery handle the parens
162
+ const subquery = c.parseSubquery()
163
+ return {
164
+ type: 'in',
165
+ expr: left,
166
+ subquery,
167
+ }
168
+ } else {
169
+ // Parse list of values - we handle the parens
170
+ c.consume() // '('
171
+ /** @type {ExprNode[]} */
172
+ const values = []
173
+ while (true) {
174
+ values.push(parseExpression(c))
175
+ if (!c.match('comma')) break
176
+ }
177
+ c.expect('paren', ')')
178
+ return {
179
+ type: 'in valuelist',
180
+ expr: left,
181
+ values,
182
+ }
183
+ }
184
+ }
185
+
186
+ if (tok.type === 'operator' && isBinaryOp(tok.value)) {
187
+ c.consume()
188
+ const right = parsePrimary(c)
189
+ return {
190
+ type: 'binary',
191
+ op: tok.value,
192
+ left,
193
+ right,
194
+ }
195
+ }
196
+
197
+ return left
198
+ }
@@ -1,7 +1,8 @@
1
1
  import { isAggregateFunc, isStringFunc } from '../validation.js'
2
+ import { parseComparison } from './comparison.js'
2
3
 
3
4
  /**
4
- * @import { BinaryOp, ExprCursor, ExprNode, WhenClause } from '../types.js'
5
+ * @import { ExprCursor, ExprNode, WhenClause } from '../types.js'
5
6
  */
6
7
 
7
8
  /**
@@ -16,7 +17,7 @@ export function parseExpression(c) {
16
17
  * @param {ExprCursor} c
17
18
  * @returns {ExprNode}
18
19
  */
19
- function parsePrimary(c) {
20
+ export function parsePrimary(c) {
20
21
  const tok = c.current()
21
22
 
22
23
  if (tok.type === 'paren' && tok.value === '(') {
@@ -253,9 +254,6 @@ function parseNot(c) {
253
254
  const nextTok = c.current()
254
255
  if (nextTok.type === 'keyword' && nextTok.value === 'EXISTS') {
255
256
  c.consume() // EXISTS
256
- if (!c.parseSubquery) {
257
- throw new Error('Subquery parsing not available in this context')
258
- }
259
257
  const subquery = c.parseSubquery()
260
258
  return {
261
259
  type: 'not exists',
@@ -271,217 +269,3 @@ function parseNot(c) {
271
269
  }
272
270
  return parseComparison(c)
273
271
  }
274
-
275
- /**
276
- * @param {ExprCursor} c
277
- * @returns {ExprNode}
278
- */
279
- function parseComparison(c) {
280
- const left = parsePrimary(c)
281
- const tok = c.current()
282
-
283
- // IS [NOT] NULL
284
- if (tok.type === 'keyword' && tok.value === 'IS') {
285
- c.consume()
286
- const notToken = c.current()
287
- if (notToken.type === 'keyword' && notToken.value === 'NOT') {
288
- c.consume()
289
- c.expect('keyword', 'NULL')
290
- return {
291
- type: 'unary',
292
- op: 'IS NOT NULL',
293
- argument: left,
294
- }
295
- }
296
- c.expect('keyword', 'NULL')
297
- return {
298
- type: 'unary',
299
- op: 'IS NULL',
300
- argument: left,
301
- }
302
- }
303
-
304
- // [NOT] LIKE
305
- if (tok.type === 'keyword' && tok.value === 'NOT') {
306
- const nextTok = c.peek(1)
307
- if (nextTok.type === 'keyword' && nextTok.value === 'LIKE') {
308
- c.consume() // NOT
309
- c.consume() // LIKE
310
- const right = parsePrimary(c)
311
- return {
312
- type: 'unary',
313
- op: 'NOT',
314
- argument: {
315
- type: 'binary',
316
- op: 'LIKE',
317
- left,
318
- right,
319
- },
320
- }
321
- }
322
- }
323
-
324
- if (tok.type === 'keyword' && tok.value === 'LIKE') {
325
- c.consume()
326
- const right = parsePrimary(c)
327
- return {
328
- type: 'binary',
329
- op: 'LIKE',
330
- left,
331
- right,
332
- }
333
- }
334
-
335
- // [NOT] BETWEEN - convert to range comparison
336
- if (tok.type === 'keyword' && tok.value === 'NOT') {
337
- const nextTok = c.peek(1)
338
- if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
339
- c.consume() // NOT
340
- c.consume() // BETWEEN
341
- const lower = parsePrimary(c)
342
- c.expect('keyword', 'AND')
343
- const upper = parsePrimary(c)
344
- // NOT BETWEEN -> expr < lower OR expr > upper
345
- return {
346
- type: 'binary',
347
- op: 'OR',
348
- left: { type: 'binary', op: '<', left, right: lower },
349
- right: { type: 'binary', op: '>', left, right: upper },
350
- }
351
- }
352
- }
353
-
354
- if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
355
- c.consume()
356
- const lower = parsePrimary(c)
357
- c.expect('keyword', 'AND')
358
- const upper = parsePrimary(c)
359
- // BETWEEN -> expr >= lower AND expr <= upper
360
- return {
361
- type: 'binary',
362
- op: 'AND',
363
- left: { type: 'binary', op: '>=', left, right: lower },
364
- right: { type: 'binary', op: '<=', left, right: upper },
365
- }
366
- }
367
-
368
- // [NOT] IN
369
- if (tok.type === 'keyword' && tok.value === 'NOT') {
370
- const nextTok = c.peek(1)
371
- if (nextTok.type === 'keyword' && nextTok.value === 'IN') {
372
- c.consume() // NOT
373
- c.consume() // IN
374
-
375
- // Check if it's a subquery or a list of values by peeking ahead
376
- // parseSubquery expects to consume the opening paren itself
377
- const parenTok = c.current()
378
- if (parenTok.type !== 'paren' || parenTok.value !== '(') {
379
- throw new Error('Expected ( after IN')
380
- }
381
- const peekTok = c.peek(1)
382
- if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
383
- // Subquery - let parseSubquery handle the parens
384
- if (!c.parseSubquery) {
385
- throw new Error('Subquery parsing not available in this context')
386
- }
387
- const subquery = c.parseSubquery()
388
- return {
389
- type: 'unary',
390
- op: 'NOT',
391
- argument: {
392
- type: 'in',
393
- expr: left,
394
- subquery,
395
- },
396
- }
397
- } else {
398
- // Parse list of values - we handle the parens
399
- c.consume() // '('
400
- /** @type {ExprNode[]} */
401
- const values = []
402
- while (true) {
403
- values.push(parseExpression(c))
404
- if (!c.match('comma')) break
405
- }
406
- c.expect('paren', ')')
407
- return {
408
- type: 'unary',
409
- op: 'NOT',
410
- argument: {
411
- type: 'in valuelist',
412
- expr: left,
413
- values,
414
- },
415
- }
416
- }
417
- }
418
- }
419
-
420
- if (tok.type === 'keyword' && tok.value === 'IN') {
421
- c.consume() // IN
422
-
423
- // Check if it's a subquery or a list of values by peeking ahead
424
- // parseSubquery expects to consume the opening paren itself
425
- const parenTok = c.current()
426
- if (parenTok.type !== 'paren' || parenTok.value !== '(') {
427
- throw new Error('Expected ( after IN')
428
- }
429
- const peekTok = c.peek(1)
430
- if (peekTok.type === 'keyword' && peekTok.value === 'SELECT') {
431
- // Subquery - let parseSubquery handle the parens
432
- if (!c.parseSubquery) {
433
- throw new Error('Subquery parsing not available in this context')
434
- }
435
- const subquery = c.parseSubquery()
436
- return {
437
- type: 'in',
438
- expr: left,
439
- subquery,
440
- }
441
- } else {
442
- // Parse list of values - we handle the parens
443
- c.consume() // '('
444
- /** @type {ExprNode[]} */
445
- const values = []
446
- while (true) {
447
- values.push(parseExpression(c))
448
- if (!c.match('comma')) break
449
- }
450
- c.expect('paren', ')')
451
- return {
452
- type: 'in valuelist',
453
- expr: left,
454
- values,
455
- }
456
- }
457
- }
458
-
459
- if (tok.type === 'operator' && isComparisonOperator(tok.value)) {
460
- c.consume()
461
- const right = parsePrimary(c)
462
- return {
463
- type: 'binary',
464
- op: tok.value,
465
- left,
466
- right,
467
- }
468
- }
469
-
470
- return left
471
- }
472
-
473
- /**
474
- * @param {string} op
475
- * @returns {op is BinaryOp}
476
- */
477
- function isComparisonOperator(op) {
478
- return (
479
- op === '=' ||
480
- op === '!=' ||
481
- op === '<>' ||
482
- op === '<' ||
483
- op === '>' ||
484
- op === '<=' ||
485
- op === '>='
486
- )
487
- }
@@ -229,31 +229,22 @@ function parseAggregateItem(state, func) {
229
229
  if (cur.type === 'operator' && cur.value === '*') {
230
230
  consume(state)
231
231
  arg = { kind: 'star' }
232
- } else if (cur.type === 'identifier' && cur.value === 'CAST') {
233
- // Handle CAST inside aggregate: SUM(CAST(x AS type))
234
- expectIdentifier(state) // consume CAST
235
- expect(state, 'paren', '(')
236
- const cursor = createExprCursor(state)
237
- const expr = parseExpression(cursor)
238
- expect(state, 'keyword', 'AS')
239
- const toType = expectIdentifier(state).value
240
- expect(state, 'paren', ')')
241
- arg = {
242
- kind: 'expression',
243
- expr: { type: 'cast', expr, toType },
244
- }
245
232
  } else {
246
- // column name
247
- let name = expectIdentifier(state).value
248
- // Handle qualified column names like orders.amount
249
- if (current(state).type === 'dot') {
250
- consume(state) // consume dot
251
- const qualifiedPart = expectIdentifier(state)
252
- name = `${name}.${qualifiedPart.value}`
233
+ /** @type {'all' | 'distinct'} */
234
+ let quantifier = 'all'
235
+ if (cur.type === 'keyword' && cur.value === 'ALL') {
236
+ consume(state) // consume ALL
237
+ } else if (cur.type === 'keyword' && cur.value === 'DISTINCT') {
238
+ consume(state)
239
+ quantifier = 'distinct'
253
240
  }
241
+
242
+ const cursor = createExprCursor(state)
243
+ const expr = parseExpression(cursor)
254
244
  arg = {
255
245
  kind: 'expression',
256
- expr: { type: 'identifier', name },
246
+ expr,
247
+ quantifier,
257
248
  }
258
249
  }
259
250
 
@@ -20,6 +20,7 @@ const KEYWORDS = new Set([
20
20
  'LIMIT',
21
21
  'OFFSET',
22
22
  'AS',
23
+ 'ALL',
23
24
  'DISTINCT',
24
25
  'TRUE',
25
26
  'FALSE',
package/src/types.d.ts CHANGED
@@ -1,10 +1,21 @@
1
1
 
2
+ /**
3
+ * Hints passed to data sources for query optimization.
4
+ * All hints are optional and "best effort" - sources may ignore them.
5
+ */
6
+ export interface QueryHints {
7
+ columns?: string[] // columns needed
8
+ where?: ExprNode // where clause
9
+ limit?: number
10
+ offset?: number
11
+ }
12
+
2
13
  /**
3
14
  * Async data source for streaming SQL execution.
4
15
  * Provides an async iterator over rows.
5
16
  */
6
17
  export interface AsyncDataSource {
7
- getRows(): AsyncIterable<AsyncRow>
18
+ getRows(hints?: QueryHints): AsyncIterable<AsyncRow>
8
19
  }
9
20
  export type AsyncRow = Record<string, AsyncCell>
10
21
  export type AsyncCell = () => Promise<SqlPrimitive>
@@ -43,17 +54,9 @@ export interface FromSubquery {
43
54
  alias: string
44
55
  }
45
56
 
46
- export type BinaryOp =
47
- | 'AND'
48
- | 'OR'
49
- | '='
50
- | '!='
51
- | '<>'
52
- | '<'
53
- | '>'
54
- | '<='
55
- | '>='
56
- | 'LIKE'
57
+ export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp
58
+
59
+ export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
57
60
 
58
61
  export interface LiteralNode {
59
62
  type: 'literal'
@@ -154,6 +157,7 @@ export interface AggregateArgStar {
154
157
  export interface AggregateArgExpression {
155
158
  kind: 'expression'
156
159
  expr: ExprNode
160
+ quantifier: 'all' | 'distinct'
157
161
  }
158
162
 
159
163
  export type AggregateArg = AggregateArgStar | AggregateArgExpression
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, StringFunc} from './types.js'
3
+ * @import {AggregateFunc, BinaryOp, StringFunc} from './types.js'
4
4
  * @param {string} name
5
5
  * @returns {name is AggregateFunc}
6
6
  */
@@ -15,3 +15,11 @@ export function isAggregateFunc(name) {
15
15
  export function isStringFunc(name) {
16
16
  return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'SUBSTR', 'TRIM', 'REPLACE', 'RANDOM', 'RAND'].includes(name)
17
17
  }
18
+
19
+ /**
20
+ * @param {string} op
21
+ * @returns {op is BinaryOp}
22
+ */
23
+ export function isBinaryOp(op) {
24
+ return ['=', '!=', '<>', '<', '>', '<=', '>='].includes(op)
25
+ }