squirreling 0.16.7 → 0.16.9
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 -1
- package/package.json +7 -7
- package/src/ast.d.ts +6 -1
- package/src/backend/batch.js +1 -0
- package/src/execute/aggregates.js +3 -0
- package/src/execute/batches.js +5 -1
- package/src/execute/execute.js +30 -0
- package/src/execute/streamingAggregate.js +19 -0
- package/src/expression/batch.js +7 -0
- package/src/index.d.ts +1 -0
- package/src/parse/extractTables.js +4 -0
- package/src/parse/parse.js +11 -0
- package/src/plan/columns.js +6 -1
- package/src/plan/plan.js +3 -0
- package/src/plan/types.d.ts +6 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
Squirreling is a streaming async SQL engine in pure JavaScript. Built for the browser from the ground up: streaming input and output, pluggable data sources, and lazy async cell evaluation. This makes Squirreling ideal for querying data from network sources, APIs, or LLMs where latency and cost matter.
|
|
14
14
|
|
|
15
|
-
>
|
|
15
|
+
> Powers **[HypAware](https://hypaware.ai/?utm_source=github-readme)**, the improvement loop for all your agents.
|
|
16
16
|
|
|
17
17
|
- **Standard SQL**: Full SQL support for querying data (read-only)
|
|
18
18
|
- **Async UDFs**: User-defined functions can call APIs or models
|
|
@@ -146,6 +146,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
146
146
|
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`, `CROSS JOIN`, `POSITIONAL JOIN`, `LATERAL VIEW [OUTER] EXPLODE(...)`, with `ON` or `USING (col, ...)` conditions
|
|
147
147
|
- `GROUP BY` and `HAVING` clauses, including `GROUP BY ALL`
|
|
148
148
|
- Set operations: `UNION`, `UNION ALL`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, `EXCEPT ALL`
|
|
149
|
+
- `SHOW TABLES` to list the available tables
|
|
149
150
|
- Expressions: `CASE`, `CAST`, `TRY_CAST`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`, `IS NOT NULL`, string concatenation `||`
|
|
150
151
|
- Subscript access: zero-based array indexing `col[0]`, struct field access `col['field']`, and chains like `col[0].field`
|
|
151
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.9",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@eslint/js": "10.0.1",
|
|
43
|
-
"@types/node": "26.6.
|
|
44
|
-
"@vitest/coverage-v8": "5.0.
|
|
45
|
-
"eslint": "10.
|
|
46
|
-
"eslint-plugin-jsdoc": "
|
|
47
|
-
"globals": "17.
|
|
43
|
+
"@types/node": "26.6.5",
|
|
44
|
+
"@vitest/coverage-v8": "5.0.4",
|
|
45
|
+
"eslint": "10.12.0",
|
|
46
|
+
"eslint-plugin-jsdoc": "65.3.4",
|
|
47
|
+
"globals": "17.13.0",
|
|
48
48
|
"typescript": "7.0.2",
|
|
49
|
-
"vitest": "5.0.
|
|
49
|
+
"vitest": "5.0.4"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/ast.d.ts
CHANGED
|
@@ -41,7 +41,12 @@ export interface WithStatement extends AstBase {
|
|
|
41
41
|
query: Statement
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// SHOW TABLES statement: lists the names of the available tables
|
|
45
|
+
export interface ShowTablesStatement extends AstBase {
|
|
46
|
+
type: 'show tables'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Statement = SelectStatement | SetOperationStatement | WithStatement | ShowTablesStatement
|
|
45
50
|
|
|
46
51
|
export interface CTEDefinition extends AstBase {
|
|
47
52
|
name: string
|
package/src/backend/batch.js
CHANGED
|
@@ -125,6 +125,7 @@ export function selectVector(vector, selection) {
|
|
|
125
125
|
throw new Error(`Cannot select ${selection.length} rows from vector length ${vector.length}`)
|
|
126
126
|
}
|
|
127
127
|
if (selection.type === 'all') return vector
|
|
128
|
+
if (vector.type === 'constant') return { ...vector, length: selectedRowCount(selection) }
|
|
128
129
|
if (vector.type === 'selected') {
|
|
129
130
|
const composed = composeSelections(vector.selection, selection)
|
|
130
131
|
return {
|
|
@@ -309,6 +309,9 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
309
309
|
specs.push(spec)
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// Prepared batches can count selected rows without decoding a physical column.
|
|
313
|
+
if (table.prepareScan && specs.every(spec => spec.funcName === 'COUNT' && spec.star)) return
|
|
314
|
+
|
|
312
315
|
const physicalColumns = new Set(specs.map(spec => spec.column))
|
|
313
316
|
const hasPushdown = where || limit !== undefined || offset !== undefined
|
|
314
317
|
if (hasPushdown) {
|
package/src/execute/batches.js
CHANGED
|
@@ -238,12 +238,16 @@ function projectedBatchColumn(batch, columnIndex) {
|
|
|
238
238
|
/**
|
|
239
239
|
* @param {ColumnVector} predicate
|
|
240
240
|
* @param {number} rowCount
|
|
241
|
-
* @returns {{ selection:
|
|
241
|
+
* @returns {{ selection: RowSelection, selectedCount: number }}
|
|
242
242
|
*/
|
|
243
243
|
function predicateSelection(predicate, rowCount) {
|
|
244
244
|
if (predicate.length !== rowCount) {
|
|
245
245
|
throw new Error(`Predicate returned ${predicate.length} rows, expected ${rowCount}`)
|
|
246
246
|
}
|
|
247
|
+
if (predicate.type === 'constant') {
|
|
248
|
+
const selectedCount = predicate.value ? rowCount : 0
|
|
249
|
+
return { selection: { type: 'range', start: 0, end: selectedCount, length: rowCount }, selectedCount }
|
|
250
|
+
}
|
|
247
251
|
const indices = new Uint32Array(rowCount)
|
|
248
252
|
let selectedCount = 0
|
|
249
253
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
package/src/execute/execute.js
CHANGED
|
@@ -37,6 +37,9 @@ const YIELD_INTERVAL = 4000
|
|
|
37
37
|
export function executeSql({ tables, query, functions, signal }) {
|
|
38
38
|
const parsed = typeof query === 'string' ? parseSql({ query, functions }) : query
|
|
39
39
|
|
|
40
|
+
// Listing names does not require table contents or inferred schemas.
|
|
41
|
+
if (parsed.type === 'show tables') return executeShowTables({ tables, signal })
|
|
42
|
+
|
|
40
43
|
// Normalize tables: convert arrays to AsyncDataSource
|
|
41
44
|
/** @type {Record<string, AsyncDataSource>} */
|
|
42
45
|
const normalizedTables = {}
|
|
@@ -97,6 +100,8 @@ export function executePlan({ plan, context }) {
|
|
|
97
100
|
return executeScan(plan, context)
|
|
98
101
|
} else if (plan.type === 'SingleRow') {
|
|
99
102
|
return executeSingleRow()
|
|
103
|
+
} else if (plan.type === 'ShowTables') {
|
|
104
|
+
return executeShowTables(context)
|
|
100
105
|
} else if (plan.type === 'Count') {
|
|
101
106
|
return executeCount(plan, context)
|
|
102
107
|
} else if (plan.type === 'Filter') {
|
|
@@ -148,6 +153,31 @@ function executeSingleRow() {
|
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Executes SHOW TABLES: yields one row per available table name, sorted.
|
|
158
|
+
*
|
|
159
|
+
* @param {Pick<ExecuteSqlOptions, 'tables' | 'signal'>} context
|
|
160
|
+
* @returns {QueryResults}
|
|
161
|
+
*/
|
|
162
|
+
function executeShowTables(context) {
|
|
163
|
+
const columns = ['name']
|
|
164
|
+
const names = Object.keys(context.tables).sort()
|
|
165
|
+
return {
|
|
166
|
+
columns,
|
|
167
|
+
numRows: names.length,
|
|
168
|
+
maxRows: names.length,
|
|
169
|
+
async *rows() {
|
|
170
|
+
for (const name of names) {
|
|
171
|
+
context.signal?.throwIfAborted()
|
|
172
|
+
yield {
|
|
173
|
+
columns,
|
|
174
|
+
cells: { name: () => Promise.resolve(name) },
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
151
181
|
/**
|
|
152
182
|
* Executes a table-valued function (e.g. UNNEST, JSON_EACH).
|
|
153
183
|
* Evaluates the argument once against the outer row (for lateral joins) or an
|
|
@@ -523,6 +523,25 @@ async function accumulateBatch({ batch, inputs, specs, groups, context, rowOffse
|
|
|
523
523
|
evaluateBatchInputs(inputs.args, batch, context, rowOffset),
|
|
524
524
|
])
|
|
525
525
|
const rowCount = selectedRowCount(batch.selection)
|
|
526
|
+
context.signal?.throwIfAborted()
|
|
527
|
+
if (rowCount > 0 && keys.every(vector => vector?.type === 'constant') &&
|
|
528
|
+
specs.every(spec => spec.star && spec.funcName === 'COUNT' && !spec.node.distinct && !spec.node.filter)) {
|
|
529
|
+
const keyValues = keys.map(vector => valueAt(/** @type {ColumnVector} */ vector, 0))
|
|
530
|
+
const key = keyValues.length === 0
|
|
531
|
+
? true
|
|
532
|
+
: keyValues.length === 1 ? keyify(keyValues[0]) : keyify(...keyValues)
|
|
533
|
+
let group = groups.get(key)
|
|
534
|
+
if (!group) {
|
|
535
|
+
group = {
|
|
536
|
+
firstRow: undefined,
|
|
537
|
+
keyValues,
|
|
538
|
+
accumulators: specs.map(spec => newAccumulator(spec.funcName, spec.node.distinct)),
|
|
539
|
+
}
|
|
540
|
+
groups.set(key, group)
|
|
541
|
+
}
|
|
542
|
+
for (const accumulator of group.accumulators) accumulator.count += rowCount
|
|
543
|
+
return
|
|
544
|
+
}
|
|
526
545
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
527
546
|
if (rowIndex > 0 && rowIndex % CHUNK_SIZE === 0) {
|
|
528
547
|
await yieldToEventLoop()
|
package/src/expression/batch.js
CHANGED
|
@@ -626,6 +626,13 @@ function resolveIdentifier(identifier, columns) {
|
|
|
626
626
|
*/
|
|
627
627
|
function evaluateKernel(kernel, vectors, selection, signal, rowOffset = 0, rowOrdinals) {
|
|
628
628
|
const length = selectedRowCount(selection)
|
|
629
|
+
// Supported kernels are deterministic; streamRowIndex only labels errors.
|
|
630
|
+
// Empty selections must not evaluate expressions (including invalid casts).
|
|
631
|
+
if (length > 0 && vectors.every(vector => vector.type === 'constant')) {
|
|
632
|
+
signal?.throwIfAborted()
|
|
633
|
+
const ordinal = rowOrdinals ? Number(valueAt(rowOrdinals, 0)) : 0
|
|
634
|
+
return { type: 'constant', value: kernel(vectors, 0, rowOffset + ordinal), length }
|
|
635
|
+
}
|
|
629
636
|
if (signal && length > YIELD_INTERVAL) {
|
|
630
637
|
return evaluateKernelAsync(kernel, vectors, length, signal, rowOffset, rowOrdinals)
|
|
631
638
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ function walkStatement(stmt, cteScope, refs) {
|
|
|
46
46
|
for (const o of stmt.orderBy) walkExpr(o.expr, cteScope, refs)
|
|
47
47
|
return
|
|
48
48
|
}
|
|
49
|
+
if (stmt.type === 'show tables') {
|
|
50
|
+
// SHOW TABLES references no source tables
|
|
51
|
+
return
|
|
52
|
+
}
|
|
49
53
|
// select
|
|
50
54
|
if (!stmt.from) {
|
|
51
55
|
// FROM-less SELECT (e.g. `SELECT 1`), no source tables
|
package/src/parse/parse.js
CHANGED
|
@@ -47,6 +47,17 @@ export function parseSql({ query, functions }) {
|
|
|
47
47
|
*/
|
|
48
48
|
export function parseStatement(state) {
|
|
49
49
|
const positionStart = state.lastPos
|
|
50
|
+
const showTok = current(state)
|
|
51
|
+
if (showTok.type === 'identifier' && showTok.value.toUpperCase() === 'SHOW') {
|
|
52
|
+
consume(state)
|
|
53
|
+
const tok = current(state)
|
|
54
|
+
if (tok.type !== 'identifier' || tok.value.toUpperCase() !== 'TABLES') {
|
|
55
|
+
throw parseError(state, 'TABLES')
|
|
56
|
+
}
|
|
57
|
+
consume(state)
|
|
58
|
+
match(state, 'semicolon')
|
|
59
|
+
return { type: 'show tables', positionStart: showTok.positionStart, positionEnd: state.lastPos }
|
|
60
|
+
}
|
|
50
61
|
if (match(state, 'keyword', 'WITH')) {
|
|
51
62
|
const recursiveTok = current(state)
|
|
52
63
|
if (recursiveTok.type === 'identifier' && recursiveTok.value.toUpperCase() === 'RECURSIVE') {
|
package/src/plan/columns.js
CHANGED
|
@@ -40,7 +40,7 @@ export function fromAlias(from) {
|
|
|
40
40
|
*/
|
|
41
41
|
export function statementScope(stmt) {
|
|
42
42
|
if (stmt.type === 'with') return statementScope(stmt.query)
|
|
43
|
-
if (stmt.type === 'compound') return undefined
|
|
43
|
+
if (stmt.type === 'compound' || stmt.type === 'show tables') return undefined
|
|
44
44
|
const alias = fromAlias(stmt.from)
|
|
45
45
|
const joins = stmt.joins.map(j => j.alias ?? j.table)
|
|
46
46
|
return alias === undefined ? joins : [alias, ...joins]
|
|
@@ -329,6 +329,7 @@ export function collectColumnsFromExpr(expr, columns, aliases, context) {
|
|
|
329
329
|
* @param {ColumnCollectionContext} [context]
|
|
330
330
|
*/
|
|
331
331
|
function collectCorrelatedColumnsFromStatement(stmt, columns, context) {
|
|
332
|
+
if (stmt.type === 'show tables') return
|
|
332
333
|
if (stmt.type === 'compound') {
|
|
333
334
|
collectCorrelatedColumnsFromStatement(stmt.left, columns, context)
|
|
334
335
|
collectCorrelatedColumnsFromStatement(stmt.right, columns, context)
|
|
@@ -386,6 +387,7 @@ function collectCorrelatedColumnsFromStatement(stmt, columns, context) {
|
|
|
386
387
|
* @param {ColumnCollectionContext} [context]
|
|
387
388
|
*/
|
|
388
389
|
function collectFromFunctionArgs(stmt, columns, context) {
|
|
390
|
+
if (stmt.type === 'show tables') return
|
|
389
391
|
if (stmt.type === 'compound') {
|
|
390
392
|
collectFromFunctionArgs(stmt.left, columns, context)
|
|
391
393
|
collectFromFunctionArgs(stmt.right, columns, context)
|
|
@@ -414,6 +416,9 @@ function collectFromFunctionArgs(stmt, columns, context) {
|
|
|
414
416
|
* @returns {string[]}
|
|
415
417
|
*/
|
|
416
418
|
export function inferStatementColumns({ stmt, cteColumns, tables }) {
|
|
419
|
+
if (stmt.type === 'show tables') {
|
|
420
|
+
return ['name']
|
|
421
|
+
}
|
|
417
422
|
if (stmt.type === 'with') {
|
|
418
423
|
return inferStatementColumns({ stmt: stmt.query, cteColumns, tables })
|
|
419
424
|
}
|
package/src/plan/plan.js
CHANGED
|
@@ -38,6 +38,9 @@ export function planSql({ query, functions, tables, ctePlans, cteColumns }) {
|
|
|
38
38
|
* @returns {QueryPlan}
|
|
39
39
|
*/
|
|
40
40
|
export function planStatement({ stmt, ctePlans, cteColumns, tables, parentColumns, outerScope }) {
|
|
41
|
+
if (stmt.type === 'show tables') {
|
|
42
|
+
return { type: 'ShowTables' }
|
|
43
|
+
}
|
|
41
44
|
if (stmt.type === 'with') {
|
|
42
45
|
// Build CTE plans in order (each CTE can reference preceding CTEs)
|
|
43
46
|
ctePlans ??= new Map()
|
package/src/plan/types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type QueryPlan =
|
|
|
15
15
|
| NestedLoopJoinNode
|
|
16
16
|
| PositionalJoinNode
|
|
17
17
|
| SetOperationNode
|
|
18
|
+
| ShowTablesNode
|
|
18
19
|
| SubqueryNode
|
|
19
20
|
| TableFunctionNode
|
|
20
21
|
| WindowNode
|
|
@@ -35,6 +36,11 @@ export interface SingleRowNode {
|
|
|
35
36
|
type: 'SingleRow'
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// SHOW TABLES: yields one row per available table name
|
|
40
|
+
export interface ShowTablesNode {
|
|
41
|
+
type: 'ShowTables'
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
// Count node for COUNT(*) optimization
|
|
39
45
|
export interface CountNode {
|
|
40
46
|
type: 'Count'
|