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.
@@ -1,9 +1,10 @@
1
- import { missingClauseError, tableNotFoundError } from '../errors.js'
1
+ import { missingClauseError } from '../parseErrors.js'
2
+ import { tableNotFoundError } from '../executionErrors.js'
2
3
  import { evaluateExpr } from './expression.js'
3
4
  import { stringify } from './utils.js'
4
5
 
5
6
  /**
6
- * @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode } from '../types.js'
7
+ * @import { AsyncRow, AsyncDataSource, JoinClause, ExprNode, AsyncCells } from '../types.js'
7
8
  */
8
9
 
9
10
  /**
@@ -23,13 +24,13 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
23
24
  const join = joins[0]
24
25
  const rightSource = tables[join.table]
25
26
  if (rightSource === undefined) {
26
- throw tableNotFoundError(join.table)
27
+ throw tableNotFoundError({ tableName: join.table })
27
28
  }
28
29
 
29
30
  // Buffer right rows for hash index (required for hash join)
30
31
  /** @type {AsyncRow[]} */
31
32
  const rightRows = []
32
- for await (const row of rightSource.getRows()) {
33
+ for await (const row of rightSource.scan()) {
33
34
  rightRows.push(row)
34
35
  }
35
36
 
@@ -38,9 +39,9 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
38
39
 
39
40
  // Return streaming data source - left rows stream through without buffering
40
41
  return {
41
- async *getRows() {
42
+ async *scan() {
42
43
  yield* hashJoin({
43
- leftRows: leftSource.getRows(), // Stream directly, not buffered
44
+ leftRows: leftSource.scan(), // Stream directly, not buffered
44
45
  rightRows,
45
46
  join,
46
47
  leftTable: currentLeftTable,
@@ -54,7 +55,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
54
55
  // Multiple joins: buffer intermediate results, stream final join
55
56
  /** @type {AsyncRow[]} */
56
57
  let leftRows = []
57
- for await (const row of leftSource.getRows()) {
58
+ for await (const row of leftSource.scan()) {
58
59
  leftRows.push(row)
59
60
  }
60
61
 
@@ -63,12 +64,12 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
63
64
  const join = joins[i]
64
65
  const rightSource = tables[join.table]
65
66
  if (rightSource === undefined) {
66
- throw tableNotFoundError(join.table)
67
+ throw tableNotFoundError({ tableName: join.table })
67
68
  }
68
69
 
69
70
  /** @type {AsyncRow[]} */
70
71
  const rightRows = []
71
- for await (const row of rightSource.getRows()) {
72
+ for await (const row of rightSource.scan()) {
72
73
  rightRows.push(row)
73
74
  }
74
75
 
@@ -99,12 +100,12 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
99
100
  const lastJoin = joins[joins.length - 1]
100
101
  const rightSource = tables[lastJoin.table]
101
102
  if (rightSource === undefined) {
102
- throw tableNotFoundError(lastJoin.table)
103
+ throw tableNotFoundError({ tableName: lastJoin.table })
103
104
  }
104
105
 
105
106
  /** @type {AsyncRow[]} */
106
107
  const rightRows = []
107
- for await (const row of rightSource.getRows()) {
108
+ for await (const row of rightSource.scan()) {
108
109
  rightRows.push(row)
109
110
  }
110
111
 
@@ -112,7 +113,7 @@ export async function executeJoins(leftSource, joins, leftTableName, tables) {
112
113
  const lastRightTableName = lastJoin.alias ?? lastJoin.table
113
114
 
114
115
  return {
115
- async *getRows() {
116
+ async *scan() {
116
117
  yield* hashJoin({
117
118
  leftRows,
118
119
  rightRows,
@@ -171,12 +172,12 @@ function extractJoinKeys(onCondition, leftTable, rightTable) {
171
172
  * @returns {AsyncRow}
172
173
  */
173
174
  function createNullRow(columnNames) {
174
- /** @type {AsyncRow} */
175
- const row = {}
175
+ /** @type {AsyncCells} */
176
+ const cells = {}
176
177
  for (const col of columnNames) {
177
- row[col] = () => Promise.resolve(null)
178
+ cells[col] = () => Promise.resolve(null)
178
179
  }
179
- return row
180
+ return { columns: columnNames, cells }
180
181
  }
181
182
 
182
183
  /**
@@ -189,33 +190,35 @@ function createNullRow(columnNames) {
189
190
  * @returns {AsyncRow}
190
191
  */
191
192
  function mergeRows(leftRow, rightRow, leftTable, rightTable) {
192
- /** @type {AsyncRow} */
193
- const merged = {}
193
+ const columns = []
194
+ /** @type {AsyncCells} */
195
+ const cells = {}
194
196
 
195
197
  // Add left table columns with prefix
196
- for (const [key, cell] of Object.entries(leftRow)) {
198
+ for (const [key, cell] of Object.entries(leftRow.cells)) {
197
199
  // Skip already-prefixed keys (from previous joins)
198
200
  if (!key.includes('.')) {
199
- merged[`${leftTable}.${key}`] = cell
200
- } else {
201
- merged[key] = cell
201
+ const alias = `${leftTable}.${key}`
202
+ cells[alias] = cell
202
203
  }
203
- // Also keep unqualified name for convenience (may be overwritten if ambiguous)
204
- merged[key] = cell
204
+ // Also keep unqualified name for convenience
205
+ columns.push(key)
206
+ cells[key] = cell
205
207
  }
206
208
 
207
209
  // Add right table columns with prefix
208
- for (const [key, cell] of Object.entries(rightRow)) {
210
+ for (const [key, cell] of Object.entries(rightRow.cells)) {
209
211
  if (!key.includes('.')) {
210
- merged[`${rightTable}.${key}`] = cell
212
+ cells[`${rightTable}.${key}`] = cell
211
213
  } else {
212
- merged[key] = cell
214
+ cells[key] = cell
213
215
  }
214
216
  // Unqualified name (overwrites if same name exists in left table)
215
- merged[key] = cell
217
+ columns.push(key)
218
+ cells[key] = cell
216
219
  }
217
220
 
218
- return merged
221
+ return { columns, cells }
219
222
  }
220
223
 
221
224
  /**
@@ -244,7 +247,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
244
247
  const keys = extractJoinKeys(onCondition, leftTable, rightTable)
245
248
 
246
249
  // Get column names for NULL row generation (right side is always buffered)
247
- const rightCols = rightRows.length ? Object.keys(rightRows[0]) : []
250
+ const rightCols = rightRows.length ? rightRows[0].columns : []
248
251
  const rightPrefixedCols = rightCols.flatMap(col =>
249
252
  col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
250
253
  )
@@ -280,8 +283,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
280
283
  for await (const leftRow of leftRows) {
281
284
  // Capture left column info from first row (for NULL row generation)
282
285
  if (!leftPrefixedCols) {
283
- const leftCols = Object.keys(leftRow)
284
- leftPrefixedCols = leftCols.flatMap(col =>
286
+ leftPrefixedCols = leftRow.columns.flatMap(col =>
285
287
  col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
286
288
  )
287
289
  }
@@ -322,8 +324,7 @@ async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tab
322
324
  for await (const leftRow of leftRows) {
323
325
  // Capture left column info from first row (for NULL row generation)
324
326
  if (!leftPrefixedCols) {
325
- const leftCols = Object.keys(leftRow)
326
- leftPrefixedCols = leftCols.flatMap(col =>
327
+ leftPrefixedCols = leftRow.columns.flatMap(col =>
327
328
  col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
328
329
  )
329
330
  }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * @import { MathFunc, SqlPrimitive } from '../types.js'
3
+ */
4
+ import { argCountError } from '../validationErrors.js'
5
+
6
+ /**
7
+ * Evaluate a math function
8
+ *
9
+ * @param {Object} options
10
+ * @param {MathFunc} options.funcName - Uppercase function name
11
+ * @param {SqlPrimitive[]} options.args - Function arguments
12
+ * @param {number} options.positionStart - Start position in query
13
+ * @param {number} options.positionEnd - End position in query
14
+ * @param {number} [options.rowNumber] - 1-based row number for error reporting
15
+ * @returns {SqlPrimitive} Result
16
+ */
17
+ export function evaluateMathFunc({ funcName, args, positionStart, positionEnd, rowNumber }) {
18
+ if (funcName === 'FLOOR') {
19
+ if (args.length !== 1) {
20
+ throw argCountError({
21
+ funcName: 'FLOOR',
22
+ expected: 1,
23
+ received: args.length,
24
+ positionStart,
25
+ positionEnd,
26
+ rowNumber,
27
+ })
28
+ }
29
+ const val = args[0]
30
+ if (val == null) return null
31
+ return Math.floor(Number(val))
32
+ }
33
+
34
+ if (funcName === 'CEIL' || funcName === 'CEILING') {
35
+ if (args.length !== 1) {
36
+ throw argCountError({
37
+ funcName,
38
+ expected: 1,
39
+ received: args.length,
40
+ positionStart,
41
+ positionEnd,
42
+ rowNumber,
43
+ })
44
+ }
45
+ const val = args[0]
46
+ if (val == null) return null
47
+ return Math.ceil(Number(val))
48
+ }
49
+
50
+ if (funcName === 'ABS') {
51
+ if (args.length !== 1) {
52
+ throw argCountError({
53
+ funcName: 'ABS',
54
+ expected: 1,
55
+ received: args.length,
56
+ positionStart,
57
+ positionEnd,
58
+ rowNumber,
59
+ })
60
+ }
61
+ const val = args[0]
62
+ if (val == null) return null
63
+ return Math.abs(Number(val))
64
+ }
65
+
66
+ if (funcName === 'MOD') {
67
+ if (args.length !== 2) {
68
+ throw argCountError({
69
+ funcName: 'MOD',
70
+ expected: 2,
71
+ received: args.length,
72
+ positionStart,
73
+ positionEnd,
74
+ rowNumber,
75
+ })
76
+ }
77
+ const dividend = args[0]
78
+ const divisor = args[1]
79
+ if (dividend == null || divisor == null) return null
80
+ return Number(dividend) % Number(divisor)
81
+ }
82
+
83
+ if (funcName === 'EXP') {
84
+ if (args.length !== 1) {
85
+ throw argCountError({
86
+ funcName: 'EXP',
87
+ expected: 1,
88
+ received: args.length,
89
+ positionStart,
90
+ positionEnd,
91
+ rowNumber,
92
+ })
93
+ }
94
+ const val = args[0]
95
+ if (val == null) return null
96
+ return Math.exp(Number(val))
97
+ }
98
+
99
+ if (funcName === 'LN') {
100
+ if (args.length !== 1) {
101
+ throw argCountError({
102
+ funcName: 'LN',
103
+ expected: 1,
104
+ received: args.length,
105
+ positionStart,
106
+ positionEnd,
107
+ rowNumber,
108
+ })
109
+ }
110
+ const val = args[0]
111
+ if (val == null) return null
112
+ return Math.log(Number(val))
113
+ }
114
+
115
+ if (funcName === 'LOG10') {
116
+ if (args.length !== 1) {
117
+ throw argCountError({
118
+ funcName: 'LOG10',
119
+ expected: 1,
120
+ received: args.length,
121
+ positionStart,
122
+ positionEnd,
123
+ rowNumber,
124
+ })
125
+ }
126
+ const val = args[0]
127
+ if (val == null) return null
128
+ return Math.log10(Number(val))
129
+ }
130
+
131
+ if (funcName === 'POWER') {
132
+ if (args.length !== 2) {
133
+ throw argCountError({
134
+ funcName: 'POWER',
135
+ expected: 2,
136
+ received: args.length,
137
+ positionStart,
138
+ positionEnd,
139
+ rowNumber,
140
+ })
141
+ }
142
+ const base = args[0]
143
+ const exponent = args[1]
144
+ if (base == null || exponent == null) return null
145
+ return Number(base) ** Number(exponent)
146
+ }
147
+
148
+ if (funcName === 'SQRT') {
149
+ if (args.length !== 1) {
150
+ throw argCountError({
151
+ funcName: 'SQRT',
152
+ expected: 1,
153
+ received: args.length,
154
+ positionStart,
155
+ positionEnd,
156
+ rowNumber,
157
+ })
158
+ }
159
+ const val = args[0]
160
+ if (val == null) return null
161
+ return Math.sqrt(Number(val))
162
+ }
163
+
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
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
  }
@@ -0,0 +1,63 @@
1
+ // ============================================================================
2
+ // EXECUTION ERRORS - Issues during query execution
3
+ // ============================================================================
4
+
5
+ /**
6
+ * Structured execution error with position range and optional row number.
7
+ */
8
+ export class ExecutionError extends Error {
9
+ /**
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
15
+ */
16
+ constructor({ message, positionStart, positionEnd, rowNumber }) {
17
+ const rowSuffix = rowNumber != null ? ` (row ${rowNumber})` : ''
18
+ super(message + rowSuffix)
19
+ this.name = 'ExecutionError'
20
+ this.positionStart = positionStart
21
+ this.positionEnd = positionEnd
22
+ this.rowNumber = rowNumber
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Error for missing table.
28
+ *
29
+ * @param {Object} options
30
+ * @param {string} options.tableName - The missing table name
31
+ * @returns {Error}
32
+ */
33
+ export function tableNotFoundError({ tableName }) {
34
+ return new Error(`Table "${tableName}" not found. Check spelling or add it to the tables parameter.`)
35
+ }
36
+
37
+ /**
38
+ * Error for invalid context (e.g., INTERVAL without date arithmetic).
39
+ *
40
+ * @param {Object} options
41
+ * @param {string} options.item - What was used incorrectly
42
+ * @param {string} options.validContext - Where it can be used
43
+ * @param {number} options.positionStart - Start position in query
44
+ * @param {number} options.positionEnd - End position in query
45
+ * @param {number} [options.rowNumber] - 1-based row number where error occurred
46
+ * @returns {ExecutionError}
47
+ */
48
+ export function invalidContextError({ item, validContext, positionStart, positionEnd, rowNumber }) {
49
+ return new ExecutionError({ message: `${item} can only be used with ${validContext}`, positionStart, positionEnd, rowNumber })
50
+ }
51
+
52
+ /**
53
+ * Error for unsupported operation combinations.
54
+ *
55
+ * @param {Object} options
56
+ * @param {string} options.operation - The unsupported operation
57
+ * @param {string} [options.hint] - How to fix it
58
+ * @returns {Error}
59
+ */
60
+ export function unsupportedOperationError({ operation, hint }) {
61
+ const suffix = hint ? `. ${hint}` : ''
62
+ return new Error(`${operation}${suffix}`)
63
+ }
package/src/index.js CHANGED
@@ -2,3 +2,4 @@ export { executeSql } from './execute/execute.js'
2
2
  export { parseSql } from './parse/parse.js'
3
3
  export { collect } from './execute/utils.js'
4
4
  export { cachedDataSource } from './backend/dataSource.js'
5
+ export { ParseError } from './parseErrors.js'