squirreling 0.5.0 → 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.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -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
  },
@@ -10,7 +10,7 @@ import { executeJoins } from './join.js'
10
10
  import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
11
11
 
12
12
  /**
13
- * @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
13
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
14
14
  */
15
15
 
16
16
  /**
@@ -20,7 +20,7 @@ import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
20
20
  * @yields {AsyncRow} async generator yielding result rows
21
21
  */
22
22
  export async function* executeSql({ tables, query }) {
23
- const select = parseSql(query)
23
+ const select = typeof query === 'string' ? parseSql(query) : query
24
24
 
25
25
  // Check for unsupported operations
26
26
  if (!select.from) {
@@ -81,15 +81,15 @@ export async function* executeSelect(select, tables) {
81
81
  /**
82
82
  * Creates a stable string key for a row to enable deduplication
83
83
  *
84
- * @param {AsyncRow} row
84
+ * @param {AsyncCells} cells
85
85
  * @returns {Promise<string>} a stable string representation of the row
86
86
  */
87
- async function stableRowKey(row) {
88
- const keys = Object.keys(row).sort()
87
+ async function stableRowKey(cells) {
88
+ const keys = Object.keys(cells).sort()
89
89
  /** @type {string[]} */
90
90
  const parts = []
91
91
  for (const k of keys) {
92
- const v = await row[k]()
92
+ const v = await cells[k]()
93
93
  parts.push(k + ':' + stringify(v))
94
94
  }
95
95
  return parts.join('|')
@@ -109,7 +109,7 @@ async function applyDistinct(rows, distinct) {
109
109
  /** @type {AsyncRow[]} */
110
110
  const result = []
111
111
  for (const row of rows) {
112
- const key = await stableRowKey(row)
112
+ const key = await stableRowKey(row.cells)
113
113
  if (seen.has(key)) continue
114
114
  seen.add(key)
115
115
  result.push(row)
@@ -255,7 +255,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
255
255
  offset: select.offset,
256
256
  }
257
257
 
258
- for await (const row of dataSource.getRows(hints)) {
258
+ for await (const row of dataSource.scan(hints)) {
259
259
  rowIndex++
260
260
  // WHERE filter
261
261
  if (select.where) {
@@ -270,17 +270,21 @@ async function* evaluateStreaming(select, dataSource, tables) {
270
270
  }
271
271
 
272
272
  // SELECT projection
273
- /** @type {AsyncRow} */
274
- const outRow = {}
273
+ /** @type {string[]} */
274
+ const columns = []
275
+ /** @type {AsyncCells} */
276
+ const cells = {}
275
277
  const currentRowIndex = rowIndex
276
278
  for (const col of select.columns) {
277
279
  if (col.kind === 'star') {
278
- for (const [key, cell] of Object.entries(row)) {
279
- outRow[key] = cell
280
+ for (const key of row.columns) {
281
+ columns.push(key)
282
+ cells[key] = row.cells[key]
280
283
  }
281
284
  } else if (col.kind === 'derived') {
282
285
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
283
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
286
+ columns.push(alias)
287
+ cells[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
284
288
  } else if (col.kind === 'aggregate') {
285
289
  throw new Error(
286
290
  'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
@@ -290,7 +294,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
290
294
 
291
295
  // DISTINCT: skip duplicate rows
292
296
  if (seen) {
293
- const key = await stableRowKey(outRow)
297
+ const key = await stableRowKey(cells)
294
298
  if (seen.has(key)) continue
295
299
  seen.add(key)
296
300
  // OFFSET applies to distinct rows
@@ -300,7 +304,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
300
304
  }
301
305
  }
302
306
 
303
- yield outRow
307
+ yield { columns, cells }
304
308
  rowsYielded++
305
309
  if (rowsYielded >= limit) {
306
310
  break
@@ -330,7 +334,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
330
334
  // Step 1: Collect all rows from data source
331
335
  /** @type {AsyncRow[]} */
332
336
  const working = []
333
- for await (const row of dataSource.getRows(hints)) {
337
+ for await (const row of dataSource.scan(hints)) {
334
338
  working.push(row)
335
339
  }
336
340
 
@@ -392,14 +396,16 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
392
396
  }
393
397
 
394
398
  for (const group of groups) {
395
- /** @type {AsyncRow} */
396
- const resultRow = {}
399
+ const columns = []
400
+ /** @type {AsyncCells} */
401
+ const cells = {}
397
402
  for (const col of select.columns) {
398
403
  if (col.kind === 'star') {
399
404
  const firstRow = group[0]
400
405
  if (firstRow) {
401
- for (const [key, cell] of Object.entries(firstRow)) {
402
- resultRow[key] = cell
406
+ for (const key of firstRow.columns) {
407
+ columns.push(key)
408
+ cells[key] = firstRow.cells[key]
403
409
  }
404
410
  }
405
411
  continue
@@ -407,29 +413,32 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
407
413
 
408
414
  if (col.kind === 'derived') {
409
415
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
416
+ columns.push(alias)
410
417
  if (group.length > 0) {
411
- resultRow[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
418
+ cells[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
412
419
  } else {
413
- delete resultRow[alias]
420
+ delete cells[alias]
414
421
  }
415
422
  continue
416
423
  }
417
424
 
418
425
  if (col.kind === 'aggregate') {
419
426
  const alias = col.alias ?? defaultAggregateAlias(col)
420
- resultRow[alias] = () => evaluateAggregate({ col, rows: group, tables })
427
+ columns.push(alias)
428
+ cells[alias] = () => evaluateAggregate({ col, rows: group, tables })
421
429
  continue
422
430
  }
423
431
  }
432
+ const asyncRow = { columns, cells }
424
433
 
425
434
  // Apply HAVING filter before adding to projected results
426
435
  if (select.having) {
427
- if (!await evaluateHavingExpr(select.having, resultRow, group, tables)) {
436
+ if (!await evaluateHavingExpr(select.having, asyncRow, group, tables)) {
428
437
  continue
429
438
  }
430
439
  }
431
440
 
432
- projected.push(resultRow)
441
+ projected.push(asyncRow)
433
442
  }
434
443
  } else {
435
444
  // No grouping, simple projection
@@ -446,19 +455,22 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
446
455
  }
447
456
 
448
457
  for (const row of rowsToProject) {
449
- /** @type {AsyncRow} */
450
- const outRow = {}
458
+ const columns = []
459
+ /** @type {AsyncCells} */
460
+ const cells = {}
451
461
  for (const col of select.columns) {
452
462
  if (col.kind === 'star') {
453
- for (const [key, cell] of Object.entries(row)) {
454
- outRow[key] = cell
463
+ for (const key of row.columns) {
464
+ columns.push(key)
465
+ cells[key] = row.cells[key]
455
466
  }
456
467
  } else if (col.kind === 'derived') {
457
468
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
458
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
469
+ columns.push(alias)
470
+ cells[alias] = () => evaluateExpr({ node: col.expr, row, tables })
459
471
  }
460
472
  }
461
- projected.push(outRow)
473
+ projected.push({ columns, cells })
462
474
  }
463
475
  }
464
476
 
@@ -32,14 +32,14 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
32
32
 
33
33
  if (node.type === 'identifier') {
34
34
  // Try exact match first (handles both qualified and unqualified names)
35
- if (row[node.name]) {
36
- return row[node.name]()
35
+ if (row.cells[node.name]) {
36
+ return row.cells[node.name]()
37
37
  }
38
38
  // For qualified names like 'users.id', also try just the column part
39
39
  if (node.name.includes('.')) {
40
40
  const colName = node.name.split('.').pop()
41
- if (colName && row[colName]) {
42
- return row[colName]()
41
+ if (colName && row.cells[colName]) {
42
+ return row.cells[colName]()
43
43
  }
44
44
  }
45
45
  return null
@@ -48,13 +48,10 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
48
48
  // Scalar subquery - returns a single value
49
49
  if (node.type === 'subquery') {
50
50
  const gen = executeSelect(node.subquery, tables)
51
- const first = await gen.next() // Start the generator
51
+ const { value } = await gen.next() // Start the generator
52
52
  gen.return(undefined) // Stop further execution
53
- if (!first.value) return null
54
- /** @type {AsyncRow} */
55
- const firstRow = first.value
56
- const firstKey = Object.keys(firstRow)[0]
57
- return firstRow[firstKey]()
53
+ if (!value) return null
54
+ return value.cells[value.columns[0]]()
58
55
  }
59
56
 
60
57
  // Unary operators
@@ -484,14 +481,11 @@ export async function evaluateExpr({ node, row, tables, rowIndex }) {
484
481
  if (node.type === 'in') {
485
482
  const exprVal = await evaluateExpr({ node: node.expr, row, tables, rowIndex })
486
483
  const results = executeSelect(node.subquery, tables)
487
- /** @type {SqlPrimitive[]} */
488
- const values = []
489
484
  for await (const resRow of results) {
490
- const firstKey = Object.keys(resRow)[0]
491
- const val = await resRow[firstKey]()
492
- values.push(val)
485
+ const value = await resRow.cells[resRow.columns[0]]()
486
+ if (exprVal === value) return true
493
487
  }
494
- return values.includes(exprVal)
488
+ return false
495
489
  }
496
490
 
497
491
  // EXISTS and NOT EXISTS with subqueries
@@ -4,7 +4,7 @@ import { evaluateExpr } from './expression.js'
4
4
  import { stringify } from './utils.js'
5
5
 
6
6
  /**
7
- * @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode } from '../types.js'
7
+ * @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode, AsyncCells } from '../types.js'
8
8
  */
9
9
 
10
10
  /**
@@ -30,7 +30,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
30
30
  // Buffer right rows for hash index (required for hash join)
31
31
  /** @type {AsyncRow[]} */
32
32
  const rightRows = []
33
- for await (const row of rightSource.getRows()) {
33
+ for await (const row of rightSource.scan()) {
34
34
  rightRows.push(row)
35
35
  }
36
36
 
@@ -39,9 +39,9 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
39
39
 
40
40
  // Return streaming data source - left rows stream through without buffering
41
41
  return {
42
- async *getRows() {
42
+ async *scan() {
43
43
  yield* hashJoin({
44
- leftRows: leftSource.getRows(), // Stream directly, not buffered
44
+ leftRows: leftSource.scan(), // Stream directly, not buffered
45
45
  rightRows,
46
46
  join,
47
47
  leftTable: currentLeftTable,
@@ -55,7 +55,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
55
55
  // Multiple joins: buffer intermediate results, stream final join
56
56
  /** @type {AsyncRow[]} */
57
57
  let leftRows = []
58
- for await (const row of leftSource.getRows()) {
58
+ for await (const row of leftSource.scan()) {
59
59
  leftRows.push(row)
60
60
  }
61
61
 
@@ -69,7 +69,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
69
69
 
70
70
  /** @type {AsyncRow[]} */
71
71
  const rightRows = []
72
- for await (const row of rightSource.getRows()) {
72
+ for await (const row of rightSource.scan()) {
73
73
  rightRows.push(row)
74
74
  }
75
75
 
@@ -105,7 +105,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
105
105
 
106
106
  /** @type {AsyncRow[]} */
107
107
  const rightRows = []
108
- for await (const row of rightSource.getRows()) {
108
+ for await (const row of rightSource.scan()) {
109
109
  rightRows.push(row)
110
110
  }
111
111
 
@@ -113,7 +113,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
113
113
  const lastRightTableName = lastJoin.alias ?? lastJoin.table
114
114
 
115
115
  return {
116
- async *getRows() {
116
+ async *scan() {
117
117
  yield* hashJoin({
118
118
  leftRows,
119
119
  rightRows,
@@ -172,12 +172,12 @@ function extractJoinKeys(onCondition, leftTable, rightTable) {
172
172
  * @returns {AsyncRow}
173
173
  */
174
174
  function createNullRow(columnNames) {
175
- /** @type {AsyncRow} */
176
- const row = {}
175
+ /** @type {AsyncCells} */
176
+ const cells = {}
177
177
  for (const col of columnNames) {
178
- row[col] = () => Promise.resolve(null)
178
+ cells[col] = () => Promise.resolve(null)
179
179
  }
180
- return row
180
+ return { columns: columnNames, cells }
181
181
  }
182
182
 
183
183
  /**
@@ -190,33 +190,35 @@ function createNullRow(columnNames) {
190
190
  * @returns {AsyncRow}
191
191
  */
192
192
  function mergeRows(leftRow, rightRow, leftTable, rightTable) {
193
- /** @type {AsyncRow} */
194
- const merged = {}
193
+ const columns = []
194
+ /** @type {AsyncCells} */
195
+ const cells = {}
195
196
 
196
197
  // Add left table columns with prefix
197
- for (const [key, cell] of Object.entries(leftRow)) {
198
+ for (const [key, cell] of Object.entries(leftRow.cells)) {
198
199
  // Skip already-prefixed keys (from previous joins)
199
200
  if (!key.includes('.')) {
200
- merged[`${leftTable}.${key}`] = cell
201
- } else {
202
- merged[key] = cell
201
+ const alias = `${leftTable}.${key}`
202
+ cells[alias] = cell
203
203
  }
204
- // Also keep unqualified name for convenience (may be overwritten if ambiguous)
205
- merged[key] = cell
204
+ // Also keep unqualified name for convenience
205
+ columns.push(key)
206
+ cells[key] = cell
206
207
  }
207
208
 
208
209
  // Add right table columns with prefix
209
- for (const [key, cell] of Object.entries(rightRow)) {
210
+ for (const [key, cell] of Object.entries(rightRow.cells)) {
210
211
  if (!key.includes('.')) {
211
- merged[`${rightTable}.${key}`] = cell
212
+ cells[`${rightTable}.${key}`] = cell
212
213
  } else {
213
- merged[key] = cell
214
+ cells[key] = cell
214
215
  }
215
216
  // Unqualified name (overwrites if same name exists in left table)
216
- merged[key] = cell
217
+ columns.push(key)
218
+ cells[key] = cell
217
219
  }
218
220
 
219
- return merged
221
+ return { columns, cells }
220
222
  }
221
223
 
222
224
  /**
@@ -245,7 +247,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
245
247
  const keys = extractJoinKeys(onCondition, leftTable, rightTable)
246
248
 
247
249
  // Get column names for NULL row generation (right side is always buffered)
248
- const rightCols = rightRows.length ? Object.keys(rightRows[0]) : []
250
+ const rightCols = rightRows.length ? rightRows[0].columns : []
249
251
  const rightPrefixedCols = rightCols.flatMap(col =>
250
252
  col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
251
253
  )
@@ -281,8 +283,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
281
283
  for await (const leftRow of leftRows) {
282
284
  // Capture left column info from first row (for NULL row generation)
283
285
  if (!leftPrefixedCols) {
284
- const leftCols = Object.keys(leftRow)
285
- leftPrefixedCols = leftCols.flatMap(col =>
286
+ leftPrefixedCols = leftRow.columns.flatMap(col =>
286
287
  col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
287
288
  )
288
289
  }
@@ -323,8 +324,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
323
324
  for await (const leftRow of leftRows) {
324
325
  // Capture left column info from first row (for NULL row generation)
325
326
  if (!leftPrefixedCols) {
326
- const leftCols = Object.keys(leftRow)
327
- leftPrefixedCols = leftCols.flatMap(col =>
327
+ leftPrefixedCols = leftRow.columns.flatMap(col =>
328
328
  col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
329
329
  )
330
330
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import { SqlPrimitive } from '../types.js'
2
+ * @import { MathFunc, SqlPrimitive } from '../types.js'
3
3
  */
4
4
  import { argCountError } from '../validationErrors.js'
5
5
 
@@ -7,7 +7,7 @@ import { argCountError } from '../validationErrors.js'
7
7
  * Evaluate a math function
8
8
  *
9
9
  * @param {Object} options
10
- * @param {string} options.funcName - Uppercase function name
10
+ * @param {MathFunc} options.funcName - Uppercase function name
11
11
  * @param {SqlPrimitive[]} options.args - Function arguments
12
12
  * @param {number} options.positionStart - Start position in query
13
13
  * @param {number} options.positionEnd - End position in query
@@ -161,5 +161,180 @@ export function evaluateMathFunc({ funcName, args, positionStart, positionEnd, r
161
161
  return Math.sqrt(Number(val))
162
162
  }
163
163
 
164
- return undefined
164
+ if (funcName === 'SIN') {
165
+ if (args.length !== 1) {
166
+ throw argCountError({
167
+ funcName: 'SIN',
168
+ expected: 1,
169
+ received: args.length,
170
+ positionStart,
171
+ positionEnd,
172
+ rowNumber,
173
+ })
174
+ }
175
+ const val = args[0]
176
+ if (val == null) return null
177
+ return Math.sin(Number(val))
178
+ }
179
+
180
+ if (funcName === 'COS') {
181
+ if (args.length !== 1) {
182
+ throw argCountError({
183
+ funcName: 'COS',
184
+ expected: 1,
185
+ received: args.length,
186
+ positionStart,
187
+ positionEnd,
188
+ rowNumber,
189
+ })
190
+ }
191
+ const val = args[0]
192
+ if (val == null) return null
193
+ return Math.cos(Number(val))
194
+ }
195
+
196
+ if (funcName === 'TAN') {
197
+ if (args.length !== 1) {
198
+ throw argCountError({
199
+ funcName: 'TAN',
200
+ expected: 1,
201
+ received: args.length,
202
+ positionStart,
203
+ positionEnd,
204
+ rowNumber,
205
+ })
206
+ }
207
+ const val = args[0]
208
+ if (val == null) return null
209
+ return Math.tan(Number(val))
210
+ }
211
+
212
+ if (funcName === 'COT') {
213
+ if (args.length !== 1) {
214
+ throw argCountError({
215
+ funcName: 'COT',
216
+ expected: 1,
217
+ received: args.length,
218
+ positionStart,
219
+ positionEnd,
220
+ rowNumber,
221
+ })
222
+ }
223
+ const val = args[0]
224
+ if (val == null) return null
225
+ return 1 / Math.tan(Number(val))
226
+ }
227
+
228
+ if (funcName === 'ASIN') {
229
+ if (args.length !== 1) {
230
+ throw argCountError({
231
+ funcName: 'ASIN',
232
+ expected: 1,
233
+ received: args.length,
234
+ positionStart,
235
+ positionEnd,
236
+ rowNumber,
237
+ })
238
+ }
239
+ const val = args[0]
240
+ if (val == null) return null
241
+ return Math.asin(Number(val))
242
+ }
243
+
244
+ if (funcName === 'ACOS') {
245
+ if (args.length !== 1) {
246
+ throw argCountError({
247
+ funcName: 'ACOS',
248
+ expected: 1,
249
+ received: args.length,
250
+ positionStart,
251
+ positionEnd,
252
+ rowNumber,
253
+ })
254
+ }
255
+ const val = args[0]
256
+ if (val == null) return null
257
+ return Math.acos(Number(val))
258
+ }
259
+
260
+ if (funcName === 'ATAN') {
261
+ if (args.length !== 1) {
262
+ throw argCountError({
263
+ funcName: 'ATAN',
264
+ expected: 1,
265
+ received: args.length,
266
+ positionStart,
267
+ positionEnd,
268
+ rowNumber,
269
+ })
270
+ }
271
+ const val = args[0]
272
+ if (val == null) return null
273
+ return Math.atan(Number(val))
274
+ }
275
+
276
+ if (funcName === 'ATAN2') {
277
+ if (args.length !== 2) {
278
+ throw argCountError({
279
+ funcName: 'ATAN2',
280
+ expected: 2,
281
+ received: args.length,
282
+ positionStart,
283
+ positionEnd,
284
+ rowNumber,
285
+ })
286
+ }
287
+ const y = args[0]
288
+ const x = args[1]
289
+ if (y == null || x == null) return null
290
+ return Math.atan2(Number(y), Number(x))
291
+ }
292
+
293
+ if (funcName === 'DEGREES') {
294
+ if (args.length !== 1) {
295
+ throw argCountError({
296
+ funcName: 'DEGREES',
297
+ expected: 1,
298
+ received: args.length,
299
+ positionStart,
300
+ positionEnd,
301
+ rowNumber,
302
+ })
303
+ }
304
+ const val = args[0]
305
+ if (val == null) return null
306
+ return Number(val) * 180 / Math.PI
307
+ }
308
+
309
+ if (funcName === 'RADIANS') {
310
+ if (args.length !== 1) {
311
+ throw argCountError({
312
+ funcName: 'RADIANS',
313
+ expected: 1,
314
+ received: args.length,
315
+ positionStart,
316
+ positionEnd,
317
+ rowNumber,
318
+ })
319
+ }
320
+ const val = args[0]
321
+ if (val == null) return null
322
+ return Number(val) * Math.PI / 180
323
+ }
324
+
325
+ if (funcName === 'PI') {
326
+ if (args.length !== 0) {
327
+ throw argCountError({
328
+ funcName: 'PI',
329
+ expected: 0,
330
+ received: args.length,
331
+ positionStart,
332
+ positionEnd,
333
+ rowNumber,
334
+ })
335
+ }
336
+ return Math.PI
337
+ }
338
+
339
+ return null
165
340
  }
@@ -97,8 +97,8 @@ export async function collect(asyncRows) {
97
97
  for await (const asyncRow of asyncRows) {
98
98
  /** @type {Record<string, SqlPrimitive>} */
99
99
  const item = {}
100
- for (const [key, cell] of Object.entries(asyncRow)) {
101
- item[key] = await cell()
100
+ for (const key of asyncRow.columns) {
101
+ item[key] = await asyncRow.cells[key]()
102
102
  }
103
103
  results.push(item)
104
104
  }
@@ -7,12 +7,13 @@
7
7
  */
8
8
  export class ExecutionError extends Error {
9
9
  /**
10
- * @param {string} message - Human-readable error message
11
- * @param {number} positionStart - Start position (0-based character offset)
12
- * @param {number} positionEnd - End position (exclusive, 0-based character offset)
13
- * @param {number} [rowNumber] - 1-based row number where error occurred
10
+ * @param {Object} options
11
+ * @param {string} options.message - Human-readable error message
12
+ * @param {number} options.positionStart - Start position (0-based character offset)
13
+ * @param {number} options.positionEnd - End position (exclusive, 0-based character offset)
14
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
14
15
  */
15
- constructor(message, positionStart, positionEnd, rowNumber) {
16
+ constructor({ message, positionStart, positionEnd, rowNumber }) {
16
17
  const rowSuffix = rowNumber != null ? ` (row ${rowNumber})` : ''
17
18
  super(message + rowSuffix)
18
19
  this.name = 'ExecutionError'
@@ -45,7 +46,7 @@ export function tableNotFoundError({ tableName }) {
45
46
  * @returns {ExecutionError}
46
47
  */
47
48
  export function invalidContextError({ item, validContext, positionStart, positionEnd, rowNumber }) {
48
- return new ExecutionError(`${item} can only be used with ${validContext}`, positionStart, positionEnd, rowNumber)
49
+ return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowNumber })
49
50
  }
50
51
 
51
52
  /**
@@ -7,11 +7,12 @@
7
7
  */
8
8
  export class ParseError extends Error {
9
9
  /**
10
- * @param {string} message - Human-readable error message
11
- * @param {number} positionStart - Start position (0-based character offset)
12
- * @param {number} positionEnd - End position (exclusive, 0-based character offset)
10
+ * @param {Object} options
11
+ * @param {string} options.message - Human-readable error message
12
+ * @param {number} options.positionStart - Start position (0-based character offset)
13
+ * @param {number} options.positionEnd - End position (exclusive, 0-based character offset)
13
14
  */
14
- constructor(message, positionStart, positionEnd) {
15
+ constructor({ message, positionStart, positionEnd }) {
15
16
  super(message)
16
17
  this.name = 'ParseError'
17
18
  this.positionStart = positionStart
@@ -32,7 +33,7 @@ export class ParseError extends Error {
32
33
  */
33
34
  export function syntaxError({ expected, received, positionStart, positionEnd, after }) {
34
35
  const afterClause = after ? ` after "${after}"` : ''
35
- return new ParseError(`Expected ${expected}${afterClause} but found ${received} at position ${positionStart}`, positionStart, positionEnd)
36
+ return new ParseError({ message: `Expected ${expected}${afterClause} but found ${received} at position ${positionStart}`, positionStart, positionEnd })
36
37
  }
37
38
 
38
39
  /**
@@ -45,7 +46,7 @@ export function syntaxError({ expected, received, positionStart, positionEnd, af
45
46
  */
46
47
  export function unterminatedError(type, positionStart, positionEnd) {
47
48
  const name = type === 'string' ? 'string literal' : 'identifier'
48
- return new ParseError(`Unterminated ${name} starting at position ${positionStart}`, positionStart, positionEnd)
49
+ return new ParseError({ message: `Unterminated ${name} starting at position ${positionStart}`, positionStart, positionEnd })
49
50
  }
50
51
 
51
52
  /**
@@ -61,7 +62,7 @@ export function unterminatedError(type, positionStart, positionEnd) {
61
62
  */
62
63
  export function invalidLiteralError({ type, value, positionStart, positionEnd, validValues }) {
63
64
  const suffix = validValues ? `. Valid values: ${validValues}` : ''
64
- return new ParseError(`Invalid ${type} ${value} at position ${positionStart}${suffix}`, positionStart, positionEnd)
65
+ return new ParseError({ message: `Invalid ${type} ${value} at position ${positionStart}${suffix}`, positionStart, positionEnd })
65
66
  }
66
67
 
67
68
  /**
@@ -76,9 +77,9 @@ export function invalidLiteralError({ type, value, positionStart, positionEnd, v
76
77
  export function unexpectedCharError({ char, positionStart, expectsSelect = false }) {
77
78
  const positionEnd = positionStart + 1
78
79
  if (expectsSelect) {
79
- return new ParseError(`Expected SELECT but found "${char}" at position ${positionStart}. Queries must start with SELECT.`, positionStart, positionEnd)
80
+ return new ParseError({ message: `Expected SELECT but found "${char}" at position ${positionStart}. Queries must start with SELECT.`, positionStart, positionEnd })
80
81
  }
81
- return new ParseError(`Unexpected character "${char}" at position ${positionStart}`, positionStart, positionEnd)
82
+ return new ParseError({ message: `Unexpected character "${char}" at position ${positionStart}`, positionStart, positionEnd })
82
83
  }
83
84
 
84
85
  /**
@@ -95,11 +96,11 @@ export function unknownFunctionError({ funcName, positionStart, positionEnd, val
95
96
  const supported = validFunctions ||
96
97
  'COUNT, SUM, AVG, MIN, MAX, UPPER, LOWER, CONCAT, LENGTH, SUBSTRING, TRIM, REPLACE, FLOOR, CEIL, ABS, MOD, EXP, LN, LOG10, POWER, SQRT, JSON_OBJECT, JSON_VALUE, JSON_QUERY, JSON_ARRAYAGG'
97
98
 
98
- return new ParseError(
99
- `Unknown function "${funcName}" at position ${positionStart}. Supported: ${supported}`,
99
+ return new ParseError({
100
+ message: `Unknown function "${funcName}" at position ${positionStart}. Supported: ${supported}`,
100
101
  positionStart,
101
- positionEnd
102
- )
102
+ positionEnd,
103
+ })
103
104
  }
104
105
 
105
106
  /**
@@ -113,5 +114,5 @@ export function unknownFunctionError({ funcName, positionStart, positionEnd, val
113
114
  * @returns {ParseError}
114
115
  */
115
116
  export function missingClauseError({ missing, context, positionStart, positionEnd }) {
116
- return new ParseError(`${context} requires ${missing}`, positionStart ?? 0, positionEnd ?? 0)
117
+ return new ParseError({ message: `${context} requires ${missing}`, positionStart: positionStart ?? 0, positionEnd: positionEnd ?? 0 })
117
118
  }
package/src/types.d.ts CHANGED
@@ -20,16 +20,20 @@ export interface QueryHints {
20
20
  * Provides an async iterator over rows.
21
21
  */
22
22
  export interface AsyncDataSource {
23
- getRows(hints?: QueryHints): AsyncIterable<AsyncRow>
23
+ scan(hints?: QueryHints): AsyncIterable<AsyncRow>
24
24
  }
25
- export type AsyncRow = Record<string, AsyncCell>
25
+ export interface AsyncRow {
26
+ columns: string[]
27
+ cells: AsyncCells
28
+ }
29
+ export type AsyncCells = Record<string, AsyncCell>
26
30
  export type AsyncCell = () => Promise<SqlPrimitive>
27
31
 
28
32
  export type Row = Record<string, SqlPrimitive>[]
29
33
 
30
34
  export interface ExecuteSqlOptions {
31
35
  tables: Record<string, Row | AsyncDataSource>
32
- query: string
36
+ query: string | SelectStatement
33
37
  }
34
38
 
35
39
  export type SqlPrimitive =
@@ -188,6 +192,17 @@ export type MathFunc =
188
192
  | 'LOG10'
189
193
  | 'POWER'
190
194
  | 'SQRT'
195
+ | 'SIN'
196
+ | 'COS'
197
+ | 'TAN'
198
+ | 'COT'
199
+ | 'ASIN'
200
+ | 'ACOS'
201
+ | 'ATAN'
202
+ | 'ATAN2'
203
+ | 'DEGREES'
204
+ | 'RADIANS'
205
+ | 'PI'
191
206
 
192
207
  export type StringFunc =
193
208
  | 'UPPER'
package/src/validation.js CHANGED
@@ -16,6 +16,8 @@ export function isMathFunc(name) {
16
16
  return [
17
17
  'FLOOR', 'CEIL', 'CEILING', 'ABS', 'MOD',
18
18
  'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
19
+ 'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2',
20
+ 'DEGREES', 'RADIANS', 'PI',
19
21
  ].includes(name)
20
22
  }
21
23
 
@@ -38,6 +38,17 @@ const FUNCTION_SIGNATURES = {
38
38
  LOG10: 'number',
39
39
  POWER: 'base, exponent',
40
40
  SQRT: 'number',
41
+ SIN: 'radians',
42
+ COS: 'radians',
43
+ TAN: 'radians',
44
+ COT: 'radians',
45
+ ASIN: 'number',
46
+ ACOS: 'number',
47
+ ATAN: 'number',
48
+ ATAN2: 'y, x',
49
+ DEGREES: 'radians',
50
+ RADIANS: 'degrees',
51
+ PI: '',
41
52
 
42
53
  // JSON functions
43
54
  JSON_VALUE: 'expression, path',
@@ -74,7 +85,7 @@ export function argCountError({ funcName, expected, received, positionStart, pos
74
85
  expectedStr = `${expected} argument`
75
86
  }
76
87
 
77
- return new ExecutionError(`${funcName}(${signature}) function requires ${expectedStr}, got ${received}`, positionStart, positionEnd, rowNumber)
88
+ return new ExecutionError({ message: `${funcName}(${signature}) function requires ${expectedStr}, got ${received}`, positionStart, positionEnd, rowNumber })
78
89
  }
79
90
 
80
91
  /**
@@ -92,7 +103,7 @@ export function argCountError({ funcName, expected, received, positionStart, pos
92
103
  export function argValueError({ funcName, message, positionStart, positionEnd, hint, rowNumber }) {
93
104
  const signature = FUNCTION_SIGNATURES[funcName] ?? ''
94
105
  const suffix = hint ? `. ${hint}` : ''
95
- return new ExecutionError(`${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowNumber)
106
+ return new ExecutionError({ message: `${funcName}(${signature}): ${message}${suffix}`, positionStart, positionEnd, rowNumber })
96
107
  }
97
108
 
98
109
  /**
@@ -123,5 +134,5 @@ export function castError({ toType, positionStart, positionEnd, fromType, rowNum
123
134
  ? `Cannot CAST ${fromType} to ${toType}`
124
135
  : `Unsupported CAST to type ${toType}`
125
136
 
126
- return new ExecutionError(`${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`, positionStart, positionEnd, rowNumber)
137
+ return new ExecutionError({ message: `${message}. Supported types: TEXT, VARCHAR, INTEGER, INT, BIGINT, FLOAT, REAL, DOUBLE, BOOLEAN`, positionStart, positionEnd, rowNumber })
127
138
  }