squirreling 0.12.10 → 0.12.11
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/package.json +1 -1
- package/src/expression/evaluate.js +11 -0
- package/src/expression/strings.js +19 -4
- package/src/plan/columns.js +21 -0
- package/src/plan/plan.js +11 -7
- package/src/validation/tables.js +21 -16
package/package.json
CHANGED
|
@@ -47,6 +47,17 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
47
47
|
return value[node.name]
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
// Struct dot access where the prefix is itself a column name (bare or
|
|
51
|
+
// table-qualified), e.g. `item.name` reading field `name` from a struct
|
|
52
|
+
// column `item` (often introduced via UNNEST AS tc(item)).
|
|
53
|
+
const suffix = '.' + node.prefix
|
|
54
|
+
const baseColumns = row.columns.filter(col => col === node.prefix || col.endsWith(suffix))
|
|
55
|
+
if (baseColumns.length === 1) {
|
|
56
|
+
const value = await row.cells[baseColumns[0]]()
|
|
57
|
+
if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, node.name)) {
|
|
58
|
+
return value[node.name]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
// Check outer row for correlated subquery references
|
|
51
62
|
if (context.outerRow && context.outerAliases?.has(node.prefix) && node.name in context.outerRow.cells) {
|
|
52
63
|
return context.outerRow.cells[node.name]()
|
|
@@ -32,6 +32,25 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
32
32
|
// String first arg
|
|
33
33
|
const [val] = args
|
|
34
34
|
if (val == null) return null
|
|
35
|
+
|
|
36
|
+
if (funcName === 'LENGTH') {
|
|
37
|
+
if (typeof val === 'string' || Array.isArray(val)) return val.length
|
|
38
|
+
throw new ArgValueError({
|
|
39
|
+
...node,
|
|
40
|
+
message: `expected string or array, got ${typeof val === 'object' ? val instanceof Date ? 'date' : 'object' : typeof val}`,
|
|
41
|
+
hint: 'Use CAST to convert to a string first.',
|
|
42
|
+
rowIndex,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof val === 'object' && !(val instanceof Date)) {
|
|
47
|
+
throw new ArgValueError({
|
|
48
|
+
...node,
|
|
49
|
+
message: `does not support ${Array.isArray(val) ? 'array' : 'object'} arguments`,
|
|
50
|
+
hint: 'Use CAST to convert to a string first.',
|
|
51
|
+
rowIndex,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
35
54
|
const str = String(val)
|
|
36
55
|
|
|
37
56
|
if (funcName === 'UPPER') {
|
|
@@ -42,10 +61,6 @@ export function evaluateStringFunc({ funcName, node, args, rowIndex }) {
|
|
|
42
61
|
return str.toLowerCase()
|
|
43
62
|
}
|
|
44
63
|
|
|
45
|
-
if (funcName === 'LENGTH') {
|
|
46
|
-
return str.length
|
|
47
|
-
}
|
|
48
|
-
|
|
49
64
|
if (funcName === 'SUBSTRING' || funcName === 'SUBSTR') {
|
|
50
65
|
const start = Number(args[1])
|
|
51
66
|
if (!Number.isInteger(start) || start < 1) {
|
package/src/plan/columns.js
CHANGED
|
@@ -400,3 +400,24 @@ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
|
|
|
400
400
|
function lookupTableColumns(table, cteColumns, tables) {
|
|
401
401
|
return cteColumns?.get(table.toLowerCase()) ?? tables?.[table]?.columns ?? []
|
|
402
402
|
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Collects bare column names exposed by a SELECT's FROM and joins. Used by
|
|
406
|
+
* validation to recognize struct-field dot access (e.g. `item.name` on a
|
|
407
|
+
* struct-valued column `item`) instead of rejecting the prefix as an unknown
|
|
408
|
+
* table.
|
|
409
|
+
*
|
|
410
|
+
* @param {object} options
|
|
411
|
+
* @param {SelectStatement} options.select
|
|
412
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
413
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
414
|
+
* @returns {Set<string>}
|
|
415
|
+
*/
|
|
416
|
+
export function collectScopeColumns({ select, cteColumns, tables }) {
|
|
417
|
+
const result = new Set()
|
|
418
|
+
for (const col of inferSelectSourceColumns({ select, cteColumns, tables })) {
|
|
419
|
+
const dot = col.indexOf('.')
|
|
420
|
+
result.add(dot >= 0 ? col.slice(dot + 1) : col)
|
|
421
|
+
}
|
|
422
|
+
return result
|
|
423
|
+
}
|
package/src/plan/plan.js
CHANGED
|
@@ -4,7 +4,7 @@ import { findAggregate } from '../validation/aggregates.js'
|
|
|
4
4
|
import { ParseError } from '../validation/parseErrors.js'
|
|
5
5
|
import { ColumnNotFoundError, TableNotFoundError } from '../validation/tables.js'
|
|
6
6
|
import { validateNoIdentifiers, validateScan, validateTableRefs } from '../validation/tables.js'
|
|
7
|
-
import { extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, tableFunctionColumnNames } from './columns.js'
|
|
7
|
+
import { collectScopeColumns, extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, tableFunctionColumnNames } from './columns.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @import { AsyncDataSource, ExprNode, DerivedColumn, IdentifierNode, JoinClause, OrderByItem, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement, WindowFunctionNode } from '../types.js'
|
|
@@ -159,11 +159,15 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
159
159
|
// Resolve aliases (and validate qualified references)
|
|
160
160
|
// Include outerScope aliases so correlated references pass validation
|
|
161
161
|
const scopeTables = Object.fromEntries([sourceAlias, ...select.joins.map(j => j.alias ?? j.table), ...outerScope ?? []].map(a => [a, true]))
|
|
162
|
+
// Bare column names in scope, so the validator can recognize struct-field
|
|
163
|
+
// dot access on a column (e.g. `item.name` where `item` is an unnested
|
|
164
|
+
// struct column) rather than rejecting `item` as an unknown table.
|
|
165
|
+
const scopeColumns = collectScopeColumns({ select, cteColumns, tables })
|
|
162
166
|
/** @type {Map<string, ExprNode>} */
|
|
163
167
|
const aliases = new Map()
|
|
164
168
|
const columns = select.columns.map(col => {
|
|
165
169
|
if (col.type === 'derived') {
|
|
166
|
-
validateTableRefs(col.expr, scopeTables)
|
|
170
|
+
validateTableRefs(col.expr, scopeTables, scopeColumns)
|
|
167
171
|
const expr = resolveAliases(col.expr, aliases)
|
|
168
172
|
if (col.alias) {
|
|
169
173
|
aliases.set(col.alias, expr)
|
|
@@ -180,16 +184,16 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
180
184
|
const orderBy = resolveOrderByAliases(select.orderBy, aliases)
|
|
181
185
|
|
|
182
186
|
// Validate qualified references in other clauses
|
|
183
|
-
validateTableRefs(select.where, scopeTables)
|
|
184
|
-
validateTableRefs(select.having, scopeTables)
|
|
187
|
+
validateTableRefs(select.where, scopeTables, scopeColumns)
|
|
188
|
+
validateTableRefs(select.having, scopeTables, scopeColumns)
|
|
185
189
|
for (const expr of select.groupBy) {
|
|
186
|
-
validateTableRefs(expr, scopeTables)
|
|
190
|
+
validateTableRefs(expr, scopeTables, scopeColumns)
|
|
187
191
|
}
|
|
188
192
|
for (const term of select.orderBy) {
|
|
189
|
-
validateTableRefs(term.expr, scopeTables)
|
|
193
|
+
validateTableRefs(term.expr, scopeTables, scopeColumns)
|
|
190
194
|
}
|
|
191
195
|
for (const join of select.joins) {
|
|
192
|
-
validateTableRefs(join.on, scopeTables)
|
|
196
|
+
validateTableRefs(join.on, scopeTables, scopeColumns)
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// Determine scan hints for direct table scans (WHERE and LIMIT/OFFSET are
|
package/src/validation/tables.js
CHANGED
|
@@ -95,13 +95,18 @@ export function validateNoIdentifiers(expr, context) {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Validates that qualified identifiers reference known table aliases.
|
|
98
|
+
* A `prefix` may also be a bare column name in scope, in which case the
|
|
99
|
+
* identifier is struct-field access (e.g. `item.name` reads field `name`
|
|
100
|
+
* from a struct-valued column `item`).
|
|
98
101
|
*
|
|
99
102
|
* @param {ExprNode} expr
|
|
100
103
|
* @param {Record<string, any>} tables
|
|
104
|
+
* @param {Set<string>} [scopeColumns] - bare column names in scope, used to
|
|
105
|
+
* recognize struct-field dot access on a column rather than a table
|
|
101
106
|
*/
|
|
102
|
-
export function validateTableRefs(expr, tables) {
|
|
107
|
+
export function validateTableRefs(expr, tables, scopeColumns) {
|
|
103
108
|
if (!expr) return
|
|
104
|
-
if (expr.type === 'identifier' && expr.prefix && !(expr.prefix in tables)) {
|
|
109
|
+
if (expr.type === 'identifier' && expr.prefix && !(expr.prefix in tables) && !scopeColumns?.has(expr.prefix)) {
|
|
105
110
|
throw new TableNotFoundError({
|
|
106
111
|
table: expr.prefix,
|
|
107
112
|
qualified: expr.prefix + '.' + expr.name,
|
|
@@ -111,32 +116,32 @@ export function validateTableRefs(expr, tables) {
|
|
|
111
116
|
})
|
|
112
117
|
}
|
|
113
118
|
if (expr.type === 'binary') {
|
|
114
|
-
validateTableRefs(expr.left, tables)
|
|
115
|
-
validateTableRefs(expr.right, tables)
|
|
119
|
+
validateTableRefs(expr.left, tables, scopeColumns)
|
|
120
|
+
validateTableRefs(expr.right, tables, scopeColumns)
|
|
116
121
|
} else if (expr.type === 'unary') {
|
|
117
|
-
validateTableRefs(expr.argument, tables)
|
|
122
|
+
validateTableRefs(expr.argument, tables, scopeColumns)
|
|
118
123
|
} else if (expr.type === 'function') {
|
|
119
124
|
for (const arg of expr.args) {
|
|
120
|
-
validateTableRefs(arg, tables)
|
|
125
|
+
validateTableRefs(arg, tables, scopeColumns)
|
|
121
126
|
}
|
|
122
127
|
} else if (expr.type === 'window') {
|
|
123
|
-
for (const arg of expr.args) validateTableRefs(arg, tables)
|
|
124
|
-
for (const p of expr.partitionBy) validateTableRefs(p, tables)
|
|
125
|
-
for (const o of expr.orderBy) validateTableRefs(o.expr, tables)
|
|
128
|
+
for (const arg of expr.args) validateTableRefs(arg, tables, scopeColumns)
|
|
129
|
+
for (const p of expr.partitionBy) validateTableRefs(p, tables, scopeColumns)
|
|
130
|
+
for (const o of expr.orderBy) validateTableRefs(o.expr, tables, scopeColumns)
|
|
126
131
|
} else if (expr.type === 'cast') {
|
|
127
|
-
validateTableRefs(expr.expr, tables)
|
|
132
|
+
validateTableRefs(expr.expr, tables, scopeColumns)
|
|
128
133
|
} else if (expr.type === 'in valuelist') {
|
|
129
|
-
validateTableRefs(expr.expr, tables)
|
|
134
|
+
validateTableRefs(expr.expr, tables, scopeColumns)
|
|
130
135
|
for (const val of expr.values) {
|
|
131
|
-
validateTableRefs(val, tables)
|
|
136
|
+
validateTableRefs(val, tables, scopeColumns)
|
|
132
137
|
}
|
|
133
138
|
} else if (expr.type === 'case') {
|
|
134
|
-
validateTableRefs(expr.caseExpr, tables)
|
|
139
|
+
validateTableRefs(expr.caseExpr, tables, scopeColumns)
|
|
135
140
|
for (const w of expr.whenClauses) {
|
|
136
|
-
validateTableRefs(w.condition, tables)
|
|
137
|
-
validateTableRefs(w.result, tables)
|
|
141
|
+
validateTableRefs(w.condition, tables, scopeColumns)
|
|
142
|
+
validateTableRefs(w.result, tables, scopeColumns)
|
|
138
143
|
}
|
|
139
|
-
validateTableRefs(expr.elseResult, tables)
|
|
144
|
+
validateTableRefs(expr.elseResult, tables, scopeColumns)
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|