squirreling 0.4.7 → 0.5.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
@@ -76,5 +76,6 @@ console.log(allUsers)
76
76
  - `GROUP BY` and `HAVING` clauses
77
77
  - Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
78
78
  - String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
79
+ - Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
79
80
  - Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
80
81
  - Basic expressions and arithmetic operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.7",
3
+ "version": "0.5.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.1",
43
+ "eslint-plugin-jsdoc": "61.5.0",
44
44
  "typescript": "5.9.3",
45
45
  "vitest": "4.0.15"
46
46
  }
@@ -1,3 +1,5 @@
1
+ import { unknownFunctionError } from '../parseErrors.js'
2
+ import { aggregateError } from '../validationErrors.js'
1
3
  import { evaluateExpr } from './expression.js'
2
4
  import { defaultDerivedAlias, stringify } from './utils.js'
3
5
 
@@ -20,7 +22,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
20
22
  const seen = new Set()
21
23
  for (const row of rows) {
22
24
  const v = await evaluateExpr({ node: arg.expr, row, tables })
23
- if (v !== null && v !== undefined) {
25
+ if (v != null) {
24
26
  seen.add(v)
25
27
  }
26
28
  }
@@ -29,7 +31,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
29
31
  let count = 0
30
32
  for (const row of rows) {
31
33
  const v = await evaluateExpr({ node: arg.expr, row, tables })
32
- if (v !== null && v !== undefined) {
34
+ if (v != null) {
33
35
  count += 1
34
36
  }
35
37
  }
@@ -38,7 +40,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
38
40
 
39
41
  if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
40
42
  if (arg.kind === 'star') {
41
- throw new Error(func + '(*) is not supported, use a column name')
43
+ throw aggregateError({ funcName: func, issue: '(*) is not supported, use a column name' })
42
44
  }
43
45
  let sum = 0
44
46
  let count = 0
@@ -72,7 +74,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
72
74
 
73
75
  if (func === 'JSON_ARRAYAGG') {
74
76
  if (arg.kind === 'star') {
75
- throw new Error('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' })
76
78
  }
77
79
  /** @type {SqlPrimitive[]} */
78
80
  const values = []
@@ -95,7 +97,12 @@ export async function evaluateAggregate({ col, rows, tables }) {
95
97
  return values
96
98
  }
97
99
 
98
- throw new Error('Unsupported aggregate function ' + func)
100
+ throw unknownFunctionError({
101
+ funcName: func,
102
+ positionStart: 0,
103
+ positionEnd: 0,
104
+ validFunctions: 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG',
105
+ })
99
106
  }
100
107
 
101
108
  /**
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @import { SqlPrimitive, IntervalUnit } from '../types.js'
3
+ */
4
+
5
+ /**
6
+ * @param {SqlPrimitive} val
7
+ * @returns {Date | null}
8
+ */
9
+ function toDate(val) {
10
+ if (val instanceof Date) return val
11
+ const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
12
+ if (typeof val === 'string' && dateOrTime.test(val)) {
13
+ const date = new Date(val)
14
+ if (!isNaN(date.getTime())) {
15
+ return date
16
+ }
17
+ }
18
+ return null
19
+ }
20
+
21
+ /**
22
+ * Apply an interval to a date
23
+ * @param {SqlPrimitive} dateVal
24
+ * @param {number} value
25
+ * @param {IntervalUnit} unit
26
+ * @param {'+' | '-'} op
27
+ * @returns {string | null}
28
+ */
29
+ export function applyIntervalToDate(dateVal, value, unit, op) {
30
+ const date = toDate(dateVal)
31
+ if (date == null) return null
32
+
33
+ const multiplier = op === '+' ? 1 : -1
34
+ const adjusted = value * multiplier
35
+
36
+ if (unit === 'SECOND') {
37
+ date.setUTCSeconds(date.getUTCSeconds() + adjusted)
38
+ } else if (unit === 'MINUTE') {
39
+ date.setUTCMinutes(date.getUTCMinutes() + adjusted)
40
+ } else if (unit === 'HOUR') {
41
+ date.setUTCHours(date.getUTCHours() + adjusted)
42
+ } else if (unit === 'DAY') {
43
+ date.setUTCDate(date.getUTCDate() + adjusted)
44
+ } else if (unit === 'MONTH') {
45
+ date.setUTCMonth(date.getUTCMonth() + adjusted)
46
+ } else if (unit === 'YEAR') {
47
+ date.setUTCFullYear(date.getUTCFullYear() + adjusted)
48
+ }
49
+
50
+ // Return in same format as input
51
+ if (dateVal instanceof Date) return date.toISOString()
52
+ if (String(dateVal).includes('T')) {
53
+ return date.toISOString()
54
+ } else {
55
+ return date.toISOString().split('T')[0]
56
+ }
57
+ }
@@ -1,3 +1,5 @@
1
+ import { missingClauseError } from '../parseErrors.js'
2
+ import { tableNotFoundError, unsupportedOperationError } from '../executionErrors.js'
1
3
  import { generatorSource, memorySource } from '../backend/dataSource.js'
2
4
  import { parseSql } from '../parse/parse.js'
3
5
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
@@ -22,7 +24,10 @@ export async function* executeSql({ tables, query }) {
22
24
 
23
25
  // Check for unsupported operations
24
26
  if (!select.from) {
25
- throw new Error('FROM clause is required')
27
+ throw missingClauseError({
28
+ missing: 'FROM clause',
29
+ context: 'SELECT statement',
30
+ })
26
31
  }
27
32
 
28
33
  // Normalize tables: convert arrays to AsyncDataSource
@@ -57,7 +62,7 @@ export async function* executeSelect(select, tables) {
57
62
  fromTableName = select.from.alias ?? select.from.table
58
63
  dataSource = tables[select.from.table]
59
64
  if (dataSource === undefined) {
60
- throw new Error(`Table "${select.from.table}" not found`)
65
+ throw tableNotFoundError({ tableName: select.from.table })
61
66
  }
62
67
  } else {
63
68
  // Nested subquery - recursively resolve
@@ -232,6 +237,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
232
237
  async function* evaluateStreaming(select, dataSource, tables) {
233
238
  let rowsYielded = 0
234
239
  let rowsSkipped = 0
240
+ let rowIndex = 0
235
241
  const offset = select.offset ?? 0
236
242
  const limit = select.limit ?? Infinity
237
243
  if (limit <= 0) return
@@ -250,9 +256,10 @@ async function* evaluateStreaming(select, dataSource, tables) {
250
256
  }
251
257
 
252
258
  for await (const row of dataSource.getRows(hints)) {
259
+ rowIndex++
253
260
  // WHERE filter
254
261
  if (select.where) {
255
- const pass = await evaluateExpr({ node: select.where, row, tables })
262
+ const pass = await evaluateExpr({ node: select.where, row, tables, rowIndex })
256
263
  if (!pass) continue
257
264
  }
258
265
 
@@ -265,6 +272,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
265
272
  // SELECT projection
266
273
  /** @type {AsyncRow} */
267
274
  const outRow = {}
275
+ const currentRowIndex = rowIndex
268
276
  for (const col of select.columns) {
269
277
  if (col.kind === 'star') {
270
278
  for (const [key, cell] of Object.entries(row)) {
@@ -272,7 +280,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
272
280
  }
273
281
  } else if (col.kind === 'derived') {
274
282
  const alias = col.alias ?? defaultDerivedAlias(col.expr)
275
- outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
283
+ outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
276
284
  } else if (col.kind === 'aggregate') {
277
285
  throw new Error(
278
286
  'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
@@ -330,9 +338,11 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
330
338
  /** @type {AsyncRow[]} */
331
339
  const filtered = []
332
340
 
333
- for (const row of working) {
341
+ for (let i = 0; i < working.length; i++) {
342
+ const row = working[i]
343
+ const rowIndex = i + 1 // 1-based
334
344
  if (select.where) {
335
- const passes = await evaluateExpr({ node: select.where, row, tables })
345
+ const passes = await evaluateExpr({ node: select.where, row, tables, rowIndex })
336
346
 
337
347
  if (!passes) {
338
348
  continue
@@ -375,7 +385,10 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
375
385
 
376
386
  const hasStar = select.columns.some(col => col.kind === 'star')
377
387
  if (hasStar && hasAggregate) {
378
- throw new Error('SELECT * with aggregate functions is not supported in this implementation')
388
+ throw unsupportedOperationError({
389
+ operation: 'SELECT * with aggregate functions is not supported',
390
+ hint: 'Replace * with specific column names when using aggregate functions.',
391
+ })
379
392
  }
380
393
 
381
394
  for (const group of groups) {