squirreling 0.1.2 → 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/README.md CHANGED
@@ -24,12 +24,12 @@ Squirreling is a lightweight SQL engine for JavaScript applications, designed to
24
24
  ```javascript
25
25
  import { executeSql } from 'squirreling'
26
26
 
27
- const data = [
27
+ const source = [
28
28
  { id: 1, name: 'Alice' },
29
29
  { id: 2, name: 'Bob' },
30
30
  ]
31
31
 
32
- const result = executeSql(data, 'SELECT UPPER(name) AS name_upper FROM users')
32
+ const result = executeSql({ source, sql: 'SELECT UPPER(name) AS name_upper FROM users' })
33
33
  console.log(result)
34
34
  // Output: [ { name_upper: 'ALICE' }, { name_upper: 'BOB' } ]
35
35
  ```
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
+ "description": "",
4
5
  "author": "Hyperparam",
5
6
  "homepage": "https://hyperparam.app",
7
+ "keywords": [
8
+ "sql",
9
+ "data",
10
+ "dataset",
11
+ "hyperparam",
12
+ "hyparquet",
13
+ "parquet"
14
+ ],
6
15
  "license": "MIT",
7
16
  "repository": {
8
17
  "type": "git",
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @import { DataSource, RowSource } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * Creates a row accessor that wraps a plain JavaScript object
7
+ *
8
+ * @param {Record<string, any>} obj - the plain object
9
+ * @returns {RowSource} a row accessor interface
10
+ */
11
+ export function createRowAccessor(obj) {
12
+ return {
13
+ getCell(name) {
14
+ return obj[name]
15
+ },
16
+ getKeys() {
17
+ return Object.keys(obj)
18
+ },
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Creates a memory-backed data source from an array of plain objects
24
+ *
25
+ * @param {Record<string, any>[]} data - array of plain objects
26
+ * @returns {DataSource} a data source interface
27
+ */
28
+ export function createMemorySource(data) {
29
+ return {
30
+ getNumRows() {
31
+ return data.length
32
+ },
33
+ getRow(index) {
34
+ return createRowAccessor(data[index])
35
+ },
36
+ }
37
+ }
@@ -3,9 +3,9 @@ import { evaluateExpr } from './expression.js'
3
3
  /**
4
4
  * Evaluates an aggregate function over a set of rows
5
5
  *
6
- * @import { AggregateColumn, ExprNode, Row } from '../types.js'
6
+ * @import { AggregateColumn, ExprNode, RowSource } from '../types.js'
7
7
  * @param {AggregateColumn} col - aggregate column definition
8
- * @param {Row[]} rows - rows to aggregate
8
+ * @param {RowSource[]} rows - rows to aggregate
9
9
  * @returns {number | null} aggregated result
10
10
  */
11
11
  export function evaluateAggregate(col, rows) {
@@ -77,8 +77,7 @@ export function defaultAggregateAlias(col) {
77
77
  * @param {ExprNode} expr
78
78
  * @returns {string}
79
79
  */
80
- export
81
- function defaultAggregateAliasExpr(expr) {
80
+ export function defaultAggregateAliasExpr(expr) {
82
81
  if (expr.type === 'identifier') {
83
82
  return expr.name
84
83
  }
@@ -1,27 +1,30 @@
1
1
  /**
2
- * @import { FunctionColumn, FunctionNode, OrderByItem, Row, SelectStatement, SqlPrimitive } from '../types.js'
2
+ * @import { DataSource, ExecuteSqlOptions, FunctionColumn, FunctionNode, OrderByItem, RowSource, SelectStatement, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
6
6
  import { evaluateExpr } from './expression.js'
7
7
  import { createHavingContext, evaluateHavingExpr } from './having.js'
8
8
  import { parseSql } from '../parse/parse.js'
9
+ import { createMemorySource, createRowAccessor } from '../backend/memory.js'
9
10
 
10
11
  /**
11
- * Executes a SQL SELECT query against an array of data rows
12
- * @param {Row[]} rows - The data rows to query
13
- * @param {string} sql - The SQL query string
14
- * @returns {Row[]} The result rows matching the query
12
+ * Executes a SQL SELECT query against a data source
13
+ *
14
+ * @param {ExecuteSqlOptions} options - the execution options
15
+ * @returns {Record<string, any>[]} the result rows matching the query
15
16
  */
16
- export function executeSql(rows, sql) {
17
+ export function executeSql({ source, sql }) {
17
18
  const select = parseSql(sql)
18
- return evaluateSelectAst(select, rows)
19
+ const dataSource = Array.isArray(source) ? createMemorySource(source) : source
20
+ return evaluateSelectAst(select, dataSource)
19
21
  }
20
22
 
21
23
  /**
22
24
  * Generates a default alias name for a string function
23
- * @param {FunctionColumn} col - The function column definition
24
- * @returns {string} The generated alias (e.g., "upper_name", "concat_a_b")
25
+ *
26
+ * @param {FunctionColumn} col - the function column definition
27
+ * @returns {string} the generated alias (e.g., "upper_name", "concat_a_b")
25
28
  */
26
29
  function defaultFunctionAlias(col) {
27
30
  const base = col.func.toLowerCase()
@@ -37,8 +40,9 @@ function defaultFunctionAlias(col) {
37
40
 
38
41
  /**
39
42
  * Creates a stable string key for a row to enable deduplication
40
- * @param {Row} row - The data row
41
- * @returns {string} A stable string representation of the row
43
+ *
44
+ * @param {Record<string, any>} row
45
+ * @returns {string} a stable string representation of the row
42
46
  */
43
47
  function stableRowKey(row) {
44
48
  const keys = Object.keys(row).sort()
@@ -53,9 +57,10 @@ function stableRowKey(row) {
53
57
 
54
58
  /**
55
59
  * Compares two SQL values for sorting
56
- * @param {SqlPrimitive} a - First value to compare
57
- * @param {SqlPrimitive} b - Second value to compare
58
- * @returns {number} Negative if a < b, positive if a > b, 0 if equal
60
+ *
61
+ * @param {SqlPrimitive} a
62
+ * @param {SqlPrimitive} b
63
+ * @returns {number} negative if a < b, positive if a > b, 0 if equal
59
64
  */
60
65
  function compareValues(a, b) {
61
66
  if (a === b) return 0
@@ -77,15 +82,16 @@ function compareValues(a, b) {
77
82
 
78
83
  /**
79
84
  * Applies DISTINCT filtering to remove duplicate rows
80
- * @param {Row[]} rows - The input rows
85
+ *
86
+ * @param {Record<string, any>[]} rows - The input rows
81
87
  * @param {boolean} distinct - Whether to apply deduplication
82
- * @returns {Row[]} The deduplicated rows
88
+ * @returns {Record<string, any>[]} The deduplicated rows
83
89
  */
84
90
  function applyDistinct(rows, distinct) {
85
91
  if (!distinct) return rows
86
92
  /** @type {Set<string>} */
87
93
  const seen = new Set()
88
- /** @type {Row[]} */
94
+ /** @type {Record<string, any>[]} */
89
95
  const result = []
90
96
  for (const row of rows) {
91
97
  const key = stableRowKey(row)
@@ -98,20 +104,20 @@ function applyDistinct(rows, distinct) {
98
104
 
99
105
  /**
100
106
  * Applies ORDER BY sorting to rows
101
- * @param {Row[]} rows - The input rows
102
- * @param {OrderByItem[]} orderBy - The sort specifications
103
- * @returns {Row[]} The sorted rows
107
+ *
108
+ * @param {Record<string, any>[]} rows - the input rows
109
+ * @param {OrderByItem[]} orderBy - the sort specifications
110
+ * @returns {Record<string, any>[]} the sorted rows
104
111
  */
105
112
  function applyOrderBy(rows, orderBy) {
106
- if (!orderBy || orderBy.length === 0) return rows
113
+ if (!orderBy?.length) return rows
107
114
 
108
115
  const sorted = rows.slice()
109
-
110
116
  sorted.sort((a, b) => {
111
117
  for (const term of orderBy) {
112
118
  const dir = term.direction
113
- const av = evaluateExpr(term.expr, a)
114
- const bv = evaluateExpr(term.expr, b)
119
+ const av = evaluateExpr(term.expr, createRowAccessor(a))
120
+ const bv = evaluateExpr(term.expr, createRowAccessor(b))
115
121
  const cmp = compareValues(av, bv)
116
122
  if (cmp !== 0) {
117
123
  return dir === 'DESC' ? -cmp : cmp
@@ -125,41 +131,41 @@ function applyOrderBy(rows, orderBy) {
125
131
 
126
132
  /**
127
133
  * Evaluates a parsed SELECT AST against data rows
128
- * @param {SelectStatement} select - The parsed SQL AST
129
- * @param {Row[]} rows - The data rows
130
- * @returns {Row[]} The filtered, projected, and sorted result rows
134
+ *
135
+ * @param {SelectStatement} select - the parsed SQL AST
136
+ * @param {DataSource} dataSource - the data source
137
+ * @returns {Record<string, any>[]} the filtered, projected, and sorted result rows
131
138
  */
132
- function evaluateSelectAst(select, rows) {
139
+ function evaluateSelectAst(select, dataSource) {
133
140
  // Check for unsupported JOIN operations
134
141
  if (select.joins.length) {
135
142
  throw new Error('JOIN is not supported')
136
143
  }
137
144
 
138
- // WHERE
139
- let working = rows
140
- if (select.where) {
141
- /** @type {Row[]} */
142
- const filtered = []
143
- for (const row of rows) {
144
- if (evaluateExpr(select.where, row)) {
145
- filtered.push(row)
146
- }
145
+ // WHERE clause filtering
146
+ /** @type {RowSource[]} */
147
+ const working = []
148
+ const length = dataSource.getNumRows()
149
+ for (let i = 0; i < length; i++) {
150
+ const row = dataSource.getRow(i)
151
+ if (!select.where || evaluateExpr(select.where, row)) {
152
+ working.push(row)
147
153
  }
148
- working = filtered
149
154
  }
150
155
 
151
156
  const hasAggregate = select.columns.some(col => col.kind === 'aggregate')
152
157
  const useGrouping = hasAggregate || select.groupBy?.length > 0
153
158
 
154
- /** @type {Row[]} */
159
+ /** @type {Record<string, any>[]} */
155
160
  const projected = []
156
161
 
157
162
  if (useGrouping) {
158
- /** @type {Row[][]} */
163
+ // Grouping due to GROUP BY or aggregate functions
164
+ /** @type {RowSource[][]} */
159
165
  const groups = []
160
166
 
161
167
  if (select.groupBy?.length) {
162
- /** @type {Map<string, Row[]>} */
168
+ /** @type {Map<string, RowSource[]>} */
163
169
  const map = new Map()
164
170
  for (const row of working) {
165
171
  /** @type {string[]} */
@@ -187,14 +193,16 @@ function evaluateSelectAst(select, rows) {
187
193
  }
188
194
 
189
195
  for (const group of groups) {
190
- /** @type {Row} */
196
+ /** @type {Record<string, any>} */
191
197
  const resultRow = {}
192
198
  for (const col of select.columns) {
193
199
  if (col.kind === 'star') {
194
- const firstRow = group[0] || {}
195
- const keys = Object.keys(firstRow)
196
- for (const key of keys) {
197
- resultRow[key] = firstRow[key]
200
+ const firstRow = group[0]
201
+ if (firstRow) {
202
+ const keys = firstRow.getKeys()
203
+ for (const key of keys) {
204
+ resultRow[key] = firstRow.getCell(key)
205
+ }
198
206
  }
199
207
  continue
200
208
  }
@@ -203,7 +211,7 @@ function evaluateSelectAst(select, rows) {
203
211
  const name = col.column
204
212
  const alias = col.alias ?? name
205
213
  // Evaluate on first row of group (all rows have same value for GROUP BY columns)
206
- resultRow[alias] = group.length > 0 ? group[0][name] : undefined
214
+ resultRow[alias] = group[0]?.getCell(name)
207
215
  continue
208
216
  }
209
217
 
@@ -245,19 +253,20 @@ function evaluateSelectAst(select, rows) {
245
253
  projected.push(resultRow)
246
254
  }
247
255
  } else {
256
+ // No grouping, simple projection
248
257
  for (const row of working) {
249
- /** @type {Row} */
258
+ /** @type {Record<string, any>} */
250
259
  const outRow = {}
251
260
  for (const col of select.columns) {
252
261
  if (col.kind === 'star') {
253
- const keys = Object.keys(row)
262
+ const keys = row.getKeys()
254
263
  for (const key of keys) {
255
- outRow[key] = row[key]
264
+ outRow[key] = row.getCell(key)
256
265
  }
257
266
  } else if (col.kind === 'column') {
258
267
  const name = col.column
259
268
  const alias = col.alias ?? name
260
- outRow[alias] = row[name]
269
+ outRow[alias] = row.getCell(name)
261
270
  } else if (col.kind === 'function') {
262
271
  /** @type {FunctionNode} */
263
272
  const funcNode = { type: 'function', name: col.func, args: col.args }
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * Evaluates an expression node against a row of data
4
4
  *
5
- * @import { ExprNode, Row, SqlPrimitive } from '../types.js'
5
+ * @import { ExprNode, RowSource, SqlPrimitive } from '../types.js'
6
6
  * @param {ExprNode} node - The expression node to evaluate
7
- * @param {Row} row - The data row to evaluate against
7
+ * @param {RowSource} row - The data row to evaluate against
8
8
  * @returns {SqlPrimitive} The result of the evaluation
9
9
  */
10
10
  export function evaluateExpr(node, row) {
@@ -13,9 +13,10 @@ export function evaluateExpr(node, row) {
13
13
  }
14
14
 
15
15
  if (node.type === 'identifier') {
16
- return row[node.name]
16
+ return row.getCell(node.name)
17
17
  }
18
18
 
19
+ // Unary operators
19
20
  if (node.type === 'unary') {
20
21
  if (node.op === 'NOT') {
21
22
  return !evaluateExpr(node.argument, row)
@@ -33,6 +34,7 @@ export function evaluateExpr(node, row) {
33
34
  }
34
35
  }
35
36
 
37
+ // Binary operators
36
38
  if (node.type === 'binary') {
37
39
  if (node.op === 'AND') {
38
40
  const leftVal = evaluateExpr(node.left, row)
@@ -1,35 +1,56 @@
1
1
  /**
2
- * @import { ExprNode, Row, SqlPrimitive } from '../types.js'
2
+ * @import { AggregateFunc, ExprNode, RowSource, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
+ import { isAggregateFunc } from '../validation.js'
5
6
  import { evaluateExpr } from './expression.js'
6
7
 
7
8
  /**
8
9
  * Creates a context for evaluating HAVING expressions
9
- * @param {Row} resultRow - The aggregated result row
10
- * @param {Row[]} group - The group of rows
11
- * @returns {Row} A context row for HAVING evaluation
10
+ *
11
+ * @param {Record<string, any>} resultRow - the aggregated result row
12
+ * @param {RowSource[]} group - the group of rows
13
+ * @returns {RowSource} a context row for HAVING evaluation
12
14
  */
13
15
  export function createHavingContext(resultRow, group) {
14
16
  // Include the first row of the group (for GROUP BY columns)
15
- const firstRow = group[0] || {}
17
+ const firstRow = group[0]
18
+ /** @type {Record<string, any>} */
19
+ const context = {}
20
+ if (firstRow) {
21
+ const keys = firstRow.getKeys()
22
+ for (const key of keys) {
23
+ context[key] = firstRow.getCell(key)
24
+ }
25
+ }
16
26
  // Merge with result row (which has aggregates computed)
17
- return { ...firstRow, ...resultRow }
27
+ Object.assign(context, resultRow)
28
+
29
+ // Return a Row accessor wrapping the context
30
+ return {
31
+ getCell(name) {
32
+ return context[name]
33
+ },
34
+ getKeys() {
35
+ return Object.keys(context)
36
+ },
37
+ }
18
38
  }
19
39
 
20
40
  /**
21
41
  * Evaluates a HAVING expression with support for aggregate functions
22
- * @param {ExprNode} expr - The HAVING expression
23
- * @param {Row} context - The context row with aggregated values
24
- * @param {Row[]} group - The group of rows for re-evaluating aggregates
25
- * @returns {boolean} Whether the HAVING condition is satisfied
42
+ *
43
+ * @param {ExprNode} expr - the HAVING expression
44
+ * @param {RowSource} context - the context row with aggregated values
45
+ * @param {RowSource[]} group - the group of rows for re-evaluating aggregates
46
+ * @returns {boolean} whether the HAVING condition is satisfied
26
47
  */
27
48
  export function evaluateHavingExpr(expr, context, group) {
28
49
  // For HAVING, we need special handling of aggregate functions
29
50
  // They need to be re-evaluated against the group
30
51
  if (expr.type === 'function') {
31
52
  const funcName = expr.name.toUpperCase()
32
- if (['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(funcName)) {
53
+ if (isAggregateFunc(funcName)) {
33
54
  // Evaluate aggregate function on the group
34
55
  return Boolean(evaluateAggregateFunction(funcName, expr.args, group))
35
56
  }
@@ -103,15 +124,16 @@ export function evaluateHavingExpr(expr, context, group) {
103
124
 
104
125
  /**
105
126
  * Evaluates a value in a HAVING expression
106
- * @param {ExprNode} expr - The expression
107
- * @param {Row} context - The context row
108
- * @param {Row[]} group - The group of rows
109
- * @returns {SqlPrimitive} The evaluated value
127
+ *
128
+ * @param {ExprNode} expr
129
+ * @param {RowSource} context - the context row
130
+ * @param {RowSource[]} group - the group of rows
131
+ * @returns {SqlPrimitive} the evaluated value
110
132
  */
111
133
  function evaluateHavingValue(expr, context, group) {
112
134
  if (expr.type === 'function') {
113
135
  const funcName = expr.name.toUpperCase()
114
- if (['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(funcName)) {
136
+ if (isAggregateFunc(funcName)) {
115
137
  return evaluateAggregateFunction(funcName, expr.args, group)
116
138
  }
117
139
  }
@@ -126,10 +148,11 @@ function evaluateHavingValue(expr, context, group) {
126
148
 
127
149
  /**
128
150
  * Evaluates an aggregate function on a group
129
- * @param {string} funcName - The aggregate function name
130
- * @param {ExprNode[]} args - The function arguments
131
- * @param {Row[]} group - The group of rows
132
- * @returns {SqlPrimitive} The aggregate result
151
+ *
152
+ * @param {AggregateFunc} funcName - aggregate function name
153
+ * @param {ExprNode[]} args - function arguments
154
+ * @param {RowSource[]} group - the group of rows
155
+ * @returns {SqlPrimitive} the aggregate result
133
156
  */
134
157
  function evaluateAggregateFunction(funcName, args, group) {
135
158
  if (funcName === 'COUNT') {
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Row, SelectStatement } from './types.js'
1
+ import type { RowSource, SelectStatement } from './types.js'
2
2
 
3
3
  /**
4
4
  * Executes a SQL SELECT query against an array of data rows
@@ -7,7 +7,7 @@ import type { Row, SelectStatement } from './types.js'
7
7
  * @param sql - SQL query string
8
8
  * @returns rows matching the query
9
9
  */
10
- export function executeSql(rows: Row[], sql: string): Row[]
10
+ export function executeSql(rows: RowSource[], sql: string): RowSource[]
11
11
 
12
12
  /**
13
13
  * Parses a SQL query string into an abstract syntax tree
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { tokenize } from './tokenize.js'
6
6
  import { parseExpression, parsePrimary } from './expression.js'
7
+ import { isAggregateFunc, isStringFunc } from '../validation.js'
7
8
 
8
9
  // Keywords that cannot be used as implicit aliases after a column
9
10
  const RESERVED_AFTER_COLUMN = new Set([
@@ -522,19 +523,3 @@ function parseError(state, expected) {
522
523
  const after = prevToken ? ` after "${prevToken.originalValue ?? prevToken.value}"` : ''
523
524
  return new Error(`Expected ${expected}${after} at position ${tok.position}`)
524
525
  }
525
-
526
- /**
527
- * @param {string} name
528
- * @returns {name is AggregateFunc}
529
- */
530
- function isAggregateFunc(name) {
531
- return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(name)
532
- }
533
-
534
- /**
535
- * @param {string} name
536
- * @returns {name is StringFunc}
537
- */
538
- function isStringFunc(name) {
539
- return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'TRIM'].includes(name)
540
- }
package/src/types.d.ts CHANGED
@@ -1,4 +1,17 @@
1
- export type Row = Record<string, any>
1
+ export interface RowSource {
2
+ getCell(name: string): any
3
+ getKeys(): string[]
4
+ }
5
+
6
+ export interface DataSource {
7
+ getNumRows(): number
8
+ getRow(index: number): RowSource
9
+ }
10
+
11
+ export interface ExecuteSqlOptions {
12
+ source: Record<string, any>[] | DataSource
13
+ sql: string
14
+ }
2
15
 
3
16
  export type SqlPrimitive = string | number | bigint | boolean | null
4
17
 
@@ -0,0 +1,17 @@
1
+
2
+ /**
3
+ * @import {AggregateFunc, StringFunc} from './types.js'
4
+ * @param {string} name
5
+ * @returns {name is AggregateFunc}
6
+ */
7
+ export function isAggregateFunc(name) {
8
+ return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(name)
9
+ }
10
+
11
+ /**
12
+ * @param {string} name
13
+ * @returns {name is StringFunc}
14
+ */
15
+ export function isStringFunc(name) {
16
+ return ['UPPER', 'LOWER', 'CONCAT', 'LENGTH', 'SUBSTRING', 'TRIM'].includes(name)
17
+ }