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 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.3",
3
+ "version": "0.4.4",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -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
 
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'
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'