@checkdigit/eslint-athena-plugin 1.0.0-PR.2-dcdf
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/LICENSE.txt +21 -0
- package/README.md +17 -0
- package/SECURITY.md +13 -0
- package/dist-mjs/athena/api-locator.mjs +66 -0
- package/dist-mjs/athena/api-matcher.mjs +206 -0
- package/dist-mjs/athena/athena.mjs +165 -0
- package/dist-mjs/athena/column.mjs +1 -0
- package/dist-mjs/athena/context.mjs +21 -0
- package/dist-mjs/athena/index.mjs +1 -0
- package/dist-mjs/athena/service-table.mjs +45 -0
- package/dist-mjs/athena/sql-file.mjs +123 -0
- package/dist-mjs/athena/types.mjs +1 -0
- package/dist-mjs/athena/validate.mjs +619 -0
- package/dist-mjs/athena/visitor.mjs +291 -0
- package/dist-mjs/get-documentation-url.mjs +9 -0
- package/dist-mjs/index.mjs +56 -0
- package/dist-mjs/openapi/deref-schema.mjs +20 -0
- package/dist-mjs/openapi/generate-schema.mjs +375 -0
- package/dist-mjs/openapi/service-schema-generator.mjs +176 -0
- package/dist-mjs/peggy/athena-peggy.mjs +20700 -0
- package/dist-mjs/service.mjs +9 -0
- package/dist-mjs/sql-parser.mjs +28 -0
- package/dist-types/athena/api-locator.d.ts +2 -0
- package/dist-types/athena/api-matcher.d.ts +14 -0
- package/dist-types/athena/athena.d.ts +5 -0
- package/dist-types/athena/column.d.ts +1 -0
- package/dist-types/athena/context.d.ts +21 -0
- package/dist-types/athena/index.d.ts +8 -0
- package/dist-types/athena/service-table.d.ts +8 -0
- package/dist-types/athena/sql-file.d.ts +5 -0
- package/dist-types/athena/types.d.ts +493 -0
- package/dist-types/athena/validate.d.ts +14 -0
- package/dist-types/athena/visitor.d.ts +75 -0
- package/dist-types/get-documentation-url.d.ts +1 -0
- package/dist-types/index.d.ts +5 -0
- package/dist-types/openapi/deref-schema.d.ts +1 -0
- package/dist-types/openapi/generate-schema.d.ts +33 -0
- package/dist-types/openapi/service-schema-generator.d.ts +5 -0
- package/dist-types/peggy/athena-peggy.d.ts +13 -0
- package/dist-types/service.d.ts +2 -0
- package/dist-types/sql-parser.d.ts +25 -0
- package/package.json +1 -0
- package/src/api/v1/swagger.yml +619 -0
- package/src/api/v2/swagger.yml +477 -0
- package/src/athena/api-locator.ts +78 -0
- package/src/athena/api-matcher.ts +323 -0
- package/src/athena/athena.ts +224 -0
- package/src/athena/column.ts +4 -0
- package/src/athena/context.ts +47 -0
- package/src/athena/index.ts +13 -0
- package/src/athena/service-table.ts +78 -0
- package/src/athena/sql-file.ts +161 -0
- package/src/athena/types.ts +568 -0
- package/src/athena/validate.ts +902 -0
- package/src/athena/visitor.ts +406 -0
- package/src/get-documentation-url.ts +7 -0
- package/src/index.ts +67 -0
- package/src/openapi/deref-schema.ts +20 -0
- package/src/openapi/generate-schema.ts +553 -0
- package/src/openapi/service-schema-generator.ts +241 -0
- package/src/peggy/athena-peggy.ts +22149 -0
- package/src/peggy/athena.peggy +2971 -0
- package/src/service.ts +11 -0
- package/src/services/eslintAthenaPlugin/v1/swagger.schema.deref.json +1931 -0
- package/src/services/eslintAthenaPlugin/v2/swagger.schema.deref.json +978 -0
- package/src/sql-parser.ts +53 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
// athena/validate.ts
|
|
2
|
+
|
|
3
|
+
/* eslint-disable max-lines */
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Copyright (c) 2021-2026 Check Digit, LLC
|
|
7
|
+
*
|
|
8
|
+
* This code is licensed under the MIT license (see LICENSE.txt for details).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
|
|
13
|
+
import debug from 'debug';
|
|
14
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
15
|
+
import type { SchemaObject } from 'ajv/dist/2020';
|
|
16
|
+
|
|
17
|
+
import type { AST, BaseFrom, From, Select, With } from './types';
|
|
18
|
+
import { matchApi } from './api-matcher.ts';
|
|
19
|
+
import { locateApi } from './api-locator.ts';
|
|
20
|
+
import {
|
|
21
|
+
createChildContext,
|
|
22
|
+
type ResolvedColumn,
|
|
23
|
+
type ResolvedTable,
|
|
24
|
+
type VisitContext,
|
|
25
|
+
} from './context.ts';
|
|
26
|
+
import { buildServiceTables } from './service-table.ts';
|
|
27
|
+
import {
|
|
28
|
+
containsCastToArray,
|
|
29
|
+
containsCastToMap,
|
|
30
|
+
containsLambda,
|
|
31
|
+
extractBracketAccessorPath,
|
|
32
|
+
extractColumnRefs,
|
|
33
|
+
extractJsonExtractCalls,
|
|
34
|
+
extractJsonExtractPath,
|
|
35
|
+
fromClauseItems,
|
|
36
|
+
hasFunctionCalls,
|
|
37
|
+
isBaseFrom,
|
|
38
|
+
isJoin,
|
|
39
|
+
isTableExpr,
|
|
40
|
+
isUnnestFrom,
|
|
41
|
+
isValuesFrom,
|
|
42
|
+
type UnnestFrom,
|
|
43
|
+
} from './visitor.ts';
|
|
44
|
+
|
|
45
|
+
export const SYNTAXT_ERROR = 'SyntaxError';
|
|
46
|
+
export const ATHENA_ERROR = 'AthenaError';
|
|
47
|
+
|
|
48
|
+
export class AthenaError extends Error {
|
|
49
|
+
public code: string;
|
|
50
|
+
public ast?: object;
|
|
51
|
+
constructor(code: string, message: string, ast?: object) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.name = 'AthenaError';
|
|
55
|
+
if (ast !== undefined) {
|
|
56
|
+
this.ast = ast;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convert a 0-based character offset in `text` to a 1-based line / 0-based column ESLint location.
|
|
62
|
+
export function offsetToLoc(
|
|
63
|
+
text: string,
|
|
64
|
+
offset: number,
|
|
65
|
+
): { line: number; column: number } {
|
|
66
|
+
const prefix = text.slice(0, offset);
|
|
67
|
+
const lines = prefix.split('\n');
|
|
68
|
+
return { line: lines.length, column: lines[lines.length - 1]?.length ?? 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const log = debug('eslint-athena-plugin:athena');
|
|
72
|
+
const ANONYMOUS_TABLE = '<anonymous>';
|
|
73
|
+
const SUBQUERY_TABLE = '<subquery>';
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Helpers
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
function resolvedCol(
|
|
80
|
+
name: string,
|
|
81
|
+
schema: SchemaObject,
|
|
82
|
+
ast?: object,
|
|
83
|
+
): ResolvedColumn {
|
|
84
|
+
return ast !== undefined ? { name, schema, ast } : { name, schema };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getApiSchemas(serviceName: string, ctx: VisitContext) {
|
|
88
|
+
let schemas = ctx.apiSchemas.get(serviceName);
|
|
89
|
+
if (schemas === undefined) {
|
|
90
|
+
schemas = locateApi(serviceName);
|
|
91
|
+
ctx.apiSchemas.set(serviceName, schemas);
|
|
92
|
+
}
|
|
93
|
+
return schemas;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function lookupTables(nameOrAlias: string, ctx: VisitContext): ResolvedTable[] {
|
|
97
|
+
// Aliases are stored under their alias key in ctx.tables, so direct lookup wins.
|
|
98
|
+
const canonical = ctx.aliases.get(nameOrAlias) ?? nameOrAlias;
|
|
99
|
+
return ctx.tables.get(nameOrAlias) ?? ctx.tables.get(canonical) ?? [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function flattenTables(ctx: VisitContext): ResolvedTable[] {
|
|
103
|
+
return [...ctx.tables.values()].flat();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveReferencedTables(
|
|
107
|
+
tableRef: string | undefined,
|
|
108
|
+
allTables: ResolvedTable[],
|
|
109
|
+
ctx: VisitContext,
|
|
110
|
+
): ResolvedTable[] {
|
|
111
|
+
return tableRef !== undefined ? lookupTables(tableRef, ctx) : allTables;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveColumnRefParts(ref: {
|
|
115
|
+
table: string | null;
|
|
116
|
+
column: unknown;
|
|
117
|
+
}): {
|
|
118
|
+
tableRef: string | undefined;
|
|
119
|
+
colRef: string | undefined;
|
|
120
|
+
} {
|
|
121
|
+
return {
|
|
122
|
+
tableRef: ref.table ?? undefined,
|
|
123
|
+
colRef: typeof ref.column === 'string' ? ref.column : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Pass 1 — Resolve FROM clause: service tables → ctx.tables + ctx.aliases
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
function resolveServiceTable(
|
|
132
|
+
select: Select,
|
|
133
|
+
item: BaseFrom,
|
|
134
|
+
ctx: VisitContext,
|
|
135
|
+
storageKey?: string,
|
|
136
|
+
): void {
|
|
137
|
+
const { table: tableName } = item;
|
|
138
|
+
const key = storageKey ?? tableName;
|
|
139
|
+
try {
|
|
140
|
+
const apiSchemas = getApiSchemas(tableName, ctx);
|
|
141
|
+
if (apiSchemas.length === 0) {
|
|
142
|
+
throw new AthenaError(
|
|
143
|
+
ATHENA_ERROR,
|
|
144
|
+
`service not found: "${tableName}" (no swagger schema located)`,
|
|
145
|
+
item,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const operations = matchApi(select, item, apiSchemas) ?? [];
|
|
149
|
+
// Always use the canonical table name for ResolvedTable.name so that error messages
|
|
150
|
+
// ("in tables: …") show the service name, not the alias.
|
|
151
|
+
ctx.tables.set(key, buildServiceTables(tableName, operations));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof AthenaError) {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
throw new AthenaError(
|
|
157
|
+
ATHENA_ERROR,
|
|
158
|
+
error instanceof Error ? error.message : String(error),
|
|
159
|
+
item,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Returns the alias name for a ValuesFrom or a standalone UnnestFrom (UNNEST(fn(...))).
|
|
165
|
+
function getFunctionAliasName(
|
|
166
|
+
as: { name: { name: { value: string }[] } } | undefined,
|
|
167
|
+
): string | undefined {
|
|
168
|
+
return as?.name.name[0]?.value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Extracts a column alias name from an alias-arg node. Two cases arise:
|
|
172
|
+
// 1. column_ref (the normal case): { type: 'column_ref', column: 'dates' } → 'dates'
|
|
173
|
+
// 2. zero-arg keyword function: SQL type keywords like `date` are parsed as DATE() function
|
|
174
|
+
// calls; recover the column name by lowercasing the function name.
|
|
175
|
+
function extractColumnAliasName(arg: unknown): string | undefined {
|
|
176
|
+
const colRef = arg as { column?: unknown };
|
|
177
|
+
if (typeof colRef.column === 'string') {
|
|
178
|
+
return colRef.column;
|
|
179
|
+
}
|
|
180
|
+
const fnNode = arg as {
|
|
181
|
+
type?: unknown;
|
|
182
|
+
name?: { name?: { value?: string }[] };
|
|
183
|
+
args?: { value?: unknown[] };
|
|
184
|
+
};
|
|
185
|
+
if (fnNode.type === 'function' && fnNode.args?.value?.length === 0) {
|
|
186
|
+
return fnNode.name?.name?.[0]?.value?.toLowerCase();
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// UNNEST whose argument is a function call (e.g. SEQUENCE), not a column reference.
|
|
192
|
+
function isStandaloneUnnest(node: unknown): node is UnnestFrom {
|
|
193
|
+
return (
|
|
194
|
+
isUnnestFrom(node) &&
|
|
195
|
+
typeof (node.expr as { column?: unknown }).column !== 'string'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function aliasAsSingleton(alias: string | undefined): string[] {
|
|
200
|
+
return alias !== undefined ? [alias] : [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Returns every name under which this FROM item can be stored in ctx.tables.
|
|
204
|
+
// Both the alias key and the canonical table name are returned so that service
|
|
205
|
+
// tables (stored under alias) and CTEs (stored under canonical name) both survive
|
|
206
|
+
// the prune in restrictToFromClause.
|
|
207
|
+
function fromItemTableNames(item: From): string[] {
|
|
208
|
+
if (isBaseFrom(item) || isJoin(item)) {
|
|
209
|
+
return item.as !== null ? [item.as, item.table] : [item.table];
|
|
210
|
+
}
|
|
211
|
+
if (isTableExpr(item)) {
|
|
212
|
+
return [typeof item.as === 'string' ? item.as : SUBQUERY_TABLE];
|
|
213
|
+
}
|
|
214
|
+
if (isValuesFrom(item)) {
|
|
215
|
+
return aliasAsSingleton(getFunctionAliasName(item.as));
|
|
216
|
+
}
|
|
217
|
+
const unknownItem = item as unknown;
|
|
218
|
+
if (isStandaloneUnnest(unknownItem)) {
|
|
219
|
+
return aliasAsSingleton(getFunctionAliasName(unknownItem.as ?? undefined));
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function restrictToFromClause(select: Select, ctx: VisitContext): void {
|
|
225
|
+
const fromNames = new Set(
|
|
226
|
+
fromClauseItems(select).flatMap(fromItemTableNames),
|
|
227
|
+
);
|
|
228
|
+
for (const name of [...ctx.tables.keys()]) {
|
|
229
|
+
if (!fromNames.has(name)) {
|
|
230
|
+
ctx.tables.delete(name);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function tableIsResolved(
|
|
236
|
+
tableName: string,
|
|
237
|
+
storageKey: string,
|
|
238
|
+
ctx: VisitContext,
|
|
239
|
+
): boolean {
|
|
240
|
+
return ctx.tables.has(storageKey) || ctx.tables.has(tableName);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function registerAliasedTable(
|
|
244
|
+
tableAlias: string,
|
|
245
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
246
|
+
ctx: VisitContext,
|
|
247
|
+
): void {
|
|
248
|
+
ctx.tables.set(tableAlias, [{ name: tableAlias, columns }]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Shared resolution logic for BaseFrom and Join items (Join extends BaseFrom).
|
|
252
|
+
function resolveTableOrJoinItem(
|
|
253
|
+
select: Select,
|
|
254
|
+
item: BaseFrom,
|
|
255
|
+
ctx: VisitContext,
|
|
256
|
+
): void {
|
|
257
|
+
const { table: tableName, as: alias } = item;
|
|
258
|
+
if (alias !== null) {
|
|
259
|
+
ctx.aliases.set(alias, tableName);
|
|
260
|
+
}
|
|
261
|
+
const storageKey = alias ?? tableName;
|
|
262
|
+
// Skip if already resolved: CTE stored under canonical name, or duplicate alias.
|
|
263
|
+
if (!tableIsResolved(tableName, storageKey, ctx)) {
|
|
264
|
+
resolveServiceTable(select, item, ctx, alias ?? undefined);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resolveFromClause(select: Select, ctx: VisitContext): void {
|
|
269
|
+
for (const item of fromClauseItems(select)) {
|
|
270
|
+
const unknownItem = item as unknown;
|
|
271
|
+
if (isStandaloneUnnest(unknownItem)) {
|
|
272
|
+
// Standalone UNNEST(fn()) — register alias with declared column names (schema unknown).
|
|
273
|
+
const tableAlias = getFunctionAliasName(unknownItem.as ?? undefined);
|
|
274
|
+
if (tableAlias !== undefined) {
|
|
275
|
+
const columns = new Map(
|
|
276
|
+
(unknownItem.as?.args.value ?? []).map((columnRef) => {
|
|
277
|
+
const colName = extractColumnAliasName(columnRef) ?? '';
|
|
278
|
+
return [colName, [resolvedCol(colName, {})]];
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
registerAliasedTable(tableAlias, columns, ctx);
|
|
282
|
+
}
|
|
283
|
+
} else if (isUnnestFrom(unknownItem)) {
|
|
284
|
+
// CROSS JOIN UNNEST with a column ref — handled later by extractUnnestMappings.
|
|
285
|
+
} else if (isValuesFrom(item)) {
|
|
286
|
+
const tableAlias = getFunctionAliasName(item.as);
|
|
287
|
+
if (tableAlias !== undefined) {
|
|
288
|
+
const columns = new Map(
|
|
289
|
+
item.as.args.value.map((columnRef) => [
|
|
290
|
+
columnRef.column,
|
|
291
|
+
[resolvedCol(columnRef.column, {})],
|
|
292
|
+
]),
|
|
293
|
+
);
|
|
294
|
+
registerAliasedTable(tableAlias, columns, ctx);
|
|
295
|
+
}
|
|
296
|
+
} else if (isTableExpr(item)) {
|
|
297
|
+
const alias = typeof item.as === 'string' ? item.as : SUBQUERY_TABLE;
|
|
298
|
+
// eslint-disable-next-line no-use-before-define
|
|
299
|
+
checkSelect(item.expr.ast, ctx, alias);
|
|
300
|
+
} else if (isJoin(item) || isBaseFrom(item)) {
|
|
301
|
+
resolveTableOrJoinItem(select, item, ctx);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// UNNEST handling
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
interface UnnestMapping {
|
|
311
|
+
fromColumn: string;
|
|
312
|
+
toColumns: string[];
|
|
313
|
+
tableAlias?: string;
|
|
314
|
+
ast: object;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function extractUnnestMappings(select: Select): UnnestMapping[] {
|
|
318
|
+
const mappings: UnnestMapping[] = [];
|
|
319
|
+
|
|
320
|
+
for (const item of fromClauseItems(select)) {
|
|
321
|
+
if (!isUnnestFrom(item)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const fromColumn =
|
|
326
|
+
typeof (item.expr as { column?: unknown }).column === 'string'
|
|
327
|
+
? (item.expr as { column: string }).column
|
|
328
|
+
: undefined;
|
|
329
|
+
if (fromColumn === undefined) {
|
|
330
|
+
// Standalone UNNEST (e.g. UNNEST(SEQUENCE(...))) — registered in resolveFromClause.
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const aliasArgs = item.as?.args.value ?? [];
|
|
335
|
+
const toColumns: string[] = [];
|
|
336
|
+
for (const col of aliasArgs) {
|
|
337
|
+
const colName = extractColumnAliasName(col);
|
|
338
|
+
if (colName !== undefined) {
|
|
339
|
+
toColumns.push(colName);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
assert.ok(
|
|
343
|
+
toColumns.length > 0,
|
|
344
|
+
'UNNEST alias must have at least one column name',
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const tableAlias = getFunctionAliasName(item.as ?? undefined);
|
|
348
|
+
mappings.push({
|
|
349
|
+
fromColumn,
|
|
350
|
+
toColumns,
|
|
351
|
+
...(tableAlias !== undefined ? { tableAlias } : {}),
|
|
352
|
+
ast: item.expr,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return mappings;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Resolves UNNEST source schema into target column entries.
|
|
360
|
+
// When hasKnownApiOperation is true, an unrecognized schema type throws; otherwise unknown columns
|
|
361
|
+
// are registered with an empty schema (the schema is an estimate for non-API sources).
|
|
362
|
+
function buildUnnestColumnMap(
|
|
363
|
+
fromColumn: string,
|
|
364
|
+
toColumns: string[],
|
|
365
|
+
sourceSchema: ResolvedColumn['schema'] | undefined,
|
|
366
|
+
sourceAst: object | undefined,
|
|
367
|
+
ast: object,
|
|
368
|
+
hasKnownApiOperation: boolean,
|
|
369
|
+
): Map<string, ResolvedColumn[]> {
|
|
370
|
+
if (sourceSchema?.type === 'array') {
|
|
371
|
+
const [toColumn] = toColumns;
|
|
372
|
+
assert.ok(toColumn !== undefined);
|
|
373
|
+
return new Map([
|
|
374
|
+
[toColumn, [resolvedCol(toColumn, sourceSchema.items, sourceAst)]],
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
if (sourceSchema?.type === 'object') {
|
|
378
|
+
const [keyColumn, valueColumn] = toColumns;
|
|
379
|
+
assert.ok(
|
|
380
|
+
keyColumn !== undefined && valueColumn !== undefined,
|
|
381
|
+
`UNNEST of map column '${fromColumn}' requires exactly two alias columns (key, value)`,
|
|
382
|
+
);
|
|
383
|
+
const addlProps = (sourceSchema as SchemaObject)[
|
|
384
|
+
'additionalProperties'
|
|
385
|
+
] as unknown;
|
|
386
|
+
const valueSchema: SchemaObject =
|
|
387
|
+
typeof addlProps === 'object' && addlProps !== null
|
|
388
|
+
? addlProps
|
|
389
|
+
: { type: 'string' };
|
|
390
|
+
return new Map([
|
|
391
|
+
[keyColumn, [resolvedCol(keyColumn, { type: 'string' }, sourceAst)]],
|
|
392
|
+
[valueColumn, [resolvedCol(valueColumn, valueSchema, sourceAst)]],
|
|
393
|
+
]);
|
|
394
|
+
}
|
|
395
|
+
if (!hasKnownApiOperation) {
|
|
396
|
+
return new Map(
|
|
397
|
+
toColumns.map((toColumn) => [toColumn, [resolvedCol(toColumn, {})]]),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
throw new AthenaError(
|
|
401
|
+
ATHENA_ERROR,
|
|
402
|
+
`UNNEST source column '${fromColumn}' must resolve to an array or map schema`,
|
|
403
|
+
ast,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function applyUnnestPre(
|
|
408
|
+
mappings: UnnestMapping[],
|
|
409
|
+
ctx: VisitContext,
|
|
410
|
+
): UnnestMapping[] {
|
|
411
|
+
const deferred: UnnestMapping[] = [];
|
|
412
|
+
|
|
413
|
+
for (const { fromColumn, toColumns, tableAlias, ast } of mappings) {
|
|
414
|
+
const ownerTable = flattenTables(ctx).find((table) =>
|
|
415
|
+
table.columns.has(fromColumn),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
if (ownerTable === undefined) {
|
|
419
|
+
deferred.push({
|
|
420
|
+
fromColumn,
|
|
421
|
+
toColumns,
|
|
422
|
+
...(tableAlias !== undefined ? { tableAlias } : {}),
|
|
423
|
+
ast,
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const sourceColumns = ownerTable.columns.get(fromColumn) ?? [];
|
|
429
|
+
const unnestTableName = `${ownerTable.name ?? ANONYMOUS_TABLE}:<unnested>`;
|
|
430
|
+
const apiOperation =
|
|
431
|
+
ownerTable.apiOperation !== undefined
|
|
432
|
+
? { apiOperation: ownerTable.apiOperation }
|
|
433
|
+
: {};
|
|
434
|
+
const unnestColumns = buildUnnestColumnMap(
|
|
435
|
+
fromColumn,
|
|
436
|
+
toColumns,
|
|
437
|
+
sourceColumns[0]?.schema,
|
|
438
|
+
sourceColumns[0]?.ast,
|
|
439
|
+
ast,
|
|
440
|
+
ownerTable.apiOperation !== undefined,
|
|
441
|
+
);
|
|
442
|
+
const unnestEntry: ResolvedTable[] = [
|
|
443
|
+
{ name: unnestTableName, ...apiOperation, columns: unnestColumns },
|
|
444
|
+
];
|
|
445
|
+
ctx.tables.set(unnestTableName, unnestEntry);
|
|
446
|
+
if (tableAlias !== undefined) {
|
|
447
|
+
ctx.tables.set(tableAlias, unnestEntry);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return deferred;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function applyUnnestPost(
|
|
455
|
+
mappings: UnnestMapping[],
|
|
456
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
457
|
+
): void {
|
|
458
|
+
for (const { fromColumn, toColumns, ast } of mappings) {
|
|
459
|
+
const sourceColumns = columns.get(fromColumn);
|
|
460
|
+
if (sourceColumns === undefined) {
|
|
461
|
+
throw new AthenaError(
|
|
462
|
+
ATHENA_ERROR,
|
|
463
|
+
`UNNEST source column '${fromColumn}' not found in SELECT`,
|
|
464
|
+
ast,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const unnestColumns = buildUnnestColumnMap(
|
|
468
|
+
fromColumn,
|
|
469
|
+
toColumns,
|
|
470
|
+
sourceColumns[0]?.schema,
|
|
471
|
+
sourceColumns[0]?.ast,
|
|
472
|
+
ast,
|
|
473
|
+
true,
|
|
474
|
+
);
|
|
475
|
+
for (const [key, value] of unnestColumns) {
|
|
476
|
+
columns.set(key, value);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Pass 2 — Resolve SELECT columns helpers
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
function resolveDefaultSchemaColumn(
|
|
486
|
+
columnAlias: string | undefined,
|
|
487
|
+
indexedName: string,
|
|
488
|
+
columnAST: unknown,
|
|
489
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
490
|
+
): void {
|
|
491
|
+
const name = columnAlias ?? indexedName;
|
|
492
|
+
columns.set(name, [
|
|
493
|
+
resolvedCol(name, { type: 'string' }, columnAST as object),
|
|
494
|
+
]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function expandWildcard(
|
|
498
|
+
referencedTables: ResolvedTable[],
|
|
499
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
500
|
+
): void {
|
|
501
|
+
for (const table of referencedTables) {
|
|
502
|
+
for (const [colName, cols] of table.columns) {
|
|
503
|
+
columns.set(colName, cols);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function schemaPropertyHint(resolvedColumns: ResolvedColumn[]): string {
|
|
509
|
+
if (resolvedColumns.length === 0) {
|
|
510
|
+
return '';
|
|
511
|
+
}
|
|
512
|
+
const firstSchema = resolvedColumns[0]?.schema;
|
|
513
|
+
if (
|
|
514
|
+
!resolvedColumns.every(
|
|
515
|
+
(col) => JSON.stringify(col.schema) === JSON.stringify(firstSchema),
|
|
516
|
+
)
|
|
517
|
+
) {
|
|
518
|
+
return '';
|
|
519
|
+
}
|
|
520
|
+
if (firstSchema?.type !== 'object' || firstSchema.properties === undefined) {
|
|
521
|
+
return '';
|
|
522
|
+
}
|
|
523
|
+
const propNames = Object.keys(firstSchema.properties);
|
|
524
|
+
return propNames.length > 0
|
|
525
|
+
? `; available properties: ${propNames.join(', ')}`
|
|
526
|
+
: '';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function resolveSchemaAtPath(
|
|
530
|
+
colRef: string,
|
|
531
|
+
propertyAccessor: string,
|
|
532
|
+
resolvedColumns: ResolvedColumn[],
|
|
533
|
+
ast?: object,
|
|
534
|
+
): SchemaObject[] {
|
|
535
|
+
// Double-dot handles allOf / anyOf / oneOf wrappers that may appear in the schema.
|
|
536
|
+
const adjustedPath = `$.${propertyAccessor.substring(1).replace(/(?<sep>\.|\[)/gu, '..properties$<sep>')}`;
|
|
537
|
+
log('adjusted path', adjustedPath);
|
|
538
|
+
|
|
539
|
+
const extractedSchemas = resolvedColumns.flatMap((col) =>
|
|
540
|
+
JSONPath<SchemaObject[]>({ json: col.schema, path: adjustedPath }),
|
|
541
|
+
);
|
|
542
|
+
log('extracted schemas', extractedSchemas);
|
|
543
|
+
|
|
544
|
+
if (extractedSchemas.length === 0) {
|
|
545
|
+
throw new AthenaError(
|
|
546
|
+
ATHENA_ERROR,
|
|
547
|
+
`Column "${colRef}" has no property at path "${propertyAccessor}"${schemaPropertyHint(resolvedColumns)}`,
|
|
548
|
+
ast,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return extractedSchemas;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function navigateSchemaPath(
|
|
555
|
+
colRef: string,
|
|
556
|
+
propertyAccessor: string,
|
|
557
|
+
resolvedColumns: ResolvedColumn[],
|
|
558
|
+
colName: string,
|
|
559
|
+
columnAST: unknown,
|
|
560
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
561
|
+
): void {
|
|
562
|
+
const errorAst = (extractJsonExtractCalls(columnAST)[0]?.fnNode ??
|
|
563
|
+
columnAST) as object;
|
|
564
|
+
const extractedSchemas = resolveSchemaAtPath(
|
|
565
|
+
colRef,
|
|
566
|
+
propertyAccessor,
|
|
567
|
+
resolvedColumns,
|
|
568
|
+
errorAst,
|
|
569
|
+
);
|
|
570
|
+
columns.set(
|
|
571
|
+
colName,
|
|
572
|
+
extractedSchemas.map((schema) =>
|
|
573
|
+
resolvedCol(colName, schema, columnAST as object),
|
|
574
|
+
),
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// Pass 2 — Resolve SELECT columns → Map<name, ResolvedColumn[]>
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
function throwUnknownTableError(
|
|
583
|
+
tableRef: string | undefined,
|
|
584
|
+
colRef: string,
|
|
585
|
+
ctx: VisitContext,
|
|
586
|
+
ref: object,
|
|
587
|
+
): never {
|
|
588
|
+
throw new AthenaError(
|
|
589
|
+
ATHENA_ERROR,
|
|
590
|
+
`Table or alias "${tableRef ?? colRef}" does not exist. Known tables: ${[...ctx.tables.keys()].join(', ')}`,
|
|
591
|
+
ref,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function resolveReferencedTablesOrThrow(
|
|
596
|
+
tableRef: string | undefined,
|
|
597
|
+
colRef: string,
|
|
598
|
+
allTables: ResolvedTable[],
|
|
599
|
+
ctx: VisitContext,
|
|
600
|
+
ref: object,
|
|
601
|
+
): ResolvedTable[] {
|
|
602
|
+
const referencedTables = resolveReferencedTables(tableRef, allTables, ctx);
|
|
603
|
+
if (referencedTables.length === 0) {
|
|
604
|
+
throwUnknownTableError(tableRef, colRef, ctx, ref);
|
|
605
|
+
}
|
|
606
|
+
return referencedTables;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function lookupColumnOrThrow(
|
|
610
|
+
colRef: string,
|
|
611
|
+
ref: object,
|
|
612
|
+
referencedTables: ResolvedTable[],
|
|
613
|
+
): ResolvedColumn[] {
|
|
614
|
+
const resolvedColumns = referencedTables.flatMap(
|
|
615
|
+
(table) => table.columns.get(colRef) ?? [],
|
|
616
|
+
);
|
|
617
|
+
if (resolvedColumns.length === 0) {
|
|
618
|
+
const tableNames = [
|
|
619
|
+
...new Set(
|
|
620
|
+
referencedTables.map(
|
|
621
|
+
(referenceTable) => referenceTable.name ?? ANONYMOUS_TABLE,
|
|
622
|
+
),
|
|
623
|
+
),
|
|
624
|
+
].join(', ');
|
|
625
|
+
const availableCols = [
|
|
626
|
+
...new Set(
|
|
627
|
+
referencedTables.flatMap((table) => [...table.columns.keys()]),
|
|
628
|
+
),
|
|
629
|
+
].join(', ');
|
|
630
|
+
throw new AthenaError(
|
|
631
|
+
ATHENA_ERROR,
|
|
632
|
+
`Column "${colRef}" does not exist in table(s) ${tableNames}. Available columns: ${availableCols}`,
|
|
633
|
+
ref,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return resolvedColumns;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function checkColumnRefsExist(
|
|
640
|
+
ast: unknown,
|
|
641
|
+
allTables: ResolvedTable[],
|
|
642
|
+
ctx: VisitContext,
|
|
643
|
+
selectColumns?: Map<string, ResolvedColumn[]>,
|
|
644
|
+
): void {
|
|
645
|
+
if (containsLambda(ast)) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
for (const ref of extractColumnRefs(ast)) {
|
|
649
|
+
const { tableRef, colRef } = resolveColumnRefParts(ref);
|
|
650
|
+
if (colRef === undefined || colRef === '*') {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (tableRef === undefined && selectColumns?.has(colRef) === true) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const referencedTables = resolveReferencedTablesOrThrow(
|
|
657
|
+
tableRef,
|
|
658
|
+
colRef,
|
|
659
|
+
allTables,
|
|
660
|
+
ctx,
|
|
661
|
+
ref,
|
|
662
|
+
);
|
|
663
|
+
lookupColumnOrThrow(colRef, ref, referencedTables);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function validateComplexColumnExpression(
|
|
668
|
+
columnAST: unknown,
|
|
669
|
+
allTables: ResolvedTable[],
|
|
670
|
+
ctx: VisitContext,
|
|
671
|
+
): void {
|
|
672
|
+
for (const { ref, path, fnNode } of extractJsonExtractCalls(columnAST)) {
|
|
673
|
+
const { tableRef, colRef } = resolveColumnRefParts(ref);
|
|
674
|
+
if (colRef === undefined) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const referencedTables = resolveReferencedTables(tableRef, allTables, ctx);
|
|
678
|
+
const resolvedColumns = referencedTables.flatMap(
|
|
679
|
+
(table) => table.columns.get(colRef) ?? [],
|
|
680
|
+
);
|
|
681
|
+
if (resolvedColumns.length > 0) {
|
|
682
|
+
resolveSchemaAtPath(colRef, path, resolvedColumns, fnNode);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function resolveSingleColumnRef(
|
|
688
|
+
columnAST: unknown,
|
|
689
|
+
columnAlias: string | undefined,
|
|
690
|
+
indexedName: string,
|
|
691
|
+
ref: NonNullable<ReturnType<typeof extractColumnRefs>[number]>,
|
|
692
|
+
allTables: ResolvedTable[],
|
|
693
|
+
ctx: VisitContext,
|
|
694
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
695
|
+
): void {
|
|
696
|
+
const { tableRef, colRef } = resolveColumnRefParts(ref);
|
|
697
|
+
assert.ok(colRef !== undefined, 'column_ref must have a string column name');
|
|
698
|
+
|
|
699
|
+
const referencedTables = resolveReferencedTablesOrThrow(
|
|
700
|
+
tableRef,
|
|
701
|
+
colRef,
|
|
702
|
+
allTables,
|
|
703
|
+
ctx,
|
|
704
|
+
ref,
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
if (colRef === '*') {
|
|
708
|
+
expandWildcard(referencedTables, columns);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const withFunctions = hasFunctionCalls(columnAST);
|
|
713
|
+
const colName = columnAlias ?? (withFunctions ? indexedName : colRef);
|
|
714
|
+
const resolvedColumns = lookupColumnOrThrow(colRef, ref, referencedTables);
|
|
715
|
+
|
|
716
|
+
const propertyAccessor =
|
|
717
|
+
extractJsonExtractPath(columnAST) ?? extractBracketAccessorPath(columnAST);
|
|
718
|
+
if (propertyAccessor !== undefined) {
|
|
719
|
+
navigateSchemaPath(
|
|
720
|
+
colRef,
|
|
721
|
+
propertyAccessor,
|
|
722
|
+
resolvedColumns,
|
|
723
|
+
colName,
|
|
724
|
+
columnAST,
|
|
725
|
+
columns,
|
|
726
|
+
);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
columns.set(
|
|
731
|
+
colName,
|
|
732
|
+
resolvedColumns.map((col) =>
|
|
733
|
+
resolvedCol(colName, col.schema, columnAST as object),
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function applySchemaTypeOverride(
|
|
739
|
+
columnAST: unknown,
|
|
740
|
+
columnAlias: string | undefined,
|
|
741
|
+
indexedName: string,
|
|
742
|
+
columns: Map<string, ResolvedColumn[]>,
|
|
743
|
+
predicate: (expression: unknown) => boolean,
|
|
744
|
+
schemaType: 'array' | 'object',
|
|
745
|
+
): void {
|
|
746
|
+
if (!predicate(columnAST)) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const colName = columnAlias ?? indexedName;
|
|
750
|
+
const existing = columns.get(colName);
|
|
751
|
+
if (existing !== undefined && existing[0]?.schema.type !== schemaType) {
|
|
752
|
+
columns.set(
|
|
753
|
+
colName,
|
|
754
|
+
existing.map((col) =>
|
|
755
|
+
resolvedCol(col.name, { type: schemaType }, col.ast),
|
|
756
|
+
),
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function validateClauseExpression(
|
|
762
|
+
expression: unknown,
|
|
763
|
+
allTables: ResolvedTable[],
|
|
764
|
+
ctx: VisitContext,
|
|
765
|
+
selectColumns?: Map<string, ResolvedColumn[]>,
|
|
766
|
+
): void {
|
|
767
|
+
checkColumnRefsExist(expression, allTables, ctx, selectColumns);
|
|
768
|
+
validateComplexColumnExpression(expression, allTables, ctx);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function resolveSelectColumns(
|
|
772
|
+
select: Select,
|
|
773
|
+
ctx: VisitContext,
|
|
774
|
+
): Map<string, ResolvedColumn[]> {
|
|
775
|
+
const allTables = flattenTables(ctx);
|
|
776
|
+
const columns = new Map<string, ResolvedColumn[]>();
|
|
777
|
+
|
|
778
|
+
for (const [index, columnAST] of select.columns.entries()) {
|
|
779
|
+
log('resolving column', columnAST);
|
|
780
|
+
|
|
781
|
+
const columnAlias = (columnAST as { as?: string | null }).as ?? undefined;
|
|
782
|
+
const indexedName = `_col${String(index)}`;
|
|
783
|
+
const columnRefs = extractColumnRefs(columnAST);
|
|
784
|
+
|
|
785
|
+
if (columnRefs.length !== 1) {
|
|
786
|
+
validateClauseExpression(columnAST, allTables, ctx);
|
|
787
|
+
resolveDefaultSchemaColumn(columnAlias, indexedName, columnAST, columns);
|
|
788
|
+
} else {
|
|
789
|
+
const [ref] = columnRefs;
|
|
790
|
+
assert.ok(ref !== undefined);
|
|
791
|
+
resolveSingleColumnRef(
|
|
792
|
+
columnAST,
|
|
793
|
+
columnAlias,
|
|
794
|
+
indexedName,
|
|
795
|
+
ref,
|
|
796
|
+
allTables,
|
|
797
|
+
ctx,
|
|
798
|
+
columns,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
applySchemaTypeOverride(
|
|
803
|
+
columnAST,
|
|
804
|
+
columnAlias,
|
|
805
|
+
indexedName,
|
|
806
|
+
columns,
|
|
807
|
+
containsCastToArray,
|
|
808
|
+
'array',
|
|
809
|
+
);
|
|
810
|
+
applySchemaTypeOverride(
|
|
811
|
+
columnAST,
|
|
812
|
+
columnAlias,
|
|
813
|
+
indexedName,
|
|
814
|
+
columns,
|
|
815
|
+
containsCastToMap,
|
|
816
|
+
'object',
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return columns;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ---------------------------------------------------------------------------
|
|
824
|
+
// Top-level SELECT resolution
|
|
825
|
+
// ---------------------------------------------------------------------------
|
|
826
|
+
|
|
827
|
+
function checkSelect(
|
|
828
|
+
selectAST: Select | With,
|
|
829
|
+
ctx: VisitContext,
|
|
830
|
+
withTableName?: string,
|
|
831
|
+
): Map<string, ResolvedColumn[]> {
|
|
832
|
+
const select = 'stmt' in selectAST ? selectAST.stmt.ast : selectAST;
|
|
833
|
+
const selectCtx = createChildContext(ctx);
|
|
834
|
+
|
|
835
|
+
resolveFromClause(select, selectCtx);
|
|
836
|
+
restrictToFromClause(select, selectCtx);
|
|
837
|
+
|
|
838
|
+
const unnestMappings = extractUnnestMappings(select);
|
|
839
|
+
const deferredUnnest = applyUnnestPre(unnestMappings, selectCtx);
|
|
840
|
+
|
|
841
|
+
const columns = resolveSelectColumns(select, selectCtx);
|
|
842
|
+
|
|
843
|
+
applyUnnestPost(deferredUnnest, columns);
|
|
844
|
+
|
|
845
|
+
log('resolved columns', [...columns.keys()]);
|
|
846
|
+
|
|
847
|
+
const allTables = flattenTables(selectCtx);
|
|
848
|
+
for (const item of fromClauseItems(select)) {
|
|
849
|
+
const onExpr = (item as { on?: unknown }).on;
|
|
850
|
+
if (onExpr !== undefined) {
|
|
851
|
+
validateClauseExpression(onExpr, allTables, selectCtx);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (select.where !== null) {
|
|
855
|
+
validateClauseExpression(select.where, allTables, selectCtx);
|
|
856
|
+
}
|
|
857
|
+
if (select.having !== null) {
|
|
858
|
+
validateClauseExpression(select.having, allTables, selectCtx, columns);
|
|
859
|
+
}
|
|
860
|
+
for (const orderItem of select.orderby ?? []) {
|
|
861
|
+
validateClauseExpression(orderItem.expr, allTables, selectCtx, columns);
|
|
862
|
+
}
|
|
863
|
+
if (select.groupby?.columns !== undefined) {
|
|
864
|
+
for (const groupCol of select.groupby.columns) {
|
|
865
|
+
validateClauseExpression(groupCol, allTables, selectCtx, columns);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (select._next !== undefined) {
|
|
870
|
+
const nextColumns = checkSelect(select._next, ctx, withTableName);
|
|
871
|
+
if (columns.size !== nextColumns.size) {
|
|
872
|
+
throw new AthenaError(
|
|
873
|
+
ATHENA_ERROR,
|
|
874
|
+
`UNION ALL parts have different number of columns: ${columns.size.toString()} vs ${nextColumns.size.toString()}`,
|
|
875
|
+
select._next,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (withTableName !== undefined) {
|
|
881
|
+
const resolvedTable: ResolvedTable = { name: withTableName, columns };
|
|
882
|
+
ctx.tables.set(withTableName, [resolvedTable]);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return columns;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function checkAthenaAst(ast: AST, ctx: VisitContext): void {
|
|
889
|
+
assert.ok(ast.type === 'select');
|
|
890
|
+
const select = ast;
|
|
891
|
+
|
|
892
|
+
if (select.with !== null) {
|
|
893
|
+
for (const withItem of select.with) {
|
|
894
|
+
checkSelect(withItem.stmt.ast, ctx, withItem.name.value);
|
|
895
|
+
}
|
|
896
|
+
select.with = null;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
checkSelect(select, ctx);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* eslint-enable max-lines */
|