squirreling 0.7.4 → 0.7.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -37,10 +37,10 @@
37
37
  "test": "vitest run"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/node": "25.0.3",
40
+ "@types/node": "25.0.6",
41
41
  "@vitest/coverage-v8": "4.0.16",
42
42
  "eslint": "9.39.2",
43
- "eslint-plugin-jsdoc": "61.5.0",
43
+ "eslint-plugin-jsdoc": "62.0.0",
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "4.0.16"
46
46
  }
@@ -10,7 +10,7 @@ import { resolveTableSource } from './tableSource.js'
10
10
  import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
11
11
 
12
12
  /**
13
- * @import { AsyncCells, AsyncDataSource, AsyncRow, CTEDefinition, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive, UserDefinedFunction, WithClause } from '../types.js'
13
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, ExprNode, OrderByItem, QueryHints, SelectColumn, SelectStatement, SqlPrimitive, UserDefinedFunction, WithClause } from '../types.js'
14
14
  */
15
15
 
16
16
  /**
@@ -143,9 +143,10 @@ async function applyDistinct(rows, distinct) {
143
143
  * @param {OrderByItem[]} options.orderBy - the sort specifications
144
144
  * @param {Record<string, AsyncDataSource>} options.tables
145
145
  * @param {Record<string, UserDefinedFunction>} [options.functions]
146
+ * @param {Map<string, ExprNode>} [options.aliases] - SELECT column aliases for ORDER BY resolution
146
147
  * @returns {Promise<AsyncRow[]>} the sorted rows
147
148
  */
148
- async function sortRows({ rows, orderBy, tables, functions }) {
149
+ async function sortRows({ rows, orderBy, tables, functions, aliases }) {
149
150
  if (!orderBy.length) return rows
150
151
 
151
152
  // Cache for evaluated values: evaluatedValues[rowIdx][colIdx]
@@ -177,6 +178,7 @@ async function sortRows({ rows, orderBy, tables, functions }) {
177
178
  row: rows[idx],
178
179
  tables,
179
180
  functions,
181
+ aliases,
180
182
  })
181
183
  }
182
184
  }
@@ -459,7 +461,16 @@ async function* evaluateBuffered({ select, dataSource, tables, functions, hasAgg
459
461
  } else {
460
462
  // No grouping, simple projection
461
463
  // Sort before projection so ORDER BY can access columns not in SELECT
462
- const sorted = await sortRows({ rows: filtered, orderBy: select.orderBy, tables, functions })
464
+
465
+ // Pass aliases so ORDER BY can reference SELECT column aliases
466
+ /** @type {Map<string, ExprNode>} */
467
+ const aliases = new Map()
468
+ for (const col of select.columns) {
469
+ if (col.kind === 'derived' && col.alias) {
470
+ aliases.set(col.alias, col.expr)
471
+ }
472
+ }
473
+ const sorted = await sortRows({ rows: filtered, orderBy: select.orderBy, tables, functions, aliases })
463
474
 
464
475
  // OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
465
476
  // to avoid reading expensive cells for rows that won't be in the final result
@@ -1,5 +1,5 @@
1
1
  import { unknownFunctionError } from '../parseErrors.js'
2
- import { invalidContextError } from '../executionErrors.js'
2
+ import { columnNotFoundError, invalidContextError } from '../executionErrors.js'
3
3
  import { aggregateError, argValueError, castError } from '../validationErrors.js'
4
4
  import { isAggregateFunc, isMathFunc, isRegexpFunc, isStringFunc } from '../validation.js'
5
5
  import { applyIntervalToDate } from './date.js'
@@ -23,26 +23,38 @@ import { applyBinaryOp, stringify } from './utils.js'
23
23
  * @param {Record<string, UserDefinedFunction>} [params.functions] - User-defined functions
24
24
  * @param {number} [params.rowIndex] - 1-based row index for error reporting
25
25
  * @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
26
+ * @param {Map<string, ExprNode>} [params.aliases] - SELECT column aliases for ORDER BY resolution
26
27
  * @returns {Promise<SqlPrimitive>} The result of the evaluation
27
28
  */
28
- export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows }) {
29
+ export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases }) {
29
30
  if (node.type === 'literal') {
30
31
  return node.value
31
32
  }
32
33
 
33
34
  if (node.type === 'identifier') {
34
35
  // Try exact match first (handles both qualified and unqualified names)
35
- if (row.cells[node.name]) {
36
+ if (node.name in row.cells) {
36
37
  return row.cells[node.name]()
37
38
  }
38
39
  // For qualified names like 'users.id', also try just the column part
39
40
  if (node.name.includes('.')) {
40
41
  const colName = node.name.split('.').pop()
41
- if (colName && row.cells[colName]) {
42
+ if (colName && colName in row.cells) {
42
43
  return row.cells[colName]()
43
44
  }
44
45
  }
45
- return null
46
+ // Check if it's a SELECT alias (for ORDER BY)
47
+ if (aliases?.has(node.name)) {
48
+ return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases })
49
+ }
50
+ // Unknown identifier
51
+ throw columnNotFoundError({
52
+ columnName: node.name,
53
+ availableColumns: Object.keys(row.cells),
54
+ positionStart: node.positionStart,
55
+ positionEnd: node.positionEnd,
56
+ rowNumber: rowIndex,
57
+ })
46
58
  }
47
59
 
48
60
  // Scalar subquery - returns a single value
@@ -57,16 +69,16 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
57
69
  // Unary operators
58
70
  if (node.type === 'unary') {
59
71
  if (node.op === 'NOT') {
60
- return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
72
+ return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
61
73
  }
62
74
  if (node.op === 'IS NULL') {
63
- return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) == null
75
+ return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) == null
64
76
  }
65
77
  if (node.op === 'IS NOT NULL') {
66
- return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) != null
78
+ return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) != null
67
79
  }
68
80
  if (node.op === '-') {
69
- const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
81
+ const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
70
82
  if (val == null) return null
71
83
  return -val
72
84
  }
@@ -76,15 +88,15 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
76
88
  if (node.type === 'binary') {
77
89
  // Handle date +/- interval at AST level
78
90
  if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
79
- const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
91
+ const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
80
92
  return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
81
93
  }
82
94
  if (node.op === '+' && node.left.type === 'interval') {
83
- const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
95
+ const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
84
96
  return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
85
97
  }
86
98
 
87
- const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
99
+ const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
88
100
 
89
101
  // Short-circuit evaluation for AND and OR
90
102
  if (node.op === 'AND') {
@@ -94,7 +106,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
94
106
  if (left) return true
95
107
  }
96
108
 
97
- const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
109
+ const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
98
110
  return applyBinaryOp(node.op, left, right)
99
111
  }
100
112
 
@@ -196,7 +208,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
196
208
  }
197
209
 
198
210
  /** @type {SqlPrimitive[]} */
199
- const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows })))
211
+ const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases })))
200
212
 
201
213
  if (isStringFunc(funcName)) {
202
214
  return evaluateStringFunc({
@@ -20,7 +20,7 @@ import { applyBinaryOp } from './utils.js'
20
20
  */
21
21
  export async function evaluateHavingExpr({ expr, row, group, tables, functions }) {
22
22
  // Having context
23
- const context = { ...group[0] ?? {}, ...row }
23
+ const context = { ...group[0], ...row }
24
24
 
25
25
  // For HAVING, we need special handling of aggregate functions
26
26
  // They need to be re-evaluated against the group
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import {AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
2
+ * @import { AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  /**
@@ -24,8 +24,6 @@ export class ExecutionError extends Error {
24
24
  }
25
25
 
26
26
  /**
27
- * Error for missing table.
28
- *
29
27
  * @param {Object} options
30
28
  * @param {string} options.tableName - The missing table name
31
29
  * @returns {Error}
@@ -50,8 +48,6 @@ export function invalidContextError({ item, validContext, positionStart, positio
50
48
  }
51
49
 
52
50
  /**
53
- * Error for unsupported operation combinations.
54
- *
55
51
  * @param {Object} options
56
52
  * @param {string} options.operation - The unsupported operation
57
53
  * @param {string} [options.hint] - How to fix it
@@ -61,3 +57,24 @@ export function unsupportedOperationError({ operation, hint }) {
61
57
  const suffix = hint ? `. ${hint}` : ''
62
58
  return new Error(`${operation}${suffix}`)
63
59
  }
60
+
61
+ /**
62
+ * @param {Object} options
63
+ * @param {string} options.columnName - The missing column name
64
+ * @param {string[]} options.availableColumns - List of available column names
65
+ * @param {number} options.positionStart - Start position in query
66
+ * @param {number} options.positionEnd - End position in query
67
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
68
+ * @returns {ExecutionError}
69
+ */
70
+ export function columnNotFoundError({ columnName, availableColumns, positionStart, positionEnd, rowNumber }) {
71
+ const available = availableColumns.length > 0
72
+ ? `. Available columns: ${availableColumns.join(', ')}`
73
+ : ''
74
+ return new ExecutionError({
75
+ message: `Column "${columnName}" not found${available}`,
76
+ positionStart,
77
+ positionEnd,
78
+ rowNumber,
79
+ })
80
+ }
@@ -97,29 +97,32 @@ export function parseSql({ query, functions }) {
97
97
  function parseSelectList(state) {
98
98
  /** @type {SelectColumn[]} */
99
99
  const cols = []
100
- const tok = current(state)
101
100
 
102
- // Check for qualified asterisk (table.*)
103
- if (tok.type === 'identifier') {
104
- const next = peekToken(state, 1)
105
- const nextNext = peekToken(state, 2)
106
- if (next.type === 'dot' && nextNext.type === 'operator' && nextNext.value === '*') {
107
- const tableTok = consume(state) // consume table name
108
- consume(state) // consume dot
109
- consume(state) // consume asterisk
110
- cols.push({ kind: 'star', table: tableTok.value })
111
- return cols
101
+ while (true) {
102
+ const tok = current(state)
103
+
104
+ // Check for qualified asterisk (table.*)
105
+ if (tok.type === 'identifier') {
106
+ const next = peekToken(state, 1)
107
+ const nextNext = peekToken(state, 2)
108
+ if (next.type === 'dot' && nextNext.type === 'operator' && nextNext.value === '*') {
109
+ const tableTok = consume(state) // consume table name
110
+ consume(state) // consume dot
111
+ consume(state) // consume asterisk
112
+ cols.push({ kind: 'star', table: tableTok.value })
113
+ if (!match(state, 'comma')) break
114
+ continue
115
+ }
112
116
  }
113
- }
114
117
 
115
- // Check for unqualified asterisk (*)
116
- if (tok.type === 'operator' && tok.value === '*') {
117
- consume(state)
118
- cols.push({ kind: 'star' })
119
- return cols
120
- }
118
+ // Check for unqualified asterisk (*)
119
+ if (tok.type === 'operator' && tok.value === '*') {
120
+ consume(state)
121
+ cols.push({ kind: 'star' })
122
+ if (!match(state, 'comma')) break
123
+ continue
124
+ }
121
125
 
122
- while (true) {
123
126
  cols.push(parseSelectItem(state))
124
127
  if (!match(state, 'comma')) break
125
128
  }
package/src/types.d.ts CHANGED
@@ -38,7 +38,7 @@ export interface ScanOptions {
38
38
  * All hints are optional and "best effort" - sources may ignore them.
39
39
  */
40
40
  export interface QueryHints {
41
- columns?: string[] // columns needed
41
+ columns?: string[] // columns needed (undefined means all columns)
42
42
  where?: ExprNode // where clause
43
43
  // important: only apply limit/offset if where is fully applied by the data source
44
44
  // otherwise, the data source must return at least enough rows to ensure the engine
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction} from './types.js'
3
+ * @import { AggregateFunc, BinaryOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction } from './types.js'
4
4
  * @param {string} name
5
5
  * @returns {name is AggregateFunc}
6
6
  */