squirreling 0.4.8 → 0.6.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
@@ -40,7 +40,10 @@ const users = [
40
40
  // ...more rows
41
41
  ]
42
42
 
43
- type AsyncRow = Record<string, AsyncCell>
43
+ interface AsyncRow {
44
+ columns: string[]
45
+ cells: Record<string, AsyncCell>
46
+ }
44
47
  type AsyncCell = () => Promise<SqlPrimitive>
45
48
 
46
49
  // Returns an async iterable of rows with async cells
@@ -76,6 +79,7 @@ console.log(allUsers)
76
79
  - `GROUP BY` and `HAVING` clauses
77
80
  - Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
78
81
  - String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
82
+ - Math functions: `ABS`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`, `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
79
83
  - Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
80
84
  - Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
81
85
  - Basic expressions and arithmetic operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.8",
3
+ "version": "0.6.0",
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": "24.10.1",
40
+ "@types/node": "24.10.2",
41
41
  "@vitest/coverage-v8": "4.0.15",
42
42
  "eslint": "9.39.1",
43
- "eslint-plugin-jsdoc": "61.4.2",
43
+ "eslint-plugin-jsdoc": "61.5.0",
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "4.0.15"
46
46
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
2
+ * @import { AsyncCell, AsyncCells, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
 
@@ -11,7 +11,7 @@
11
11
  */
12
12
  export function generatorSource(gen) {
13
13
  return {
14
- async *getRows() {
14
+ async *scan() {
15
15
  yield* gen
16
16
  },
17
17
  }
@@ -24,12 +24,12 @@ export function generatorSource(gen) {
24
24
  * @returns {AsyncRow} a row accessor interface
25
25
  */
26
26
  function asyncRow(obj) {
27
- /** @type {AsyncRow} */
28
- const row = {}
27
+ /** @type {AsyncCells} */
28
+ const cells = {}
29
29
  for (const [key, value] of Object.entries(obj)) {
30
- row[key] = () => Promise.resolve(value)
30
+ cells[key] = () => Promise.resolve(value)
31
31
  }
32
- return row
32
+ return { columns: Object.keys(obj), cells }
33
33
  }
34
34
 
35
35
  /**
@@ -40,7 +40,7 @@ function asyncRow(obj) {
40
40
  */
41
41
  export function memorySource(data) {
42
42
  return {
43
- async *getRows() {
43
+ async *scan() {
44
44
  for (const item of data) {
45
45
  yield asyncRow(item)
46
46
  }
@@ -60,15 +60,16 @@ export function cachedDataSource(source) {
60
60
  /**
61
61
  * @yields {AsyncRow}
62
62
  */
63
- async *getRows() {
63
+ async *scan() {
64
64
  let index = 0
65
- for await (const row of source.getRows()) {
65
+ for await (const row of source.scan()) {
66
66
  const rowIndex = index
67
- /** @type {AsyncRow} */
68
- const out = {}
69
- for (const [key, cell] of Object.entries(row)) {
67
+ /** @type {AsyncCells} */
68
+ const cells = {}
69
+ for (const key of row.columns) {
70
+ const cell = row.cells[key]
70
71
  // Wrap the cell to cache accesses
71
- out[key] = () => {
72
+ cells[key] = () => {
72
73
  const cacheKey = `${rowIndex}:${key}`
73
74
  let value = cache.get(cacheKey)
74
75
  if (!value) {
@@ -78,7 +79,7 @@ export function cachedDataSource(source) {
78
79
  return value
79
80
  }
80
81
  }
81
- yield out
82
+ yield { columns: row.columns, cells }
82
83
  index++
83
84
  }
84
85
  },
@@ -1,4 +1,5 @@
1
- import { aggregateError, unknownFunctionError } from '../errors.js'
1
+ import { unknownFunctionError } from '../parseErrors.js'
2
+ import { aggregateError } from '../validationErrors.js'
2
3
  import { evaluateExpr } from './expression.js'
3
4
  import { defaultDerivedAlias, stringify } from './utils.js'
4
5
 
@@ -39,7 +40,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
39
40
 
40
41
  if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
41
42
  if (arg.kind === 'star') {
42
- throw aggregateError(func, '(*) is not supported, use a column name')
43
+ throw aggregateError({ funcName: func, issue: '(*) is not supported, use a column name' })
43
44
  }
44
45
  let sum = 0
45
46
  let count = 0
@@ -73,7 +74,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
73
74
 
74
75
  if (func === 'JSON_ARRAYAGG') {
75
76
  if (arg.kind === 'star') {
76
- throw aggregateError('JSON_ARRAYAGG', '(*) is not supported, use a column name or expression')
77
+ throw aggregateError({ funcName: 'JSON_ARRAYAGG', issue: '(*) is not supported, use a column name or expression' })
77
78
  }
78
79
  /** @type {SqlPrimitive[]} */
79
80
  const values = []
@@ -96,7 +97,12 @@ export async function evaluateAggregate({ col, rows, tables }) {
96
97
  return values
97
98
  }
98
99
 
99
- throw unknownFunctionError(func, undefined, 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG')
100
+ throw unknownFunctionError({
101
+ funcName: func,
102
+ positionStart: 0,
103
+ positionEnd: 0,
104
+ validFunctions: 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG',
105
+ })
100
106
  }
101
107
 
102
108
  /**
@@ -1,4 +1,5 @@
1
- import { missingClauseError, tableNotFoundError, unsupportedOperationError } from '../errors.js'
1
+ import { missingClauseError } from '../parseErrors.js'
2
+ import { tableNotFoundError, unsupportedOperationError } from '../executionErrors.js'
2
3
  import { generatorSource, memorySource } from '../backend/dataSource.js'
3
4
  import { parseSql } from '../parse/parse.js'
4
5
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
@@ -9,7 +10,7 @@ import { executeJoins } from './join.js'
9
10
  import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
10
11
 
11
12
  /**
12
- * @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
13
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
13
14
  */
14
15
 
15
16
  /**
@@ -19,7 +20,7 @@ import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
19
20
  * @yields {AsyncRow} async generator yielding result rows
20
21
  */
21
22
  export async function* executeSql({ tables, query }) {
22
- const select = parseSql(query)
23
+ const select = typeof query === 'string' ? parseSql(query) : query
23
24
 
24
25
  // Check for unsupported operations
25
26
  if (!select.from) {
@@ -61,7 +62,7 @@ export async function* executeSelect(select, tables) {
61
62
  fromTableName = select.from.alias ?? select.from.table
62
63
  dataSource = tables[select.from.table]
63
64
  if (dataSource === undefined) {
64
- throw tableNotFoundError(select.from.table)
65
+ throw tableNotFoundError({ tableName: select.from.table })
65
66
  }
66
67
  } else {
67
68
  // Nested subquery - recursively resolve
@@ -80,15 +81,15 @@ export async function* executeSelect(select, tables) {
80
81
  /**
81
82
  * Creates a stable string key for a row to enable deduplication
82
83
  *
83
- * @param {AsyncRow} row
84
+ * @param {AsyncCells} cells
84
85
  * @returns {Promise<string>} a stable string representation of the row
85
86
  */
86
- async function stableRowKey(row) {
87
- const keys = Object.keys(row).sort()
87
+ async function stableRowKey(cells) {
88
+ const keys = Object.keys(cells).sort()
88
89
  /** @type {string[]} */
89
90
  const parts = []
90
91
  for (const k of keys) {
91
- const v = await row[k]()
92
+ const v = await cells[k]()
92
93
  parts.push(k + ':' + stringify(v))
93
94
  }
94
95
  return parts.join('|')
@@ -108,7 +109,7 @@ async function applyDistinct(rows, distinct) {
108
109
  /** @type {AsyncRow[]} */
109
110
  const result = []
110
111
  for (const row of rows) {
111
- const key = await stableRowKey(row)
112
+ const key = await stableRowKey(row.cells)
112
113
  if (seen.has(key)) continue
113
114
  seen.add(key)
114
115
  result.push(row)
@@ -236,6 +237,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
236
237
  async function* evaluateStreaming(select, dataSource, tables) {
237
238
  let rowsYielded = 0
238
239
  let rowsSkipped = 0
240
+ let rowIndex = 0
239
241
  const offset = select.offset ?? 0
240
242
  const limit = select.limit ?? Infinity
241
243
  if (limit <= 0) return
@@ -253,10 +255,11 @@ async function* evaluateStreaming(select, dataSource, tables) {
253
255
  offset: select.offset,
254
256
  }
255
257
 
256
- for await (const row of dataSource.getRows(hints)) {
258
+ for await (const row of dataSource.scan(hints)) {
259
+ rowIndex++
257
260
  // WHERE filter
258
261
  if (select.where) {
259
- const pass = await evaluateExpr({ node: select.where, row, tables })
262
+ const pass = await evaluateExpr({ node: select.where, row, tables, rowIndex })
260
263
  if (!pass) continue
261
264
  }
262
265
 
@@ -267,16 +270,21 @@ async function* evaluateStreaming(select, dataSource, tables) {
267
270
  }
268
271
 
269
272
  // SELECT projection
270
- /** @type {AsyncRow} */
271
- const outRow = {}
273
+ /** @type {string[]} */
274
+ const columns = []
275
+ /** @type {AsyncCells} */
276
+ const cells = {}
277
+ const currentRowIndex = rowIndex
272
278
  for (const col of select.columns) {
273
279
  if (col.kind === 'star') {
274
- for (const [key, cell] of Object.entries(row)) {
275
- outRow[key] = cell
280
+ for (const key of row.columns) {
281
+ columns.push(key)
282
+ cells[key] = row.cells[key]
276
283
  }
277
284
  } else if (col.kind === 'derived') {
278
285
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
279
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
286
+ columns.push(alias)
287
+ cells[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
280
288
  } else if (col.kind === 'aggregate') {
281
289
  throw new Error(
282
290
  'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
@@ -286,7 +294,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
286
294
 
287
295
  // DISTINCT: skip duplicate rows
288
296
  if (seen) {
289
- const key = await stableRowKey(outRow)
297
+ const key = await stableRowKey(cells)
290
298
  if (seen.has(key)) continue
291
299
  seen.add(key)
292
300
  // OFFSET applies to distinct rows
@@ -296,7 +304,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
296
304
  }
297
305
  }
298
306
 
299
- yield outRow
307
+ yield { columns, cells }
300
308
  rowsYielded++
301
309
  if (rowsYielded >= limit) {
302
310
  break
@@ -326,7 +334,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
326
334
  // Step 1: Collect all rows from data source
327
335
  /** @type {AsyncRow[]} */
328
336
  const working = []
329
- for await (const row of dataSource.getRows(hints)) {
337
+ for await (const row of dataSource.scan(hints)) {
330
338
  working.push(row)
331
339
  }
332
340
 
@@ -334,9 +342,11 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
334
342
  /** @type {AsyncRow[]} */
335
343
  const filtered = []
336
344
 
337
- for (const row of working) {
345
+ for (let i = 0; i < working.length; i++) {
346
+ const row = working[i]
347
+ const rowIndex = i + 1 // 1-based
338
348
  if (select.where) {
339
- const passes = await evaluateExpr({ node: select.where, row, tables })
349
+ const passes = await evaluateExpr({ node: select.where, row, tables, rowIndex })
340
350
 
341
351
  if (!passes) {
342
352
  continue
@@ -379,21 +389,23 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
379
389
 
380
390
  const hasStar = select.columns.some(col => col.kind === 'star')
381
391
  if (hasStar && hasAggregate) {
382
- throw unsupportedOperationError(
383
- 'SELECT * with aggregate functions is not supported',
384
- 'Replace * with specific column names when using aggregate functions.'
385
- )
392
+ throw unsupportedOperationError({
393
+ operation: 'SELECT * with aggregate functions is not supported',
394
+ hint: 'Replace * with specific column names when using aggregate functions.',
395
+ })
386
396
  }
387
397
 
388
398
  for (const group of groups) {
389
- /** @type {AsyncRow} */
390
- const resultRow = {}
399
+ const columns = []
400
+ /** @type {AsyncCells} */
401
+ const cells = {}
391
402
  for (const col of select.columns) {
392
403
  if (col.kind === 'star') {
393
404
  const firstRow = group[0]
394
405
  if (firstRow) {
395
- for (const [key, cell] of Object.entries(firstRow)) {
396
- resultRow[key] = cell
406
+ for (const key of firstRow.columns) {
407
+ columns.push(key)
408
+ cells[key] = firstRow.cells[key]
397
409
  }
398
410
  }
399
411
  continue
@@ -401,29 +413,32 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
401
413
 
402
414
  if (col.kind === 'derived') {
403
415
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
416
+ columns.push(alias)
404
417
  if (group.length > 0) {
405
- resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
418
+ cells[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
406
419
  } else {
407
- delete resultRow[alias]
420
+ delete cells[alias]
408
421
  }
409
422
  continue
410
423
  }
411
424
 
412
425
  if (col.kind === 'aggregate') {
413
426
  const alias = col.alias ?? defaultAggregateAlias(col)
414
- resultRow[alias] = () => evaluateAggregate({ col, rows: group, tables })
427
+ columns.push(alias)
428
+ cells[alias] = () => evaluateAggregate({ col, rows: group, tables })
415
429
  continue
416
430
  }
417
431
  }
432
+ const asyncRow = { columns, cells }
418
433
 
419
434
  // Apply HAVING filter before adding to projected results
420
435
  if (select.having) {
421
- if (!await evaluateHavingExpr(select.having, resultRow, group, tables)) {
436
+ if (!await evaluateHavingExpr(select.having, asyncRow, group, tables)) {
422
437
  continue
423
438
  }
424
439
  }
425
440
 
426
- projected.push(resultRow)
441
+ projected.push(asyncRow)
427
442
  }
428
443
  } else {
429
444
  // No grouping, simple projection
@@ -440,19 +455,22 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
440
455
  }
441
456
 
442
457
  for (const row of rowsToProject) {
443
- /** @type {AsyncRow} */
444
- const outRow = {}
458
+ const columns = []
459
+ /** @type {AsyncCells} */
460
+ const cells = {}
445
461
  for (const col of select.columns) {
446
462
  if (col.kind === 'star') {
447
- for (const [key, cell] of Object.entries(row)) {
448
- outRow[key] = cell
463
+ for (const key of row.columns) {
464
+ columns.push(key)
465
+ cells[key] = row.cells[key]
449
466
  }
450
467
  } else if (col.kind === 'derived') {
451
468
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
452
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
469
+ columns.push(alias)
470
+ cells[alias] = () => evaluateExpr({ node: col.expr, row, tables })
453
471
  }
454
472
  }
455
- projected.push(outRow)
473
+ projected.push({ columns, cells })
456
474
  }
457
475
  }
458
476