squirreling 0.12.7 → 0.12.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 CHANGED
@@ -154,14 +154,14 @@ Squirreling mostly follows the SQL standard. The following features are supporte
154
154
 
155
155
  ### Functions
156
156
 
157
- - Aggregate: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `JSON_ARRAYAGG`, `STRING_AGG`
157
+ - Aggregate: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `ARRAY_AGG`, `JSON_ARRAYAGG`, `STRING_AGG`
158
158
  - String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`, `POSITION`, `STRPOS`
159
159
  - Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
160
160
  - Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
161
161
  - Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_PART`, `DATE_TRUNC`, `EXTRACT`, `INTERVAL`
162
- - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`
162
+ - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`
163
163
  - Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_SORT`, `CARDINALITY`
164
- - Table functions: `UNNEST`
164
+ - Table functions: `UNNEST`, `JSON_EACH`
165
165
  - Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`
166
166
  - Spatial: `ST_GeomFromText`, `ST_MakeEnvelope`, `ST_AsText`, `ST_Intersects`, `ST_Contains`, `ST_ContainsProperly`, `ST_Within`, `ST_Overlaps`, `ST_Touches`, `ST_Equals`, `ST_Crosses`, `ST_Covers`, `ST_CoveredBy`, `ST_DWithin`
167
167
  - Conditional: `COALESCE`, `NULLIF`, `GREATEST`, `LEAST`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.7",
3
+ "version": "0.12.9",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
package/src/ast.d.ts CHANGED
@@ -65,7 +65,7 @@ export interface FromFunction extends AstBase {
65
65
  funcName: string
66
66
  args: ExprNode[]
67
67
  alias?: string
68
- columnAlias?: string
68
+ columnAliases: string[]
69
69
  }
70
70
 
71
71
  export type ArithmeticOp = '+' | '-' | '*' | '/' | '%'
@@ -106,6 +106,14 @@ export interface FunctionNode extends AstBase {
106
106
  filter?: ExprNode
107
107
  }
108
108
 
109
+ export interface WindowFunctionNode extends AstBase {
110
+ type: 'window'
111
+ funcName: string
112
+ args: ExprNode[]
113
+ partitionBy: ExprNode[]
114
+ orderBy: OrderByItem[]
115
+ }
116
+
109
117
  export type CastType = 'TEXT' | 'STRING' | 'VARCHAR' | 'INTEGER' | 'INT' | 'BIGINT' | 'FLOAT' | 'REAL' | 'DOUBLE' | 'BOOLEAN' | 'BOOL'
110
118
 
111
119
  export interface CastNode extends AstBase {
@@ -166,6 +174,7 @@ export type ExprNode =
166
174
  | UnaryNode
167
175
  | BinaryNode
168
176
  | FunctionNode
177
+ | WindowFunctionNode
169
178
  | CastNode
170
179
  | InSubqueryNode
171
180
  | InValuesNode
@@ -9,6 +9,7 @@ import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
9
9
  import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
10
10
  import { executeSort } from './sort.js'
11
11
  import { addBounds, minBounds, stableRowKey } from './utils.js'
12
+ import { executeWindow } from './window.js'
12
13
 
13
14
  /**
14
15
  * @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, ExecuteSqlOptions, ExprNode, IdentifierNode, QueryResults, SelectColumn, SqlPrimitive, Statement } from '../types.js'
@@ -120,24 +121,38 @@ export function executePlan({ plan, context }) {
120
121
  return executeSetOperation(plan, context)
121
122
  } else if (plan.type === 'TableFunction') {
122
123
  return executeTableFunction(plan, context)
124
+ } else if (plan.type === 'Window') {
125
+ return executeWindow(plan, context)
123
126
  }
124
127
  return { columns: [], async *rows() {} }
125
128
  }
126
129
 
127
130
  /**
128
- * Executes a table-valued function (e.g. UNNEST).
129
- * Evaluates the argument once against an empty row and yields one row per
130
- * element of the resulting array. Null or non-array input yields zero rows.
131
+ * Executes a table-valued function (e.g. UNNEST, JSON_EACH).
132
+ * Evaluates the argument once against the outer row (for lateral joins) or an
133
+ * empty row, then yields rows derived from the resulting value.
131
134
  *
132
135
  * @param {TableFunctionNode} plan
133
136
  * @param {ExecuteContext} context
134
137
  * @returns {QueryResults}
135
138
  */
136
139
  function executeTableFunction(plan, context) {
137
- if (plan.funcName !== 'UNNEST') {
138
- throw new Error(`Unsupported table function: ${plan.funcName}`)
140
+ if (plan.funcName === 'UNNEST') {
141
+ return executeUnnest(plan, context)
142
+ } else if (plan.funcName === 'JSON_EACH') {
143
+ return executeJsonEach(plan, context)
139
144
  }
140
- const columns = [plan.columnName]
145
+ throw new Error(`Unsupported table function: ${plan.funcName}`)
146
+ }
147
+
148
+ /**
149
+ * @param {TableFunctionNode} plan
150
+ * @param {ExecuteContext} context
151
+ * @returns {QueryResults}
152
+ */
153
+ function executeUnnest(plan, context) {
154
+ const columns = plan.columnNames
155
+ const [columnName] = columns
141
156
  return {
142
157
  columns,
143
158
  async *rows() {
@@ -149,9 +164,65 @@ function executeTableFunction(plan, context) {
149
164
  if (context.signal?.aborted) return
150
165
  yield {
151
166
  columns,
152
- cells: { [plan.columnName]: () => Promise.resolve(element) },
167
+ cells: { [columnName]: () => Promise.resolve(element) },
168
+ }
169
+ }
170
+ },
171
+ }
172
+ }
173
+
174
+ /**
175
+ * @param {TableFunctionNode} plan
176
+ * @param {ExecuteContext} context
177
+ * @returns {QueryResults}
178
+ */
179
+ function executeJsonEach(plan, context) {
180
+ const columns = plan.columnNames
181
+ const [keyCol, valueCol] = columns
182
+ return {
183
+ columns,
184
+ async *rows() {
185
+ /** @type {AsyncRow} */
186
+ const row = context.outerRow ?? { columns: [], cells: {} }
187
+ const value = await evaluateExpr({ node: plan.args[0], row, rowIndex: 1, context })
188
+ if (value == null) return
189
+ let parsed = value
190
+ if (typeof value === 'string') {
191
+ try {
192
+ parsed = JSON.parse(value)
193
+ } catch {
194
+ throw new Error('JSON_EACH(value): invalid JSON string. Argument must be valid JSON.')
195
+ }
196
+ }
197
+ if (Array.isArray(parsed)) {
198
+ for (let i = 0; i < parsed.length; i++) {
199
+ if (context.signal?.aborted) return
200
+ const k = i
201
+ const v = parsed[i]
202
+ yield {
203
+ columns,
204
+ cells: {
205
+ [keyCol]: () => Promise.resolve(k),
206
+ [valueCol]: () => Promise.resolve(v),
207
+ },
208
+ }
209
+ }
210
+ return
211
+ }
212
+ if (typeof parsed === 'object' && parsed !== null) {
213
+ for (const [k, v] of Object.entries(parsed)) {
214
+ if (context.signal?.aborted) return
215
+ yield {
216
+ columns,
217
+ cells: {
218
+ [keyCol]: () => Promise.resolve(k),
219
+ [valueCol]: () => Promise.resolve(v),
220
+ },
221
+ }
153
222
  }
223
+ return
154
224
  }
225
+ throw new Error('JSON_EACH(value): argument must be a JSON object or array')
155
226
  },
156
227
  }
157
228
  }
@@ -95,7 +95,7 @@ export function executeNestedLoopJoin(plan, context) {
95
95
  function executeLateralJoin(plan, context) {
96
96
  const left = executePlan({ plan: plan.left, context })
97
97
  // Right columns are known statically for table functions (the common case).
98
- const rightCols = plan.right.type === 'TableFunction' ? [plan.right.columnName] : []
98
+ const rightCols = plan.right.type === 'TableFunction' ? plan.right.columnNames : []
99
99
  return {
100
100
  columns: mergeColumnNames(left.columns, rightCols, plan.leftAlias, plan.rightAlias),
101
101
  async *rows() {
@@ -0,0 +1,154 @@
1
+ import { evaluateExpr } from '../expression/evaluate.js'
2
+ import { executePlan } from './execute.js'
3
+ import { compareForTerm, keyify } from './utils.js'
4
+
5
+ /**
6
+ * @import { AsyncRow, ExecuteContext, QueryResults, SqlPrimitive } from '../types.js'
7
+ * @import { WindowNode, WindowSpec } from '../plan/types.js'
8
+ */
9
+
10
+ /**
11
+ * Executes a Window plan node: buffers the child's rows, assigns each window
12
+ * function's output per partition, and yields rows in input order with the
13
+ * synthetic window cells attached.
14
+ *
15
+ * @param {WindowNode} plan
16
+ * @param {ExecuteContext} context
17
+ * @returns {QueryResults}
18
+ */
19
+ export function executeWindow(plan, context) {
20
+ const child = executePlan({ plan: plan.child, context })
21
+ const extraColumns = plan.windows.map(w => w.alias)
22
+
23
+ // Streaming fast path: every window is OVER () with no partition/order, so
24
+ // each row's output depends only on its position in the input stream. Avoids
25
+ // buffering — critical for large scans (e.g. parquet).
26
+ const streamable = plan.windows.every(w => w.partitionBy.length === 0 && w.orderBy.length === 0)
27
+
28
+ if (streamable) {
29
+ return {
30
+ columns: [...child.columns, ...extraColumns],
31
+ numRows: child.numRows,
32
+ maxRows: child.maxRows,
33
+ async *rows() {
34
+ let i = 0
35
+ for await (const row of child.rows()) {
36
+ if (context.signal?.aborted) return
37
+ i++
38
+ const cells = { ...row.cells }
39
+ for (const w of plan.windows) {
40
+ const value = assignRowNumber(w.funcName, i - 1)
41
+ cells[w.alias] = () => Promise.resolve(value)
42
+ }
43
+ yield {
44
+ columns: [...row.columns, ...extraColumns],
45
+ cells,
46
+ }
47
+ }
48
+ },
49
+ }
50
+ }
51
+
52
+ return {
53
+ columns: [...child.columns, ...extraColumns],
54
+ numRows: child.numRows,
55
+ maxRows: child.maxRows,
56
+ async *rows() {
57
+ /** @type {AsyncRow[]} */
58
+ const rows = []
59
+ for await (const row of child.rows()) {
60
+ if (context.signal?.aborted) return
61
+ rows.push(row)
62
+ }
63
+ if (rows.length === 0) return
64
+
65
+ // One SqlPrimitive per window spec per row, indexed by row input position.
66
+ /** @type {SqlPrimitive[][]} */
67
+ const windowValues = plan.windows.map(() => new Array(rows.length))
68
+
69
+ for (let w = 0; w < plan.windows.length; w++) {
70
+ await computeWindow(plan.windows[w], rows, windowValues[w], context)
71
+ if (context.signal?.aborted) return
72
+ }
73
+
74
+ for (let i = 0; i < rows.length; i++) {
75
+ if (context.signal?.aborted) return
76
+ const row = rows[i]
77
+ const cells = { ...row.cells }
78
+ for (let w = 0; w < plan.windows.length; w++) {
79
+ const { alias } = plan.windows[w]
80
+ const value = windowValues[w][i]
81
+ cells[alias] = () => Promise.resolve(value)
82
+ }
83
+ yield {
84
+ columns: [...row.columns, ...extraColumns],
85
+ cells,
86
+ }
87
+ }
88
+ },
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Computes a single window function across all rows, writing the per-row
94
+ * output values into `output`.
95
+ *
96
+ * @param {WindowSpec} spec
97
+ * @param {AsyncRow[]} rows
98
+ * @param {SqlPrimitive[]} output
99
+ * @param {ExecuteContext} context
100
+ */
101
+ async function computeWindow(spec, rows, output, context) {
102
+ // Bucket row indices by partition key.
103
+ /** @type {Map<string | number | bigint | boolean, number[]>} */
104
+ const partitions = new Map()
105
+ const partitionKeys = await Promise.all(rows.map(row =>
106
+ Promise.all(spec.partitionBy.map(expr => evaluateExpr({ node: expr, row, context })))
107
+ ))
108
+ for (let i = 0; i < rows.length; i++) {
109
+ const key = keyify(...partitionKeys[i])
110
+ let bucket = partitions.get(key)
111
+ if (!bucket) {
112
+ bucket = []
113
+ partitions.set(key, bucket)
114
+ }
115
+ bucket.push(i)
116
+ }
117
+
118
+ for (const bucket of partitions.values()) {
119
+ if (context.signal?.aborted) return
120
+
121
+ // Order within the partition. Empty ORDER BY → input order.
122
+ if (spec.orderBy.length) {
123
+ const orderValues = await Promise.all(bucket.map(idx =>
124
+ Promise.all(spec.orderBy.map(term => evaluateExpr({ node: term.expr, row: rows[idx], context })))
125
+ ))
126
+ /** @type {{ idx: number, values: SqlPrimitive[], pos: number }[]} */
127
+ const entries = bucket.map((idx, k) => ({ idx, values: orderValues[k], pos: k }))
128
+ entries.sort((a, b) => {
129
+ for (let i = 0; i < spec.orderBy.length; i++) {
130
+ const cmp = compareForTerm(a.values[i], b.values[i], spec.orderBy[i])
131
+ if (cmp !== 0) return cmp
132
+ }
133
+ return a.pos - b.pos
134
+ })
135
+ for (let k = 0; k < entries.length; k++) {
136
+ output[entries[k].idx] = assignRowNumber(spec.funcName, k)
137
+ }
138
+ } else {
139
+ for (let k = 0; k < bucket.length; k++) {
140
+ output[bucket[k]] = assignRowNumber(spec.funcName, k)
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * @param {string} funcName
148
+ * @param {number} rank - 0-based rank within the partition
149
+ * @returns {SqlPrimitive}
150
+ */
151
+ function assignRowNumber(funcName, rank) {
152
+ if (funcName === 'ROW_NUMBER') return rank + 1
153
+ throw new Error(`Unsupported window function: ${funcName}`)
154
+ }
@@ -31,6 +31,9 @@ export function derivedAlias(expr) {
31
31
  }
32
32
  return expr.funcName.toLowerCase() + '_' + expr.args.map(derivedAlias).join('_')
33
33
  }
34
+ if (expr.type === 'window') {
35
+ return expr.funcName.toLowerCase()
36
+ }
34
37
  if (expr.type === 'interval') {
35
38
  return `interval_${expr.value}_${expr.unit.toLowerCase()}`
36
39
  }
@@ -272,7 +272,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
272
272
  return values[lower] + (values[upper] - values[lower]) * (pos - lower)
273
273
  }
274
274
 
275
- if (funcName === 'JSON_ARRAYAGG') {
275
+ if (funcName === 'JSON_ARRAYAGG' || funcName === 'ARRAY_AGG') {
276
276
  if (node.distinct) {
277
277
  /** @type {SqlPrimitive[]} */
278
278
  const values = []
@@ -417,6 +417,40 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
417
417
  return result
418
418
  }
419
419
 
420
+ if (funcName === 'JSON_VALID') {
421
+ const value = args[0]
422
+ if (value == null) return null
423
+ if (typeof value !== 'string') return false
424
+ try {
425
+ JSON.parse(value)
426
+ return true
427
+ } catch {
428
+ return false
429
+ }
430
+ }
431
+
432
+ if (funcName === 'JSON_TYPE') {
433
+ let value = args[0]
434
+ if (value == null) return null
435
+ if (typeof value === 'string') {
436
+ try {
437
+ value = JSON.parse(value)
438
+ } catch {
439
+ throw new ArgValueError({
440
+ ...node,
441
+ message: 'invalid JSON string',
442
+ hint: 'Argument must be valid JSON.',
443
+ rowIndex,
444
+ })
445
+ }
446
+ }
447
+ if (value === null) return 'null'
448
+ if (Array.isArray(value)) return 'array'
449
+ if (value instanceof Date) return 'string'
450
+ if (typeof value === 'bigint') return 'number'
451
+ return typeof value
452
+ }
453
+
420
454
  if (funcName === 'JSON_ARRAY_LENGTH') {
421
455
  let arr = args[0]
422
456
  if (arr == null) return null
@@ -1,10 +1,10 @@
1
- import { isAggregateFunc, isKnownFunction, niladicFuncs, validateFunctionArgs } from '../validation/functions.js'
1
+ import { isAggregateFunc, isKnownFunction, isWindowFunc, niladicFuncs, validateFunctionArgs } from '../validation/functions.js'
2
2
  import { ParseError, UnknownFunctionError } from '../validation/parseErrors.js'
3
3
  import { parseExpression } from './expression.js'
4
4
  import { consume, current, expect, match } from './state.js'
5
5
 
6
6
  /**
7
- * @import { ExprNode, ParserState } from '../types.js'
7
+ * @import { ExprNode, OrderByItem, ParserState } from '../types.js'
8
8
  */
9
9
 
10
10
  /**
@@ -84,6 +84,30 @@ export function parseFunctionCall(state, positionStart) {
84
84
  })
85
85
  }
86
86
 
87
+ // Check for WITHIN GROUP (ORDER BY expr) clause — standard SQL ordered-set aggregate syntax.
88
+ // Supported for PERCENTILE_CONT: PERCENTILE_CONT(fraction) WITHIN GROUP (ORDER BY expr)
89
+ const withinTok = current(state)
90
+ if (match(state, 'keyword', 'WITHIN')) {
91
+ if (funcNameUpper !== 'PERCENTILE_CONT') {
92
+ throw new ParseError({
93
+ message: `WITHIN GROUP is only supported for PERCENTILE_CONT, not "${funcName}"`,
94
+ ...withinTok,
95
+ })
96
+ }
97
+ if (args.length !== 1) {
98
+ throw new ParseError({
99
+ message: `${funcName}: cannot combine WITHIN GROUP with a value argument`,
100
+ ...withinTok,
101
+ })
102
+ }
103
+ expect(state, 'keyword', 'GROUP')
104
+ expect(state, 'paren', '(')
105
+ expect(state, 'keyword', 'ORDER')
106
+ expect(state, 'keyword', 'BY')
107
+ args.push(parseExpression(state))
108
+ expect(state, 'paren', ')')
109
+ }
110
+
87
111
  // Validate argument count at parse time
88
112
  validateFunctionArgs(funcNameUpper, args.length, positionStart, state.lastPos, state.functions)
89
113
 
@@ -104,13 +128,43 @@ export function parseFunctionCall(state, positionStart) {
104
128
  expect(state, 'paren', ')')
105
129
  }
106
130
 
107
- // Check for OVER clause (window functions not supported)
131
+ // Check for OVER clause
108
132
  const overTok = current(state)
109
- if (overTok.type === 'identifier' && overTok.value.toUpperCase() === 'OVER') {
133
+ const hasOver = overTok.type === 'identifier' && overTok.value.toUpperCase() === 'OVER'
134
+
135
+ if (hasOver) {
136
+ if (!isWindowFunc(funcNameUpper)) {
137
+ throw new ParseError({
138
+ message: `Window functions are not supported: ${funcName}(...) OVER (...)`,
139
+ positionStart,
140
+ positionEnd: overTok.positionEnd,
141
+ })
142
+ }
143
+ if (filter) {
144
+ throw new ParseError({
145
+ message: `FILTER cannot be combined with OVER for "${funcName}"`,
146
+ positionStart,
147
+ positionEnd: overTok.positionEnd,
148
+ })
149
+ }
150
+ consume(state)
151
+ const { partitionBy, orderBy } = parseWindowSpec(state, positionStart)
152
+ return {
153
+ type: 'window',
154
+ funcName,
155
+ args,
156
+ partitionBy,
157
+ orderBy,
158
+ positionStart,
159
+ positionEnd: state.lastPos,
160
+ }
161
+ }
162
+
163
+ if (isWindowFunc(funcNameUpper)) {
110
164
  throw new ParseError({
111
- message: `Window functions are not supported: ${funcName}(...) OVER (...)`,
165
+ message: `${funcName}() requires an OVER clause at position ${positionStart}`,
112
166
  positionStart,
113
- positionEnd: overTok.positionEnd,
167
+ positionEnd: state.lastPos,
114
168
  })
115
169
  }
116
170
 
@@ -124,3 +178,64 @@ export function parseFunctionCall(state, positionStart) {
124
178
  positionEnd: state.lastPos,
125
179
  }
126
180
  }
181
+
182
+ /**
183
+ * Parses the window spec after OVER: ( [PARTITION BY expr[, ...]] [ORDER BY expr [ASC|DESC] [NULLS FIRST|LAST][, ...]] )
184
+ *
185
+ * @param {ParserState} state
186
+ * @param {number} positionStart - start position of the enclosing function call (for OrderByItem positions)
187
+ * @returns {{ partitionBy: ExprNode[], orderBy: OrderByItem[] }}
188
+ */
189
+ function parseWindowSpec(state, positionStart) {
190
+ expect(state, 'paren', '(')
191
+ /** @type {ExprNode[]} */
192
+ const partitionBy = []
193
+ /** @type {OrderByItem[]} */
194
+ const orderBy = []
195
+
196
+ const partitionTok = current(state)
197
+ if (partitionTok.type === 'identifier' && partitionTok.value.toUpperCase() === 'PARTITION') {
198
+ consume(state)
199
+ expect(state, 'keyword', 'BY')
200
+ while (true) {
201
+ partitionBy.push(parseExpression(state))
202
+ if (!match(state, 'comma')) break
203
+ }
204
+ }
205
+
206
+ if (match(state, 'keyword', 'ORDER')) {
207
+ expect(state, 'keyword', 'BY')
208
+ while (true) {
209
+ const expr = parseExpression(state)
210
+ /** @type {'ASC' | 'DESC'} */
211
+ let direction = 'ASC'
212
+ if (match(state, 'keyword', 'ASC')) {
213
+ direction = 'ASC'
214
+ } else if (match(state, 'keyword', 'DESC')) {
215
+ direction = 'DESC'
216
+ }
217
+ /** @type {'FIRST' | 'LAST' | undefined} */
218
+ let nulls
219
+ if (match(state, 'keyword', 'NULLS')) {
220
+ const tok = consume(state)
221
+ const upper = tok.value.toUpperCase()
222
+ if (tok.type === 'identifier' && upper === 'FIRST') {
223
+ nulls = 'FIRST'
224
+ } else if (tok.type === 'identifier' && upper === 'LAST') {
225
+ nulls = 'LAST'
226
+ } else {
227
+ throw new ParseError({
228
+ message: `Expected FIRST or LAST after NULLS at position ${tok.positionStart}`,
229
+ positionStart: tok.positionStart,
230
+ positionEnd: tok.positionEnd,
231
+ })
232
+ }
233
+ }
234
+ orderBy.push({ expr, direction, nulls, positionStart, positionEnd: state.lastPos })
235
+ if (!match(state, 'comma')) break
236
+ }
237
+ }
238
+
239
+ expect(state, 'paren', ')')
240
+ return { partitionBy, orderBy }
241
+ }
@@ -468,15 +468,22 @@ export function parseFromFunction(state) {
468
468
  validateFunctionArgs(funcName, args.length, positionStart, state.lastPos, state.functions)
469
469
 
470
470
  const alias = parseTableAlias(state)
471
- /** @type {string | undefined} */
472
- let columnAlias
471
+ /** @type {string[]} */
472
+ const columnAliases = []
473
473
  if (alias && match(state, 'paren', '(')) {
474
474
  const colStart = state.lastPos
475
- const colTok = expect(state, 'identifier')
476
- columnAlias = colTok.value
477
- if (match(state, 'comma')) {
475
+ while (true) {
476
+ const colTok = expect(state, 'identifier')
477
+ columnAliases.push(colTok.value)
478
+ if (!match(state, 'comma')) break
479
+ }
480
+ const maxCols = tableFunctionColumnCount(funcName)
481
+ if (columnAliases.length > maxCols) {
482
+ const colLabels = tableFunctionDefaultColumns(funcName).join(', ')
478
483
  throw new ParseError({
479
- message: `${funcName} produces a single column; only one column alias is allowed`,
484
+ message: maxCols === 1
485
+ ? `${funcName} produces a single column; only one column alias is allowed`
486
+ : `${funcName} produces at most ${maxCols} columns (${colLabels}); too many column aliases`,
480
487
  positionStart: colStart,
481
488
  positionEnd: state.lastPos,
482
489
  })
@@ -489,12 +496,31 @@ export function parseFromFunction(state) {
489
496
  funcName,
490
497
  args,
491
498
  alias,
492
- columnAlias,
499
+ columnAliases,
493
500
  positionStart,
494
501
  positionEnd: state.lastPos,
495
502
  }
496
503
  }
497
504
 
505
+ /**
506
+ * Default column names produced by a table-valued function.
507
+ * @param {string} funcName
508
+ * @returns {string[]}
509
+ */
510
+ export function tableFunctionDefaultColumns(funcName) {
511
+ if (funcName === 'JSON_EACH') return ['key', 'value']
512
+ return [funcName.toLowerCase()]
513
+ }
514
+
515
+ /**
516
+ * Maximum number of output columns a table-valued function can produce.
517
+ * @param {string} funcName
518
+ * @returns {number}
519
+ */
520
+ export function tableFunctionColumnCount(funcName) {
521
+ return tableFunctionDefaultColumns(funcName).length
522
+ }
523
+
498
524
  /**
499
525
  * Parses an optional table alias (e.g., "FROM users u" or "FROM users AS u")
500
526
  * @param {ParserState} state
@@ -1,3 +1,4 @@
1
+ import { tableFunctionDefaultColumns } from '../parse/parse.js'
1
2
  import { derivedAlias } from '../expression/alias.js'
2
3
 
3
4
  /**
@@ -18,13 +19,19 @@ export function fromAlias(from) {
18
19
  }
19
20
 
20
21
  /**
21
- * Returns the single output column name for a FROM table function.
22
+ * Returns the output column names for a FROM table function, applying any
23
+ * column aliases over the function's default column names.
22
24
  *
23
25
  * @param {FromFunction} from
24
- * @returns {string}
26
+ * @returns {string[]}
25
27
  */
26
- export function tableFunctionColumnName(from) {
27
- return from.columnAlias ?? from.funcName.toLowerCase()
28
+ export function tableFunctionColumnNames(from) {
29
+ const defaults = tableFunctionDefaultColumns(from.funcName)
30
+ const result = []
31
+ for (let i = 0; i < defaults.length; i++) {
32
+ result.push(from.columnAliases[i] ?? defaults[i])
33
+ }
34
+ return result
28
35
  }
29
36
 
30
37
  /**
@@ -204,6 +211,10 @@ function collectColumnsFromExpr(expr, columns, aliases) {
204
211
  collectColumnsFromExpr(arg, columns, aliases)
205
212
  }
206
213
  collectColumnsFromExpr(expr.filter, columns, aliases)
214
+ } else if (expr.type === 'window') {
215
+ for (const arg of expr.args) collectColumnsFromExpr(arg, columns, aliases)
216
+ for (const p of expr.partitionBy) collectColumnsFromExpr(p, columns, aliases)
217
+ for (const o of expr.orderBy) collectColumnsFromExpr(o.expr, columns, aliases)
207
218
  } else if (expr.type === 'cast') {
208
219
  collectColumnsFromExpr(expr.expr, columns, aliases)
209
220
  } else if (expr.type === 'in valuelist') {
@@ -330,18 +341,21 @@ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
330
341
  }
331
342
 
332
343
  if (select.from.type === 'function') {
333
- // Table functions currently produce a single column
334
344
  if (!select.joins.length) {
335
- return [tableFunctionColumnName(select.from)]
345
+ return tableFunctionColumnNames(select.from)
336
346
  }
337
347
  /** @type {string[]} */
338
348
  const result = []
339
349
  const alias = fromAlias(select.from)
340
- result.push(`${alias}.${tableFunctionColumnName(select.from)}`)
350
+ for (const col of tableFunctionColumnNames(select.from)) {
351
+ result.push(`${alias}.${col}`)
352
+ }
341
353
  for (const join of select.joins) {
342
354
  const joinAlias = join.alias ?? join.table
343
355
  if (join.fromFunction) {
344
- result.push(`${joinAlias}.${tableFunctionColumnName(join.fromFunction)}`)
356
+ for (const col of tableFunctionColumnNames(join.fromFunction)) {
357
+ result.push(`${joinAlias}.${col}`)
358
+ }
345
359
  } else {
346
360
  for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
347
361
  result.push(`${joinAlias}.${col}`)
@@ -365,7 +379,9 @@ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
365
379
  for (const join of select.joins) {
366
380
  const joinAlias = join.alias ?? join.table
367
381
  if (join.fromFunction) {
368
- result.push(`${joinAlias}.${tableFunctionColumnName(join.fromFunction)}`)
382
+ for (const col of tableFunctionColumnNames(join.fromFunction)) {
383
+ result.push(`${joinAlias}.${col}`)
384
+ }
369
385
  } else {
370
386
  for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
371
387
  result.push(`${joinAlias}.${col}`)
package/src/plan/plan.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { derivedAlias } from '../expression/alias.js'
2
2
  import { parseSql } from '../parse/parse.js'
3
3
  import { findAggregate } from '../validation/aggregates.js'
4
+ import { ParseError } from '../validation/parseErrors.js'
4
5
  import { ColumnNotFoundError, TableNotFoundError } from '../validation/tables.js'
5
6
  import { validateNoIdentifiers, validateScan, validateTableRefs } from '../validation/tables.js'
6
- import { extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, tableFunctionColumnName } from './columns.js'
7
+ import { extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, tableFunctionColumnNames } from './columns.js'
7
8
 
8
9
  /**
9
- * @import { AsyncDataSource, ExprNode, DerivedColumn, IdentifierNode, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement } from '../types.js'
10
- * @import { QueryPlan } from './types.d.ts'
10
+ * @import { AsyncDataSource, ExprNode, DerivedColumn, IdentifierNode, JoinClause, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement, WindowFunctionNode } from '../types.js'
11
+ * @import { QueryPlan, WindowSpec } from './types.d.ts'
11
12
  */
12
13
 
13
14
  /**
@@ -106,12 +107,51 @@ function planSetOperation({ compound, ctePlans, cteColumns, tables, parentColumn
106
107
  * @returns {QueryPlan}
107
108
  */
108
109
  function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outerScope }) {
110
+ // Reject window functions in clauses where they're not permitted.
111
+ expectNoWindowFunction(select.where, 'WHERE')
112
+ expectNoWindowFunction(select.having, 'HAVING')
113
+ for (const expr of select.groupBy) expectNoWindowFunction(expr, 'GROUP BY')
114
+ for (const term of select.orderBy) expectNoWindowFunction(term.expr, 'ORDER BY')
115
+ for (const join of select.joins) expectNoWindowFunction(join.on, 'JOIN ON')
116
+
117
+ // Collect window functions from SELECT columns and rewrite them to identifiers
118
+ // pointing at the synthetic cells produced by the Window plan node.
119
+ /** @type {WindowSpec[]} */
120
+ const windows = []
121
+ const windowColumns = select.columns.map(col => {
122
+ if (col.type !== 'derived') return col
123
+ const originalAlias = col.alias ?? derivedAlias(col.expr)
124
+ const expr = collectWindows(col.expr, windows)
125
+ if (expr === col.expr) return col
126
+ return { ...col, expr, alias: originalAlias }
127
+ })
128
+
129
+ if (windows.length && select.columns.some(col => col.type === 'derived' && findAggregate(col.expr))) {
130
+ throw new ParseError({
131
+ message: 'Window functions are not supported in queries with aggregation',
132
+ ...select,
133
+ })
134
+ }
135
+ if (windows.length && select.groupBy.length) {
136
+ throw new ParseError({
137
+ message: 'Window functions are not supported in queries with aggregation',
138
+ ...select,
139
+ })
140
+ }
141
+
142
+ // Preserve the pre-substitution columns for column-extraction, so synthetic
143
+ // `__window_N` identifiers are not requested from the data source.
144
+ const originalSelect = select
145
+ select = { ...select, columns: windowColumns }
146
+
109
147
  // Check for aggregation
110
148
  const hasAggregate = select.columns.some(col =>
111
149
  col.type === 'derived' && findAggregate(col.expr)
112
150
  )
113
151
  const useGrouping = hasAggregate || select.groupBy.length > 0
114
- const needsBuffering = useGrouping || select.orderBy.length > 0
152
+ // Windows with PARTITION BY or ORDER BY buffer; `OVER ()` streams.
153
+ const bufferingWindows = windows.some(w => w.partitionBy.length > 0 || w.orderBy.length > 0)
154
+ const needsBuffering = useGrouping || select.orderBy.length > 0 || bufferingWindows
115
155
 
116
156
  // Source alias for FROM clause
117
157
  const sourceAlias = fromAlias(select.from)
@@ -155,7 +195,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
155
195
  // included so they are only applied to fresh scans, not CTE/subquery plans)
156
196
  /** @type {ScanOptions} */
157
197
  const hints = {}
158
- const perTableColumns = extractColumns({ select, parentColumns })
198
+ const perTableColumns = extractColumns({ select: originalSelect, parentColumns })
159
199
  hints.columns = perTableColumns.get(sourceAlias)
160
200
  // Empty columns array means no columns were referenced, but a FROM subquery
161
201
  // still needs its own columns (e.g. for DISTINCT). Treat empty as unrestricted.
@@ -219,6 +259,12 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
219
259
  } else {
220
260
  // Non-aggregation path
221
261
 
262
+ // Window functions: insert before Sort so outer ORDER BY can reference
263
+ // the window output aliases.
264
+ if (windows.length) {
265
+ plan = { type: 'Window', windows, child: plan }
266
+ }
267
+
222
268
  // ORDER BY (before projection so it can access all columns)
223
269
  // Resolve SELECT aliases in ORDER BY expressions at plan time
224
270
  if (select.orderBy.length) {
@@ -321,7 +367,7 @@ function planTableFunction(from) {
321
367
  type: 'TableFunction',
322
368
  funcName: from.funcName,
323
369
  args: from.args,
324
- columnName: tableFunctionColumnName(from),
370
+ columnNames: tableFunctionColumnNames(from),
325
371
  }
326
372
  }
327
373
 
@@ -609,6 +655,126 @@ function validateLateralSubqueries({ expr, ctePlans, cteColumns, tables, outerSc
609
655
  }
610
656
  }
611
657
 
658
+ /**
659
+ * Walks an expression, replacing every window function subnode with an
660
+ * identifier that points at a synthetic `__window_N` cell. The collected
661
+ * WindowSpec entries drive the Window plan node. Returns the same node
662
+ * reference when no window function is present, so untouched expressions
663
+ * aren't shallow-cloned.
664
+ *
665
+ * @param {ExprNode} expr
666
+ * @param {WindowSpec[]} windows
667
+ * @returns {ExprNode}
668
+ */
669
+ function collectWindows(expr, windows) {
670
+ if (!expr || !findWindow(expr)) return expr
671
+ if (expr.type === 'window') {
672
+ const alias = `__window_${windows.length}`
673
+ windows.push({
674
+ alias,
675
+ funcName: expr.funcName.toUpperCase(),
676
+ args: expr.args,
677
+ partitionBy: expr.partitionBy,
678
+ orderBy: expr.orderBy,
679
+ })
680
+ return {
681
+ type: 'identifier',
682
+ name: alias,
683
+ positionStart: expr.positionStart,
684
+ positionEnd: expr.positionEnd,
685
+ }
686
+ }
687
+ if (expr.type === 'unary') {
688
+ return { ...expr, argument: collectWindows(expr.argument, windows) }
689
+ }
690
+ if (expr.type === 'binary') {
691
+ return { ...expr, left: collectWindows(expr.left, windows), right: collectWindows(expr.right, windows) }
692
+ }
693
+ if (expr.type === 'function') {
694
+ return { ...expr, args: expr.args.map(a => collectWindows(a, windows)) }
695
+ }
696
+ if (expr.type === 'cast') {
697
+ return { ...expr, expr: collectWindows(expr.expr, windows) }
698
+ }
699
+ if (expr.type === 'in valuelist') {
700
+ return {
701
+ ...expr,
702
+ expr: collectWindows(expr.expr, windows),
703
+ values: expr.values.map(v => collectWindows(v, windows)),
704
+ }
705
+ }
706
+ if (expr.type === 'case') {
707
+ return {
708
+ ...expr,
709
+ caseExpr: expr.caseExpr && collectWindows(expr.caseExpr, windows),
710
+ whenClauses: expr.whenClauses.map(w => ({
711
+ ...w,
712
+ condition: collectWindows(w.condition, windows),
713
+ result: collectWindows(w.result, windows),
714
+ })),
715
+ elseResult: expr.elseResult && collectWindows(expr.elseResult, windows),
716
+ }
717
+ }
718
+ return expr
719
+ }
720
+
721
+ /**
722
+ * Throws if the expression tree contains a window function.
723
+ *
724
+ * @param {ExprNode | undefined} expr
725
+ * @param {string} clause
726
+ */
727
+ function expectNoWindowFunction(expr, clause) {
728
+ const win = findWindow(expr)
729
+ if (win) {
730
+ throw new ParseError({
731
+ message: `Window function ${win.funcName.toUpperCase()} is not allowed in ${clause} clause`,
732
+ positionStart: win.positionStart,
733
+ positionEnd: win.positionEnd,
734
+ })
735
+ }
736
+ }
737
+
738
+ /**
739
+ * @param {ExprNode | undefined} expr
740
+ * @returns {WindowFunctionNode | undefined}
741
+ */
742
+ function findWindow(expr) {
743
+ if (!expr) return undefined
744
+ if (expr.type === 'window') return expr
745
+ if (expr.type === 'binary') return findWindow(expr.left) || findWindow(expr.right)
746
+ if (expr.type === 'unary') return findWindow(expr.argument)
747
+ if (expr.type === 'function') {
748
+ for (const arg of expr.args) {
749
+ const found = findWindow(arg)
750
+ if (found) return found
751
+ }
752
+ return undefined
753
+ }
754
+ if (expr.type === 'cast') return findWindow(expr.expr)
755
+ if (expr.type === 'in valuelist') {
756
+ const found = findWindow(expr.expr)
757
+ if (found) return found
758
+ for (const val of expr.values) {
759
+ const f = findWindow(val)
760
+ if (f) return f
761
+ }
762
+ return undefined
763
+ }
764
+ if (expr.type === 'case') {
765
+ if (expr.caseExpr) {
766
+ const f = findWindow(expr.caseExpr)
767
+ if (f) return f
768
+ }
769
+ for (const w of expr.whenClauses) {
770
+ const f = findWindow(w.condition) || findWindow(w.result)
771
+ if (f) return f
772
+ }
773
+ if (expr.elseResult) return findWindow(expr.elseResult)
774
+ }
775
+ return undefined
776
+ }
777
+
612
778
  /**
613
779
  * Checks if every SELECT column is a plain COUNT(*).
614
780
  *
@@ -15,6 +15,7 @@ export type QueryPlan =
15
15
  | PositionalJoinNode
16
16
  | SetOperationNode
17
17
  | TableFunctionNode
18
+ | WindowNode
18
19
 
19
20
  // Scan node
20
21
  export interface ScanNode {
@@ -122,5 +123,19 @@ export interface TableFunctionNode {
122
123
  type: 'TableFunction'
123
124
  funcName: string
124
125
  args: ExprNode[]
125
- columnName: string
126
+ columnNames: string[]
127
+ }
128
+
129
+ export interface WindowSpec {
130
+ alias: string
131
+ funcName: string
132
+ args: ExprNode[]
133
+ partitionBy: ExprNode[]
134
+ orderBy: OrderByItem[]
135
+ }
136
+
137
+ export interface WindowNode {
138
+ type: 'Window'
139
+ windows: WindowSpec[]
140
+ child: QueryPlan
126
141
  }
package/src/types.d.ts CHANGED
@@ -129,7 +129,7 @@ export interface UserDefinedFunction {
129
129
  arguments: FunctionSignature
130
130
  }
131
131
 
132
- export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
132
+ export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'ARRAY_AGG' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
133
133
 
134
134
  export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE' | 'REGEXP_MATCHES'
135
135
 
@@ -11,7 +11,7 @@ export const niladicFuncs = ['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'
11
11
  * @returns {name is AggregateFunc}
12
12
  */
13
13
  export function isAggregateFunc(name) {
14
- return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
14
+ return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'ARRAY_AGG', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
15
15
  }
16
16
 
17
17
  /**
@@ -26,6 +26,14 @@ export function isMathFunc(name) {
26
26
  ].includes(name)
27
27
  }
28
28
 
29
+ /**
30
+ * @param {string} name
31
+ * @returns {boolean}
32
+ */
33
+ export function isWindowFunc(name) {
34
+ return ['ROW_NUMBER'].includes(name)
35
+ }
36
+
29
37
  /**
30
38
  * @param {string} name
31
39
  * @returns {name is RegExpFunction}
@@ -41,7 +49,7 @@ export function isRegexpFunc(name) {
41
49
  * @returns {boolean}
42
50
  */
43
51
  export function isTableFunction(name) {
44
- return ['UNNEST'].includes(name)
52
+ return ['UNNEST', 'JSON_EACH'].includes(name)
45
53
  }
46
54
 
47
55
  /**
@@ -166,7 +174,10 @@ export const FUNCTION_SIGNATURES = {
166
174
  JSON_EXTRACT: { min: 2, max: 2, signature: 'expression, path' },
167
175
  JSON_OBJECT: { min: 0, signature: 'key1, value1[, ...]' },
168
176
  JSON_ARRAY_LENGTH: { min: 1, max: 1, signature: 'array' },
177
+ JSON_VALID: { min: 1, max: 1, signature: 'value' },
178
+ JSON_TYPE: { min: 1, max: 1, signature: 'value' },
169
179
  JSON_ARRAYAGG: { min: 1, max: 1, signature: 'expression' },
180
+ ARRAY_AGG: { min: 1, max: 1, signature: 'expression' },
170
181
 
171
182
  // Array functions
172
183
  ARRAY_LENGTH: { min: 1, max: 1, signature: 'array' },
@@ -176,6 +187,7 @@ export const FUNCTION_SIGNATURES = {
176
187
 
177
188
  // Table functions (used in FROM clause)
178
189
  UNNEST: { min: 1, max: 1, signature: 'array' },
190
+ JSON_EACH: { min: 1, max: 1, signature: 'value' },
179
191
 
180
192
  // Conditional functions
181
193
  COALESCE: { min: 1, signature: 'value1, value2[, ...]' },
@@ -196,6 +208,9 @@ export const FUNCTION_SIGNATURES = {
196
208
  APPROX_QUANTILE: { min: 2, max: 2, signature: 'expression, fraction' },
197
209
  STRING_AGG: { min: 2, max: 2, signature: 'expression, separator' },
198
210
 
211
+ // Window functions
212
+ ROW_NUMBER: { min: 0, max: 0, signature: '' },
213
+
199
214
  // Spatial functions
200
215
  ST_INTERSECTS: { min: 2, max: 2, signature: 'geometry, geometry' },
201
216
  ST_CONTAINS: { min: 2, max: 2, signature: 'geometry, geometry' },
@@ -4,7 +4,7 @@ export const KEYWORDS = new Set([
4
4
  'DISTINCT', 'TRUE', 'FALSE', 'NULL', 'LIKE', 'IN', 'EXISTS', 'BETWEEN',
5
5
  'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'JOIN', 'INNER', 'LEFT', 'RIGHT',
6
6
  'FULL', 'OUTER', 'CROSS', 'POSITIONAL', 'LATERAL', 'ON', 'INTERVAL', 'DAY', 'MONTH', 'YEAR',
7
- 'HOUR', 'MINUTE', 'SECOND', 'FILTER',
7
+ 'HOUR', 'MINUTE', 'SECOND', 'FILTER', 'WITHIN',
8
8
  'UNION', 'INTERSECT', 'EXCEPT',
9
9
  ])
10
10
 
@@ -2,7 +2,7 @@ import { FUNCTION_SIGNATURES } from './functions.js'
2
2
 
3
3
  /** Well-known window functions that are not supported */
4
4
  const WINDOW_FUNCTIONS = new Set([
5
- 'ROW_NUMBER', 'RANK', 'DENSE_RANK', 'NTILE',
5
+ 'RANK', 'DENSE_RANK', 'NTILE',
6
6
  'LAG', 'LEAD', 'FIRST_VALUE', 'LAST_VALUE', 'NTH_VALUE',
7
7
  'CUME_DIST', 'PERCENT_RANK',
8
8
  ])
@@ -119,6 +119,10 @@ export function validateTableRefs(expr, tables) {
119
119
  for (const arg of expr.args) {
120
120
  validateTableRefs(arg, tables)
121
121
  }
122
+ } 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)
122
126
  } else if (expr.type === 'cast') {
123
127
  validateTableRefs(expr.expr, tables)
124
128
  } else if (expr.type === 'in valuelist') {