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 CHANGED
@@ -10,7 +10,7 @@
10
10
  ![coverage](https://img.shields.io/badge/Coverage-95-darkred)
11
11
  [![dependencies](https://img.shields.io/badge/Dependencies-0-blueviolet)](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
12
12
 
13
- Squirreling is a streaming async SQL engine for JavaScript. It is designed to provide efficient streaming of results from pluggable backends for highly efficient retrieval of data for browser applications.
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.1",
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": "24.10.4",
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",
@@ -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
- yield* hashJoin({
47
- leftRows: leftSource.scan(options), // Stream directly, not buffered
48
- rightRows,
49
- join,
50
- leftTable: currentLeftTable,
51
- rightTable,
52
- tables,
53
- functions,
54
- signal,
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 = hashJoin({
88
- leftRows,
89
- rightRows,
90
- join,
91
- leftTable: currentLeftTable,
92
- rightTable,
93
- tables,
94
- functions,
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
- yield* hashJoin({
125
- leftRows,
126
- rightRows,
127
- join,
128
- leftTable: currentLeftTable,
129
- rightTable,
130
- tables,
131
- functions,
132
- signal,
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'
@@ -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
- expect(state, 'keyword', 'ON')
66
- const condition = parseExpression(state)
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,
@@ -1,4 +1,4 @@
1
- import { tokenize } from './tokenize.js'
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 = tokenize(query)
16
+ const tokens = tokenizeSql(query)
17
17
  /** @type {ParserState} */
18
18
  const state = { tokens, pos: 0, functions }
19
19
  const select = parseSelectInternal(state)
@@ -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 tokenize(sql) {
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
  ])