squirreling 0.7.1 → 0.7.2
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 +2 -2
- package/package.json +2 -2
- package/src/execute/join.js +98 -29
- package/src/index.d.ts +10 -2
- package/src/index.js +1 -0
- package/src/parse/joins.js +10 -3
- package/src/parse/parse.js +2 -2
- package/src/parse/tokenize.js +2 -1
- package/src/types.d.ts +1 -1
- package/src/validation.js +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|

|
|
11
11
|
[](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
|
|
12
12
|
|
|
13
|
-
Squirreling is a streaming async SQL engine for
|
|
13
|
+
Squirreling is a streaming async SQL engine built for the web. It is designed to query over various data sources and provide efficient streaming of results. 100% JavaScript with zero dependencies.
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
@@ -79,7 +79,7 @@ console.log(`Collected rows:`, rows)
|
|
|
79
79
|
|
|
80
80
|
- `SELECT` statements with `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET`
|
|
81
81
|
- Subqueries in `SELECT`, `FROM`, and `WHERE` clauses
|
|
82
|
-
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`
|
|
82
|
+
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`, `POSITIONAL JOIN`
|
|
83
83
|
- `GROUP BY` and `HAVING` clauses
|
|
84
84
|
|
|
85
85
|
### Functions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "
|
|
40
|
+
"@types/node": "25.0.3",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.16",
|
|
42
42
|
"eslint": "9.39.2",
|
|
43
43
|
"eslint-plugin-jsdoc": "61.5.0",
|
package/src/execute/join.js
CHANGED
|
@@ -43,16 +43,26 @@ export async function executeJoins({ leftSource, joins, leftTable, tables, funct
|
|
|
43
43
|
return {
|
|
44
44
|
async *scan(options) {
|
|
45
45
|
const { signal } = options
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
if (join.joinType === 'POSITIONAL') {
|
|
47
|
+
yield* positionalJoin({
|
|
48
|
+
leftRows: leftSource.scan(options),
|
|
49
|
+
rightRows,
|
|
50
|
+
leftTable: currentLeftTable,
|
|
51
|
+
rightTable,
|
|
52
|
+
signal,
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
yield* hashJoin({
|
|
56
|
+
leftRows: leftSource.scan(options), // Stream directly, not buffered
|
|
57
|
+
rightRows,
|
|
58
|
+
join,
|
|
59
|
+
leftTable: currentLeftTable,
|
|
60
|
+
rightTable,
|
|
61
|
+
tables,
|
|
62
|
+
functions,
|
|
63
|
+
signal,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
56
66
|
},
|
|
57
67
|
}
|
|
58
68
|
}
|
|
@@ -84,15 +94,22 @@ export async function executeJoins({ leftSource, joins, leftTable, tables, funct
|
|
|
84
94
|
// Collect intermediate results into array for next join
|
|
85
95
|
/** @type {AsyncRow[]} */
|
|
86
96
|
const newLeftRows = []
|
|
87
|
-
const joined =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
const joined = join.joinType === 'POSITIONAL'
|
|
98
|
+
? positionalJoin({
|
|
99
|
+
leftRows,
|
|
100
|
+
rightRows,
|
|
101
|
+
leftTable: currentLeftTable,
|
|
102
|
+
rightTable,
|
|
103
|
+
})
|
|
104
|
+
: hashJoin({
|
|
105
|
+
leftRows,
|
|
106
|
+
rightRows,
|
|
107
|
+
join,
|
|
108
|
+
leftTable: currentLeftTable,
|
|
109
|
+
rightTable,
|
|
110
|
+
tables,
|
|
111
|
+
functions,
|
|
112
|
+
})
|
|
96
113
|
for await (const row of joined) {
|
|
97
114
|
newLeftRows.push(row)
|
|
98
115
|
}
|
|
@@ -121,16 +138,26 @@ export async function executeJoins({ leftSource, joins, leftTable, tables, funct
|
|
|
121
138
|
return {
|
|
122
139
|
async *scan(options) {
|
|
123
140
|
const { signal } = options
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
if (join.joinType === 'POSITIONAL') {
|
|
142
|
+
yield* positionalJoin({
|
|
143
|
+
leftRows,
|
|
144
|
+
rightRows,
|
|
145
|
+
leftTable: currentLeftTable,
|
|
146
|
+
rightTable,
|
|
147
|
+
signal,
|
|
148
|
+
})
|
|
149
|
+
} else {
|
|
150
|
+
yield* hashJoin({
|
|
151
|
+
leftRows,
|
|
152
|
+
rightRows,
|
|
153
|
+
join,
|
|
154
|
+
leftTable: currentLeftTable,
|
|
155
|
+
rightTable,
|
|
156
|
+
tables,
|
|
157
|
+
functions,
|
|
158
|
+
signal,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
134
161
|
},
|
|
135
162
|
}
|
|
136
163
|
}
|
|
@@ -230,6 +257,48 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
230
257
|
return { columns, cells }
|
|
231
258
|
}
|
|
232
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Performs a positional join between left and right row sets.
|
|
262
|
+
* Matches rows by their index position (row 0 with row 0, row 1 with row 1, etc.).
|
|
263
|
+
* When tables have different lengths, the shorter table is padded with NULLs.
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} params
|
|
266
|
+
* @param {AsyncIterable<AsyncRow>|AsyncRow[]} params.leftRows - rows from left table
|
|
267
|
+
* @param {AsyncRow[]} params.rightRows - rows from right table (must be buffered)
|
|
268
|
+
* @param {string} params.leftTable - name of left table (for column prefixing)
|
|
269
|
+
* @param {string} params.rightTable - name of right table (for column prefixing, may be alias)
|
|
270
|
+
* @param {AbortSignal} [params.signal] - abort signal for cancellation
|
|
271
|
+
* @yields {AsyncRow} joined rows
|
|
272
|
+
*/
|
|
273
|
+
async function* positionalJoin({ leftRows, rightRows, leftTable, rightTable, signal }) {
|
|
274
|
+
// Buffer left rows if streaming
|
|
275
|
+
/** @type {AsyncRow[]} */
|
|
276
|
+
const leftArr = []
|
|
277
|
+
for await (const row of leftRows) {
|
|
278
|
+
if (signal?.aborted) return
|
|
279
|
+
leftArr.push(row)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const maxLen = Math.max(leftArr.length, rightRows.length)
|
|
283
|
+
|
|
284
|
+
// Get column info for NULL row creation
|
|
285
|
+
const leftCols = leftArr[0]?.columns ?? []
|
|
286
|
+
const rightCols = rightRows[0]?.columns ?? []
|
|
287
|
+
const leftPrefixedCols = leftCols.flatMap(col =>
|
|
288
|
+
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
289
|
+
)
|
|
290
|
+
const rightPrefixedCols = rightCols.flatMap(col =>
|
|
291
|
+
col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
for (let i = 0; i < maxLen; i++) {
|
|
295
|
+
if (signal?.aborted) return
|
|
296
|
+
const leftRow = leftArr[i] ?? createNullRow(leftPrefixedCols)
|
|
297
|
+
const rightRow = rightRows[i] ?? createNullRow(rightPrefixedCols)
|
|
298
|
+
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
233
302
|
/**
|
|
234
303
|
* Performs a hash join between left and right row sets (streaming).
|
|
235
304
|
* Yields rows as they are found instead of buffering all results.
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AsyncDataSource, AsyncRow, ExecuteSqlOptions, ParseSqlOptions, SelectStatement, SqlPrimitive } from './types.js'
|
|
2
|
-
export type { AsyncDataSource, AsyncRow, ParseSqlOptions, SqlPrimitive } from './types.js'
|
|
1
|
+
import type { AsyncDataSource, AsyncRow, ExecuteSqlOptions, ParseSqlOptions, SelectStatement, SqlPrimitive, Token } from './types.js'
|
|
2
|
+
export type { AsyncCells, AsyncDataSource, AsyncRow, ExprNode, ParseSqlOptions, SelectStatement, SqlPrimitive, Token, UserDefinedFunction } from './types.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Executes a SQL SELECT query against an array of data rows
|
|
@@ -23,6 +23,14 @@ export function executeSql(options: ExecuteSqlOptions): AsyncGenerator<AsyncRow>
|
|
|
23
23
|
*/
|
|
24
24
|
export function parseSql(options: ParseSqlOptions): SelectStatement
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Tokenizes a SQL query string into an array of tokens
|
|
28
|
+
*
|
|
29
|
+
* @param sql - SQL query string to tokenize
|
|
30
|
+
* @returns array of tokens
|
|
31
|
+
*/
|
|
32
|
+
export function tokenizeSql(sql: string): Token[]
|
|
33
|
+
|
|
26
34
|
/**
|
|
27
35
|
* Collects all results from an async generator into an array
|
|
28
36
|
*
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { executeSql } from './execute/execute.js'
|
|
2
2
|
export { parseSql } from './parse/parse.js'
|
|
3
|
+
export { tokenizeSql } from './parse/tokenize.js'
|
|
3
4
|
export { collect } from './execute/utils.js'
|
|
4
5
|
export { cachedDataSource } from './backend/dataSource.js'
|
|
5
6
|
export { ParseError } from './parseErrors.js'
|
package/src/parse/joins.js
CHANGED
|
@@ -40,6 +40,9 @@ export function parseJoins(state) {
|
|
|
40
40
|
// FULL OUTER JOIN
|
|
41
41
|
}
|
|
42
42
|
joinType = 'FULL'
|
|
43
|
+
} else if (tok.value === 'POSITIONAL') {
|
|
44
|
+
consume(state)
|
|
45
|
+
joinType = 'POSITIONAL'
|
|
43
46
|
} else if (tok.value === 'JOIN') {
|
|
44
47
|
// Just JOIN (defaults to INNER)
|
|
45
48
|
consume(state)
|
|
@@ -61,9 +64,13 @@ export function parseJoins(state) {
|
|
|
61
64
|
const tableName = expectIdentifier(state).value
|
|
62
65
|
const tableAlias = parseTableAlias(state)
|
|
63
66
|
|
|
64
|
-
// Parse ON condition
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
// Parse ON condition (not for POSITIONAL joins)
|
|
68
|
+
/** @type {import('../types.js').ExprNode | undefined} */
|
|
69
|
+
let condition
|
|
70
|
+
if (joinType !== 'POSITIONAL') {
|
|
71
|
+
expect(state, 'keyword', 'ON')
|
|
72
|
+
condition = parseExpression(state)
|
|
73
|
+
}
|
|
67
74
|
|
|
68
75
|
joins.push({
|
|
69
76
|
joinType,
|
package/src/parse/parse.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tokenizeSql } from './tokenize.js'
|
|
2
2
|
import { parseExpression } from './expression.js'
|
|
3
3
|
import { RESERVED_AFTER_COLUMN, RESERVED_AFTER_TABLE, isKnownFunction } from '../validation.js'
|
|
4
4
|
import { consume, current, expect, expectIdentifier, match, parseError, peekToken } from './state.js'
|
|
@@ -13,7 +13,7 @@ import { parseJoins } from './joins.js'
|
|
|
13
13
|
* @returns {SelectStatement}
|
|
14
14
|
*/
|
|
15
15
|
export function parseSql({ query, functions }) {
|
|
16
|
-
const tokens =
|
|
16
|
+
const tokens = tokenizeSql(query)
|
|
17
17
|
/** @type {ParserState} */
|
|
18
18
|
const state = { tokens, pos: 0, functions }
|
|
19
19
|
const select = parseSelectInternal(state)
|
package/src/parse/tokenize.js
CHANGED
|
@@ -46,6 +46,7 @@ const KEYWORDS = new Set([
|
|
|
46
46
|
'RIGHT',
|
|
47
47
|
'FULL',
|
|
48
48
|
'OUTER',
|
|
49
|
+
'POSITIONAL',
|
|
49
50
|
'ON',
|
|
50
51
|
'INTERVAL',
|
|
51
52
|
'DAY',
|
|
@@ -60,7 +61,7 @@ const KEYWORDS = new Set([
|
|
|
60
61
|
* @param {string} sql
|
|
61
62
|
* @returns {Token[]}
|
|
62
63
|
*/
|
|
63
|
-
export function
|
|
64
|
+
export function tokenizeSql(sql) {
|
|
64
65
|
/** @type {Token[]} */
|
|
65
66
|
const tokens = []
|
|
66
67
|
const { length } = sql
|
package/src/types.d.ts
CHANGED
|
@@ -255,7 +255,7 @@ export interface OrderByItem {
|
|
|
255
255
|
nulls?: 'FIRST' | 'LAST'
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS'
|
|
258
|
+
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS' | 'POSITIONAL'
|
|
259
259
|
|
|
260
260
|
export interface JoinClause {
|
|
261
261
|
joinType: JoinType
|
package/src/validation.js
CHANGED
|
@@ -211,5 +211,5 @@ export const RESERVED_AFTER_COLUMN = new Set([
|
|
|
211
211
|
// Keywords that cannot be used as table aliases
|
|
212
212
|
export const RESERVED_AFTER_TABLE = new Set([
|
|
213
213
|
'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET', 'JOIN', 'INNER',
|
|
214
|
-
'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON',
|
|
214
|
+
'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON', 'POSITIONAL',
|
|
215
215
|
])
|