squirreling 0.4.3 → 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 +13 -6
- package/package.json +1 -1
- package/src/execute/columns.js +107 -0
- package/src/execute/execute.js +21 -3
- package/src/index.d.ts +3 -1
- package/src/index.js +1 -0
- package/src/types.d.ts +15 -12
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
|
-
```
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
}
|
package/src/execute/execute.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
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
|
-
|
|
48
|
-
|
|
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'
|