squirreling 0.9.5 → 0.10.1
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 +4 -0
- package/package.json +5 -3
- package/src/ast.d.ts +184 -0
- package/src/backend/dataSource.js +32 -8
- package/src/execute/aggregates.js +10 -2
- package/src/execute/execute.js +23 -20
- package/src/execute/join.js +8 -7
- package/src/expression/alias.js +2 -2
- package/src/expression/evaluate.js +21 -57
- package/src/expression/regexp.js +13 -25
- package/src/expression/strings.js +14 -22
- package/src/index.d.ts +1 -0
- package/src/parse/comparison.js +2 -2
- package/src/parse/expression.js +21 -10
- package/src/parse/functions.js +14 -23
- package/src/parse/joins.js +5 -2
- package/src/parse/parse.js +5 -4
- package/src/parse/state.js +2 -2
- package/src/parse/tokenize.js +2 -9
- package/src/parse/types.d.ts +1 -1
- package/src/plan/columns.js +36 -18
- package/src/plan/plan.js +45 -12
- package/src/spatial/bbox.js +1 -1
- package/src/types.d.ts +13 -191
- package/src/validation/aggregates.js +67 -0
- package/src/validation/executionErrors.js +35 -0
- package/src/validation/expressionErrors.js +57 -0
- package/src/validation/functions.js +281 -0
- package/src/{parseErrors.js → validation/parseErrors.js} +21 -57
- package/src/validation/planErrors.js +42 -0
- package/src/executionErrors.js +0 -80
- package/src/validation.js +0 -343
- package/src/validationErrors.js +0 -141
package/README.md
CHANGED
|
@@ -91,6 +91,8 @@ Squirreling can work with any data source that implements the `AsyncDataSource`
|
|
|
91
91
|
|
|
92
92
|
```typescript
|
|
93
93
|
interface AsyncDataSource {
|
|
94
|
+
numRows?: number
|
|
95
|
+
columns: string[]
|
|
94
96
|
scan(options: ScanOptions): ScanResults
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -113,6 +115,8 @@ The `scan()` method returns a `ScanResults` object containing a row stream and f
|
|
|
113
115
|
|
|
114
116
|
```typescript
|
|
115
117
|
const customSource: AsyncDataSource = {
|
|
118
|
+
numRows: 1000000, // optional total row count for planning
|
|
119
|
+
columns: ['id', 'name', 'active'], // columns available in this source
|
|
116
120
|
scan({ columns, where, limit, offset, signal }) {
|
|
117
121
|
// Use hints to optimize your scan, or ignore them
|
|
118
122
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"dataset",
|
|
11
11
|
"hyperparam",
|
|
12
12
|
"hyparquet",
|
|
13
|
-
"parquet"
|
|
13
|
+
"parquet",
|
|
14
|
+
"query",
|
|
15
|
+
"relational"
|
|
14
16
|
],
|
|
15
17
|
"license": "MIT",
|
|
16
18
|
"repository": {
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
"test": "vitest run"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.
|
|
42
|
+
"@types/node": "25.4.0",
|
|
41
43
|
"@vitest/coverage-v8": "4.0.18",
|
|
42
44
|
"eslint": "9.39.2",
|
|
43
45
|
"eslint-plugin-jsdoc": "62.7.1",
|
package/src/ast.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export type SqlPrimitive =
|
|
2
|
+
| string
|
|
3
|
+
| number
|
|
4
|
+
| bigint
|
|
5
|
+
| boolean
|
|
6
|
+
| Date
|
|
7
|
+
| null
|
|
8
|
+
| SqlPrimitive[]
|
|
9
|
+
| Record<string, any>
|
|
10
|
+
|
|
11
|
+
export interface SelectStatement {
|
|
12
|
+
with?: WithClause
|
|
13
|
+
distinct: boolean
|
|
14
|
+
columns: SelectColumn[]
|
|
15
|
+
from: FromTable | FromSubquery
|
|
16
|
+
joins: JoinClause[]
|
|
17
|
+
where?: ExprNode
|
|
18
|
+
groupBy: ExprNode[]
|
|
19
|
+
having?: ExprNode
|
|
20
|
+
orderBy: OrderByItem[]
|
|
21
|
+
limit?: number
|
|
22
|
+
offset?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WithClause {
|
|
26
|
+
ctes: CTEDefinition[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CTEDefinition {
|
|
30
|
+
name: string
|
|
31
|
+
query: SelectStatement
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FromTable extends AstBase {
|
|
35
|
+
kind: 'table'
|
|
36
|
+
table: string
|
|
37
|
+
alias?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FromSubquery {
|
|
41
|
+
kind: 'subquery'
|
|
42
|
+
query: SelectStatement
|
|
43
|
+
alias: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
|
|
47
|
+
|
|
48
|
+
export type BinaryOp = 'AND' | 'OR' | 'LIKE' | ComparisonOp | ArithmeticOp
|
|
49
|
+
|
|
50
|
+
export type ComparisonOp = '=' | '!=' | '<>' | '<' | '>' | '<=' | '>='
|
|
51
|
+
|
|
52
|
+
export interface LiteralNode extends AstBase {
|
|
53
|
+
type: 'literal'
|
|
54
|
+
value: SqlPrimitive
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IdentifierNode extends AstBase {
|
|
58
|
+
type: 'identifier'
|
|
59
|
+
name: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UnaryNode extends AstBase {
|
|
63
|
+
type: 'unary'
|
|
64
|
+
op: 'NOT' | 'IS NULL' | 'IS NOT NULL' | '-'
|
|
65
|
+
argument: ExprNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BinaryNode extends AstBase {
|
|
69
|
+
type: 'binary'
|
|
70
|
+
op: BinaryOp
|
|
71
|
+
left: ExprNode
|
|
72
|
+
right: ExprNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FunctionNode extends AstBase {
|
|
76
|
+
type: 'function'
|
|
77
|
+
funcName: string
|
|
78
|
+
args: ExprNode[]
|
|
79
|
+
distinct?: boolean
|
|
80
|
+
filter?: ExprNode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type CastType = 'TEXT' | 'STRING' | 'VARCHAR' | 'INTEGER' | 'INT' | 'BIGINT' | 'FLOAT' | 'REAL' | 'DOUBLE' | 'BOOLEAN' | 'BOOL'
|
|
84
|
+
|
|
85
|
+
export interface CastNode extends AstBase {
|
|
86
|
+
type: 'cast'
|
|
87
|
+
expr: ExprNode
|
|
88
|
+
toType: CastType
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface InSubqueryNode extends AstBase {
|
|
92
|
+
type: 'in'
|
|
93
|
+
expr: ExprNode
|
|
94
|
+
subquery: SelectStatement
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface InValuesNode extends AstBase {
|
|
98
|
+
type: 'in valuelist'
|
|
99
|
+
expr: ExprNode
|
|
100
|
+
values: ExprNode[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ExistsNode extends AstBase {
|
|
104
|
+
type: 'exists' | 'not exists'
|
|
105
|
+
subquery: SelectStatement
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WhenClause extends AstBase {
|
|
109
|
+
condition: ExprNode
|
|
110
|
+
result: ExprNode
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CaseNode extends AstBase {
|
|
114
|
+
type: 'case'
|
|
115
|
+
caseExpr?: ExprNode
|
|
116
|
+
whenClauses: WhenClause[]
|
|
117
|
+
elseResult?: ExprNode
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SubqueryNode extends AstBase {
|
|
121
|
+
type: 'subquery'
|
|
122
|
+
subquery: SelectStatement
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type IntervalUnit = 'DAY' | 'MONTH' | 'YEAR' | 'HOUR' | 'MINUTE' | 'SECOND'
|
|
126
|
+
|
|
127
|
+
export interface IntervalNode extends AstBase {
|
|
128
|
+
type: 'interval'
|
|
129
|
+
value: number
|
|
130
|
+
unit: IntervalUnit
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface StarNode extends AstBase {
|
|
134
|
+
type: 'star'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type ExprNode =
|
|
138
|
+
| LiteralNode
|
|
139
|
+
| IdentifierNode
|
|
140
|
+
| UnaryNode
|
|
141
|
+
| BinaryNode
|
|
142
|
+
| FunctionNode
|
|
143
|
+
| CastNode
|
|
144
|
+
| InSubqueryNode
|
|
145
|
+
| InValuesNode
|
|
146
|
+
| ExistsNode
|
|
147
|
+
| CaseNode
|
|
148
|
+
| SubqueryNode
|
|
149
|
+
| IntervalNode
|
|
150
|
+
| StarNode
|
|
151
|
+
|
|
152
|
+
export interface StarColumn {
|
|
153
|
+
kind: 'star'
|
|
154
|
+
table?: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface DerivedColumn {
|
|
158
|
+
kind: 'derived'
|
|
159
|
+
expr: ExprNode
|
|
160
|
+
alias?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type SelectColumn = StarColumn | DerivedColumn
|
|
164
|
+
|
|
165
|
+
export interface OrderByItem {
|
|
166
|
+
expr: ExprNode
|
|
167
|
+
direction: 'ASC' | 'DESC'
|
|
168
|
+
nulls?: 'FIRST' | 'LAST'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'CROSS' | 'POSITIONAL'
|
|
172
|
+
|
|
173
|
+
export interface JoinClause extends AstBase {
|
|
174
|
+
joinType: JoinType
|
|
175
|
+
table: string
|
|
176
|
+
alias?: string
|
|
177
|
+
on?: ExprNode
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// All AST node derive from this base, which includes position info for error reporting and other purposes
|
|
181
|
+
interface AstBase {
|
|
182
|
+
positionStart: number // start position in query (0-based, inclusive)
|
|
183
|
+
positionEnd: number // end position in query (0-based, exclusive)
|
|
184
|
+
}
|
|
@@ -6,27 +6,50 @@
|
|
|
6
6
|
* Creates an async row accessor that wraps a plain JavaScript object
|
|
7
7
|
*
|
|
8
8
|
* @param {Record<string, SqlPrimitive>} obj - the plain object
|
|
9
|
+
* @param {string[]} columns - list of column names (keys in the object)
|
|
9
10
|
* @returns {AsyncRow} a row accessor interface
|
|
10
11
|
*/
|
|
11
|
-
export function asyncRow(obj) {
|
|
12
|
+
export function asyncRow(obj, columns) {
|
|
12
13
|
/** @type {AsyncCells} */
|
|
13
14
|
const cells = {}
|
|
14
|
-
for (const
|
|
15
|
-
cells[key] = () => Promise.resolve(
|
|
15
|
+
for (const key of columns) {
|
|
16
|
+
cells[key] = () => Promise.resolve(obj[key])
|
|
16
17
|
}
|
|
17
|
-
return { columns
|
|
18
|
+
return { columns, cells }
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Creates an async memory-backed data source from an array of plain objects
|
|
22
23
|
*
|
|
23
|
-
* @param {
|
|
24
|
+
* @param {Object} options
|
|
25
|
+
* @param {Record<string, SqlPrimitive>[]} options.data - array of plain objects
|
|
26
|
+
* @param {string[]} [options.columns] - optional list of column names (if not provided, inferred from first row)
|
|
24
27
|
* @returns {AsyncDataSource} an async data source interface
|
|
25
28
|
*/
|
|
26
|
-
export function memorySource(data) {
|
|
29
|
+
export function memorySource({ data, columns }) {
|
|
30
|
+
if (!columns) {
|
|
31
|
+
// Columns not provided, infer from data
|
|
32
|
+
if (!data.length) {
|
|
33
|
+
throw new Error('Unknown columns: data is empty and no columns provided')
|
|
34
|
+
}
|
|
35
|
+
columns = Object.keys(data[0])
|
|
36
|
+
// Check first 1000 rows for consistent columns
|
|
37
|
+
for (let i = 1; i < data.length && i < 1000; i++) {
|
|
38
|
+
const rowColumns = Object.keys(data[i])
|
|
39
|
+
const missing = columns.find(col => !rowColumns.includes(col))
|
|
40
|
+
if (missing) {
|
|
41
|
+
throw new Error(`Inconsistent data, column "${missing}" not found in row ${i}`)
|
|
42
|
+
}
|
|
43
|
+
const extra = rowColumns.find(col => !columns.includes(col))
|
|
44
|
+
if (extra) {
|
|
45
|
+
throw new Error(`Inconsistent data, unexpected column "${extra}" found in row ${i}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
27
49
|
return {
|
|
28
50
|
numRows: data.length,
|
|
29
|
-
|
|
51
|
+
columns,
|
|
52
|
+
scan({ columns: scanColumns, where, limit, offset, signal }) {
|
|
30
53
|
// Only apply offset and limit if no where clause
|
|
31
54
|
const start = !where ? offset ?? 0 : 0
|
|
32
55
|
const end = !where && limit !== undefined ? start + limit : data.length
|
|
@@ -34,7 +57,7 @@ export function memorySource(data) {
|
|
|
34
57
|
rows: (async function* () {
|
|
35
58
|
for (let i = start; i < end && i < data.length; i++) {
|
|
36
59
|
if (signal?.aborted) break
|
|
37
|
-
yield asyncRow(data[i])
|
|
60
|
+
yield asyncRow(data[i], scanColumns ?? columns)
|
|
38
61
|
}
|
|
39
62
|
})(),
|
|
40
63
|
appliedWhere: false,
|
|
@@ -53,6 +76,7 @@ export function cachedDataSource(source) {
|
|
|
53
76
|
/** @type {Map<string, Promise<SqlPrimitive>>} */
|
|
54
77
|
const cache = new Map()
|
|
55
78
|
return {
|
|
79
|
+
...source,
|
|
56
80
|
scan(options) {
|
|
57
81
|
// Does re-run the scan, but cache avoids re-computing expensive async cells
|
|
58
82
|
// TODO: check cache first to avoid re-scanning when possible
|
|
@@ -91,7 +91,11 @@ export async function* executeHashAggregate(plan, context) {
|
|
|
91
91
|
|
|
92
92
|
// Apply HAVING filter
|
|
93
93
|
if (plan.having) {
|
|
94
|
-
|
|
94
|
+
/** @type {AsyncRow} */
|
|
95
|
+
const havingRow = {
|
|
96
|
+
columns: [...group[0].columns, ...asyncRow.columns],
|
|
97
|
+
cells: { ...group[0].cells, ...asyncRow.cells },
|
|
98
|
+
}
|
|
95
99
|
const passes = await evaluateExpr({
|
|
96
100
|
node: plan.having,
|
|
97
101
|
row: havingRow,
|
|
@@ -125,7 +129,11 @@ export async function* executeScalarAggregate(plan, context) {
|
|
|
125
129
|
|
|
126
130
|
// Apply HAVING filter
|
|
127
131
|
if (plan.having) {
|
|
128
|
-
|
|
132
|
+
/** @type {AsyncRow} */
|
|
133
|
+
const havingRow = {
|
|
134
|
+
columns: [...group[0].columns, ...asyncRow.columns],
|
|
135
|
+
cells: { ...group[0].cells, ...asyncRow.cells },
|
|
136
|
+
}
|
|
129
137
|
const passes = await evaluateExpr({
|
|
130
138
|
node: plan.having,
|
|
131
139
|
row: havingRow,
|
package/src/execute/execute.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { memorySource } from '../backend/dataSource.js'
|
|
2
|
-
import { tableNotFoundError } from '../executionErrors.js'
|
|
3
2
|
import { derivedAlias } from '../expression/alias.js'
|
|
4
3
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
5
4
|
import { parseSql } from '../parse/parse.js'
|
|
6
5
|
import { planSql } from '../plan/plan.js'
|
|
6
|
+
import { tableNotFoundError } from '../validation/planErrors.js'
|
|
7
7
|
import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
|
|
8
8
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
9
9
|
import { executeSort } from './sort.js'
|
|
@@ -26,11 +26,11 @@ export async function* executeSql({ tables, query, functions, signal }) {
|
|
|
26
26
|
// Normalize tables: convert arrays to AsyncDataSource
|
|
27
27
|
/** @type {Record<string, AsyncDataSource>} */
|
|
28
28
|
const normalizedTables = {}
|
|
29
|
-
for (const [name,
|
|
30
|
-
if (Array.isArray(
|
|
31
|
-
normalizedTables[name] = memorySource(
|
|
29
|
+
for (const [name, data] of Object.entries(tables)) {
|
|
30
|
+
if (Array.isArray(data)) {
|
|
31
|
+
normalizedTables[name] = memorySource({ data })
|
|
32
32
|
} else {
|
|
33
|
-
normalizedTables[name] =
|
|
33
|
+
normalizedTables[name] = data
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -46,7 +46,7 @@ export async function* executeSql({ tables, query, functions, signal }) {
|
|
|
46
46
|
* @yields {AsyncRow}
|
|
47
47
|
*/
|
|
48
48
|
export async function* executeSelect({ select, context }) {
|
|
49
|
-
const plan = planSql({ query: select, functions: context.functions })
|
|
49
|
+
const plan = planSql({ query: select, functions: context.functions, tables: context.tables })
|
|
50
50
|
yield* executePlan({ plan, context })
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -95,16 +95,19 @@ export async function* executePlan({ plan, context }) {
|
|
|
95
95
|
*/
|
|
96
96
|
async function* executeScan(plan, context) {
|
|
97
97
|
const { tables, signal } = context
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
// check table
|
|
99
|
+
const table = tables[plan.table]
|
|
100
|
+
if (!table) {
|
|
101
|
+
throw tableNotFoundError({ table: plan.table, tables })
|
|
101
102
|
}
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
throw new Error(`
|
|
103
|
+
// check columns
|
|
104
|
+
const missingColumn = plan.hints.columns?.find(col => !table.columns.includes(col))
|
|
105
|
+
if (missingColumn) {
|
|
106
|
+
throw new Error(`Column "${missingColumn}" not found. Available columns: ${table.columns.join(', ') || '[]'}`)
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
|
|
109
|
+
// do the scan
|
|
110
|
+
const { rows, appliedWhere, appliedLimitOffset } = table.scan({ ...plan.hints, signal })
|
|
108
111
|
|
|
109
112
|
// Applied limit/offset without applied where is invalid
|
|
110
113
|
const hasLimitOffset = plan.hints.limit !== undefined || plan.hints.offset // 0 offset is noop
|
|
@@ -135,17 +138,17 @@ async function* executeScan(plan, context) {
|
|
|
135
138
|
* @yields {AsyncRow}
|
|
136
139
|
*/
|
|
137
140
|
async function* executeCount(plan, { tables, signal }) {
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
throw tableNotFoundError({
|
|
141
|
+
const table = tables[plan.table]
|
|
142
|
+
if (!table) {
|
|
143
|
+
throw tableNotFoundError({ table: plan.table, tables })
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
// Use source numRows if available
|
|
144
|
-
let count =
|
|
145
|
-
if (
|
|
147
|
+
let count = table.numRows
|
|
148
|
+
if (table.numRows === undefined) {
|
|
146
149
|
// Fall back to counting rows via scan
|
|
147
150
|
count = 0
|
|
148
|
-
const { rows } =
|
|
151
|
+
const { rows } = table.scan({ signal })
|
|
149
152
|
// eslint-disable-next-line no-unused-vars
|
|
150
153
|
for await (const _ of rows) {
|
|
151
154
|
if (signal?.aborted) return
|
package/src/execute/join.js
CHANGED
|
@@ -203,16 +203,16 @@ export async function* executeHashJoin(plan, context) {
|
|
|
203
203
|
/**
|
|
204
204
|
* Creates a NULL-filled row with the given column names
|
|
205
205
|
*
|
|
206
|
-
* @param {string[]}
|
|
206
|
+
* @param {string[]} columns
|
|
207
207
|
* @returns {AsyncRow}
|
|
208
208
|
*/
|
|
209
|
-
function createNullRow(
|
|
209
|
+
function createNullRow(columns) {
|
|
210
210
|
/** @type {AsyncCells} */
|
|
211
211
|
const cells = {}
|
|
212
|
-
for (const col of
|
|
212
|
+
for (const col of columns) {
|
|
213
213
|
cells[col] = () => Promise.resolve(null)
|
|
214
214
|
}
|
|
215
|
-
return { columns
|
|
215
|
+
return { columns, cells }
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
@@ -234,6 +234,7 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
234
234
|
// Skip already-prefixed keys (from previous joins)
|
|
235
235
|
if (!key.includes('.')) {
|
|
236
236
|
const alias = `${leftTable}.${key}`
|
|
237
|
+
columns.push(alias)
|
|
237
238
|
cells[alias] = cell
|
|
238
239
|
}
|
|
239
240
|
// Also keep unqualified name for convenience
|
|
@@ -244,9 +245,9 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
244
245
|
// Add right table columns with prefix
|
|
245
246
|
for (const [key, cell] of Object.entries(rightRow.cells)) {
|
|
246
247
|
if (!key.includes('.')) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cells[
|
|
248
|
+
const alias = `${rightTable}.${key}`
|
|
249
|
+
columns.push(alias)
|
|
250
|
+
cells[alias] = cell
|
|
250
251
|
}
|
|
251
252
|
// Unqualified name (overwrites if same name exists in left table)
|
|
252
253
|
columns.push(key)
|
package/src/expression/alias.js
CHANGED
|
@@ -31,9 +31,9 @@ export function derivedAlias(expr) {
|
|
|
31
31
|
if (expr.type === 'function') {
|
|
32
32
|
// Handle aggregate functions with star (COUNT(*) -> count_all)
|
|
33
33
|
if (expr.args.length === 1 && expr.args[0].type === 'star') {
|
|
34
|
-
return expr.
|
|
34
|
+
return expr.funcName.toLowerCase() + '_all'
|
|
35
35
|
}
|
|
36
|
-
return expr.
|
|
36
|
+
return expr.funcName.toLowerCase() + '_' + expr.args.map(derivedAlias).join('_')
|
|
37
37
|
}
|
|
38
38
|
if (expr.type === 'interval') {
|
|
39
39
|
return `interval_${expr.value}_${expr.unit.toLowerCase()}`
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { executeSelect } from '../execute/execute.js'
|
|
2
2
|
import { stringify } from '../execute/utils.js'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation.js'
|
|
6
|
-
import {
|
|
3
|
+
import { invalidContextError } from '../validation/executionErrors.js'
|
|
4
|
+
import { aggregateError, argValueError, castError } from '../validation/expressionErrors.js'
|
|
5
|
+
import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
|
|
6
|
+
import { unknownFunctionError } from '../validation/parseErrors.js'
|
|
7
|
+
import { columnNotFoundError } from '../validation/planErrors.js'
|
|
7
8
|
import { derivedAlias } from './alias.js'
|
|
8
9
|
import { applyBinaryOp } from './binary.js'
|
|
9
10
|
import { applyIntervalToDate, dateTrunc, extractField } from './date.js'
|
|
@@ -47,7 +48,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
47
48
|
// Unknown identifier
|
|
48
49
|
throw columnNotFoundError({
|
|
49
50
|
columnName: node.name,
|
|
50
|
-
availableColumns:
|
|
51
|
+
availableColumns: row.columns,
|
|
51
52
|
positionStart: node.positionStart,
|
|
52
53
|
positionEnd: node.positionEnd,
|
|
53
54
|
rowIndex,
|
|
@@ -99,7 +100,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
99
100
|
|
|
100
101
|
// Function calls
|
|
101
102
|
if (node.type === 'function') {
|
|
102
|
-
const funcName = node.
|
|
103
|
+
const funcName = node.funcName.toUpperCase()
|
|
103
104
|
|
|
104
105
|
// Handle aggregate functions
|
|
105
106
|
if (isAggregateFunc(funcName)) {
|
|
@@ -110,11 +111,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
110
111
|
if (row.columns.includes(alias)) {
|
|
111
112
|
return row.cells[alias]()
|
|
112
113
|
} else {
|
|
113
|
-
throw aggregateError(
|
|
114
|
-
funcName,
|
|
115
|
-
positionStart: node.positionStart,
|
|
116
|
-
positionEnd: node.positionEnd,
|
|
117
|
-
})
|
|
114
|
+
throw aggregateError(node)
|
|
118
115
|
}
|
|
119
116
|
}
|
|
120
117
|
|
|
@@ -225,26 +222,16 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
225
222
|
}
|
|
226
223
|
|
|
227
224
|
/** @type {SqlPrimitive[]} */
|
|
228
|
-
const args =
|
|
225
|
+
const args = node.args.length === 1
|
|
226
|
+
? [await evaluateExpr({ node: node.args[0], row, rowIndex, rows, context })]
|
|
227
|
+
: await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, rowIndex, rows, context })))
|
|
229
228
|
|
|
230
229
|
if (isStringFunc(funcName)) {
|
|
231
|
-
return evaluateStringFunc({
|
|
232
|
-
funcName,
|
|
233
|
-
args,
|
|
234
|
-
positionStart: node.positionStart,
|
|
235
|
-
positionEnd: node.positionEnd,
|
|
236
|
-
rowIndex,
|
|
237
|
-
})
|
|
230
|
+
return evaluateStringFunc({ funcName, node, args, rowIndex })
|
|
238
231
|
}
|
|
239
232
|
|
|
240
233
|
if (isRegexpFunc(funcName)) {
|
|
241
|
-
return evaluateRegexpFunc({
|
|
242
|
-
funcName,
|
|
243
|
-
args,
|
|
244
|
-
positionStart: node.positionStart,
|
|
245
|
-
positionEnd: node.positionEnd,
|
|
246
|
-
rowIndex,
|
|
247
|
-
})
|
|
234
|
+
return evaluateRegexpFunc({ funcName, node, args, rowIndex })
|
|
248
235
|
}
|
|
249
236
|
|
|
250
237
|
if (isMathFunc(funcName)) {
|
|
@@ -294,10 +281,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
294
281
|
if (funcName === 'JSON_OBJECT') {
|
|
295
282
|
if (args.length % 2 !== 0) {
|
|
296
283
|
throw argValueError({
|
|
297
|
-
|
|
284
|
+
...node,
|
|
298
285
|
message: 'requires an even number of arguments (key-value pairs)',
|
|
299
|
-
positionStart: node.positionStart,
|
|
300
|
-
positionEnd: node.positionEnd,
|
|
301
286
|
rowIndex,
|
|
302
287
|
})
|
|
303
288
|
}
|
|
@@ -308,10 +293,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
308
293
|
const value = args[i + 1]
|
|
309
294
|
if (key == null) {
|
|
310
295
|
throw argValueError({
|
|
311
|
-
|
|
296
|
+
...node,
|
|
312
297
|
message: 'key cannot be null',
|
|
313
|
-
positionStart: node.positionStart,
|
|
314
|
-
positionEnd: node.positionEnd,
|
|
315
298
|
hint: 'All keys must be non-null values.',
|
|
316
299
|
rowIndex,
|
|
317
300
|
})
|
|
@@ -358,10 +341,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
358
341
|
jsonArg = JSON.parse(jsonArg)
|
|
359
342
|
} catch {
|
|
360
343
|
throw argValueError({
|
|
361
|
-
|
|
344
|
+
...node,
|
|
362
345
|
message: 'invalid JSON string',
|
|
363
|
-
positionStart: node.positionStart,
|
|
364
|
-
positionEnd: node.positionEnd,
|
|
365
346
|
hint: 'First argument must be valid JSON.',
|
|
366
347
|
rowIndex,
|
|
367
348
|
})
|
|
@@ -369,10 +350,8 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
369
350
|
}
|
|
370
351
|
if (typeof jsonArg !== 'object' || jsonArg instanceof Date) {
|
|
371
352
|
throw argValueError({
|
|
372
|
-
|
|
353
|
+
...node,
|
|
373
354
|
message: `first argument must be JSON string or object, got ${typeof jsonArg}`,
|
|
374
|
-
positionStart: node.positionStart,
|
|
375
|
-
positionEnd: node.positionEnd,
|
|
376
355
|
rowIndex,
|
|
377
356
|
})
|
|
378
357
|
}
|
|
@@ -412,30 +391,20 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
412
391
|
}
|
|
413
392
|
}
|
|
414
393
|
|
|
415
|
-
throw unknownFunctionError(
|
|
416
|
-
funcName,
|
|
417
|
-
positionStart: node.positionStart,
|
|
418
|
-
positionEnd: node.positionEnd,
|
|
419
|
-
})
|
|
394
|
+
throw unknownFunctionError(node)
|
|
420
395
|
}
|
|
421
396
|
|
|
422
397
|
if (node.type === 'cast') {
|
|
423
398
|
const val = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
|
|
424
399
|
if (val == null) return null
|
|
425
|
-
const toType = node
|
|
400
|
+
const { toType } = node
|
|
426
401
|
if (toType === 'TEXT' || toType === 'STRING' || toType === 'VARCHAR') {
|
|
427
402
|
if (typeof val === 'object') return stringify(val)
|
|
428
403
|
return String(val)
|
|
429
404
|
}
|
|
430
405
|
// Can only cast primitives to other primitive types
|
|
431
406
|
if (typeof val === 'object') {
|
|
432
|
-
throw castError({
|
|
433
|
-
toType: node.toType,
|
|
434
|
-
positionStart: node.positionStart,
|
|
435
|
-
positionEnd: node.positionEnd,
|
|
436
|
-
fromType: 'object',
|
|
437
|
-
rowIndex,
|
|
438
|
-
})
|
|
407
|
+
throw castError({ ...node, fromType: 'object', rowIndex })
|
|
439
408
|
}
|
|
440
409
|
if (toType === 'INTEGER' || toType === 'INT') {
|
|
441
410
|
const num = Number(val)
|
|
@@ -453,12 +422,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
453
422
|
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
454
423
|
return Boolean(val)
|
|
455
424
|
}
|
|
456
|
-
throw castError({
|
|
457
|
-
toType: node.toType,
|
|
458
|
-
positionStart: node.positionStart,
|
|
459
|
-
positionEnd: node.positionEnd,
|
|
460
|
-
rowIndex,
|
|
461
|
-
})
|
|
425
|
+
throw castError({ ...node, rowIndex })
|
|
462
426
|
}
|
|
463
427
|
|
|
464
428
|
// IN and NOT IN with value lists
|