@checkdigit/eslint-plugin 7.18.0-PR.143-e2e9 → 7.18.0-PR.143-cf9d

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.
@@ -27,6 +27,7 @@ import {
27
27
  extractColumnRefs,
28
28
  extractJsonExtractCalls,
29
29
  extractJsonExtractPath,
30
+ fromClauseItems,
30
31
  hasFunctionCalls,
31
32
  isBaseFrom,
32
33
  isJoin,
@@ -61,6 +62,7 @@ export function offsetToLoc(text: string, offset: number): { line: number; colum
61
62
 
62
63
  const log = debug('eslint-plugin:athena');
63
64
  const ANONYMOUS_TABLE = '<anonymous>';
65
+ const SUBQUERY_TABLE = '<subquery>';
64
66
 
65
67
  // ---------------------------------------------------------------------------
66
68
  // Helpers
@@ -80,22 +82,31 @@ function getApiSchemas(serviceName: string, ctx: VisitContext) {
80
82
  }
81
83
 
82
84
  function lookupTables(nameOrAlias: string, ctx: VisitContext): ResolvedTable[] {
83
- // Direct hit first: aliases are now stored under their alias key in ctx.tables.
84
- if (ctx.tables.has(nameOrAlias)) {
85
- return ctx.tables.get(nameOrAlias) ?? [];
86
- }
85
+ // Aliases are stored under their alias key in ctx.tables, so direct lookup wins.
87
86
  const canonical = ctx.aliases.get(nameOrAlias) ?? nameOrAlias;
88
- return ctx.tables.get(canonical) ?? [];
87
+ return ctx.tables.get(nameOrAlias) ?? ctx.tables.get(canonical) ?? [];
89
88
  }
90
89
 
91
- function fromClauseItems(select: Select): From[] {
92
- if (Array.isArray(select.from)) {
93
- return select.from;
94
- }
95
- if (select.from !== null) {
96
- return [select.from];
97
- }
98
- return [];
90
+ function flattenTables(ctx: VisitContext): ResolvedTable[] {
91
+ return [...ctx.tables.values()].flat();
92
+ }
93
+
94
+ function resolveReferencedTables(
95
+ tableRef: string | undefined,
96
+ allTables: ResolvedTable[],
97
+ ctx: VisitContext,
98
+ ): ResolvedTable[] {
99
+ return tableRef !== undefined ? lookupTables(tableRef, ctx) : allTables;
100
+ }
101
+
102
+ function resolveColumnRefParts(ref: { table: string | null; column: unknown }): {
103
+ tableRef: string | undefined;
104
+ colRef: string | undefined;
105
+ } {
106
+ return {
107
+ tableRef: ref.table ?? undefined,
108
+ colRef: typeof ref.column === 'string' ? ref.column : undefined,
109
+ };
99
110
  }
100
111
 
101
112
  // ---------------------------------------------------------------------------
@@ -138,8 +149,7 @@ function extractColumnAliasName(arg: unknown): string | undefined {
138
149
  }
139
150
  const fnNode = arg as { type?: unknown; name?: { name?: { value?: string }[] }; args?: { value?: unknown[] } };
140
151
  if (fnNode.type === 'function' && fnNode.args?.value?.length === 0) {
141
- const functionName = fnNode.name?.name?.[0]?.value;
142
- return functionName !== undefined ? functionName.toLowerCase() : undefined;
152
+ return fnNode.name?.name?.[0]?.value?.toLowerCase();
143
153
  }
144
154
  return undefined;
145
155
  }
@@ -149,6 +159,10 @@ function isStandaloneUnnest(node: unknown): node is UnnestFrom {
149
159
  return isUnnestFrom(node) && typeof (node.expr as { column?: unknown }).column !== 'string';
150
160
  }
151
161
 
162
+ function aliasAsSingleton(alias: string | undefined): string[] {
163
+ return alias !== undefined ? [alias] : [];
164
+ }
165
+
152
166
  // Returns every name under which this FROM item can be stored in ctx.tables.
153
167
  // Both the alias key and the canonical table name are returned so that service
154
168
  // tables (stored under alias) and CTEs (stored under canonical name) both survive
@@ -158,16 +172,14 @@ function fromItemTableNames(item: From): string[] {
158
172
  return item.as !== null ? [item.as, item.table] : [item.table];
159
173
  }
160
174
  if (isTableExpr(item)) {
161
- return [typeof item.as === 'string' ? item.as : '<subquery>'];
175
+ return [typeof item.as === 'string' ? item.as : SUBQUERY_TABLE];
162
176
  }
163
177
  if (isValuesFrom(item)) {
164
- const alias = getFunctionAliasName(item.as);
165
- return alias !== undefined ? [alias] : [];
178
+ return aliasAsSingleton(getFunctionAliasName(item.as));
166
179
  }
167
180
  const unknownItem = item as unknown;
168
181
  if (isStandaloneUnnest(unknownItem)) {
169
- const alias = getFunctionAliasName(unknownItem.as);
170
- return alias !== undefined ? [alias] : [];
182
+ return aliasAsSingleton(getFunctionAliasName(unknownItem.as));
171
183
  }
172
184
  return [];
173
185
  }
@@ -185,6 +197,10 @@ function tableIsResolved(tableName: string, storageKey: string, ctx: VisitContex
185
197
  return ctx.tables.has(storageKey) || ctx.tables.has(tableName);
186
198
  }
187
199
 
200
+ function registerAliasedTable(tableAlias: string, columns: Map<string, ResolvedColumn[]>, ctx: VisitContext): void {
201
+ ctx.tables.set(tableAlias, [{ name: tableAlias, columns }]);
202
+ }
203
+
188
204
  // Shared resolution logic for BaseFrom and Join items (Join extends BaseFrom).
189
205
  function resolveTableOrJoinItem(select: Select, item: BaseFrom, ctx: VisitContext): void {
190
206
  const { table: tableName, as: alias } = item;
@@ -211,7 +227,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
211
227
  return [colName, [resolvedCol(colName, {})]];
212
228
  }),
213
229
  );
214
- ctx.tables.set(tableAlias, [{ name: tableAlias, columns }]);
230
+ registerAliasedTable(tableAlias, columns, ctx);
215
231
  }
216
232
  } else if (isUnnestFrom(unknownItem)) {
217
233
  // CROSS JOIN UNNEST with a column ref — handled later by extractUnnestMappings.
@@ -221,10 +237,10 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
221
237
  const columns = new Map(
222
238
  item.as.args.value.map((columnRef) => [columnRef.column, [resolvedCol(columnRef.column, {})]]),
223
239
  );
224
- ctx.tables.set(tableAlias, [{ name: tableAlias, columns }]);
240
+ registerAliasedTable(tableAlias, columns, ctx);
225
241
  }
226
242
  } else if (isTableExpr(item)) {
227
- const alias = typeof item.as === 'string' ? item.as : '<subquery>';
243
+ const alias = typeof item.as === 'string' ? item.as : SUBQUERY_TABLE;
228
244
  // eslint-disable-next-line no-use-before-define
229
245
  checkSelect(item.expr.ast, ctx, alias);
230
246
  } else if (isJoin(item) || isBaseFrom(item)) {
@@ -283,11 +299,51 @@ function extractUnnestMappings(select: Select): UnnestMapping[] {
283
299
  return mappings;
284
300
  }
285
301
 
302
+ // Resolves UNNEST source schema into target column entries.
303
+ // When hasKnownApiOperation is true, an unrecognised schema type throws; otherwise unknown columns
304
+ // are registered with an empty schema (the schema is an estimate for non-API sources).
305
+ function buildUnnestColumnMap(
306
+ fromColumn: string,
307
+ toColumns: string[],
308
+ sourceSchema: ResolvedColumn['schema'] | undefined,
309
+ sourceAst: object | undefined,
310
+ ast: object,
311
+ hasKnownApiOperation: boolean,
312
+ ): Map<string, ResolvedColumn[]> {
313
+ if (sourceSchema?.type === 'array') {
314
+ const [toColumn] = toColumns;
315
+ assert.ok(toColumn !== undefined);
316
+ return new Map([[toColumn, [resolvedCol(toColumn, sourceSchema.items, sourceAst)]]]);
317
+ }
318
+ if (sourceSchema?.type === 'object') {
319
+ const [keyColumn, valueColumn] = toColumns;
320
+ assert.ok(
321
+ keyColumn !== undefined && valueColumn !== undefined,
322
+ `UNNEST of map column '${fromColumn}' requires exactly two alias columns (key, value)`,
323
+ );
324
+ const addlProps = (sourceSchema as SchemaObject)['additionalProperties'] as unknown;
325
+ const valueSchema: SchemaObject =
326
+ typeof addlProps === 'object' && addlProps !== null ? addlProps : { type: 'string' };
327
+ return new Map([
328
+ [keyColumn, [resolvedCol(keyColumn, { type: 'string' }, sourceAst)]],
329
+ [valueColumn, [resolvedCol(valueColumn, valueSchema, sourceAst)]],
330
+ ]);
331
+ }
332
+ if (!hasKnownApiOperation) {
333
+ return new Map(toColumns.map((toColumn) => [toColumn, [resolvedCol(toColumn, {})]]));
334
+ }
335
+ throw new AthenaError(
336
+ ATHENA_ERROR,
337
+ `UNNEST source column '${fromColumn}' must resolve to an array or map schema`,
338
+ ast,
339
+ );
340
+ }
341
+
286
342
  function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMapping[] {
287
343
  const deferred: UnnestMapping[] = [];
288
344
 
289
345
  for (const { fromColumn, toColumns, tableAlias, ast } of mappings) {
290
- const ownerTable = [...ctx.tables.values()].flat().find((table) => table.columns.has(fromColumn));
346
+ const ownerTable = flattenTables(ctx).find((table) => table.columns.has(fromColumn));
291
347
 
292
348
  if (ownerTable === undefined) {
293
349
  deferred.push({ fromColumn, toColumns, ...(tableAlias !== undefined ? { tableAlias } : {}), ast });
@@ -295,58 +351,17 @@ function applyUnnestPre(mappings: UnnestMapping[], ctx: VisitContext): UnnestMap
295
351
  }
296
352
 
297
353
  const sourceColumns = ownerTable.columns.get(fromColumn) ?? [];
298
- const sourceSchema = sourceColumns[0]?.schema;
299
354
  const unnestTableName = `${ownerTable.name ?? ANONYMOUS_TABLE}:<unnested>`;
300
355
  const apiOperation = ownerTable.apiOperation !== undefined ? { apiOperation: ownerTable.apiOperation } : {};
301
-
302
- let unnestEntry: ResolvedTable[];
303
- if (sourceSchema?.type === 'array') {
304
- const [toColumn] = toColumns;
305
- assert.ok(toColumn !== undefined);
306
- unnestEntry = [
307
- {
308
- name: unnestTableName,
309
- ...apiOperation,
310
- columns: new Map([[toColumn, [resolvedCol(toColumn, sourceSchema.items, sourceColumns[0]?.ast)]]]),
311
- },
312
- ];
313
- } else if (sourceSchema?.type === 'object') {
314
- const [keyColumn, valueColumn] = toColumns;
315
- assert.ok(
316
- keyColumn !== undefined && valueColumn !== undefined,
317
- `UNNEST of map column '${fromColumn}' requires exactly two alias columns (key, value)`,
318
- );
319
- const addlProps = (sourceSchema as SchemaObject)['additionalProperties'] as unknown;
320
- const valueSchema: SchemaObject =
321
- typeof addlProps === 'object' && addlProps !== null ? addlProps : { type: 'string' };
322
- unnestEntry = [
323
- {
324
- name: unnestTableName,
325
- ...apiOperation,
326
- columns: new Map([
327
- [keyColumn, [resolvedCol(keyColumn, { type: 'string' }, sourceColumns[0]?.ast)]],
328
- [valueColumn, [resolvedCol(valueColumn, valueSchema, sourceColumns[0]?.ast)]],
329
- ]),
330
- },
331
- ];
332
- } else if (ownerTable.apiOperation !== undefined) {
333
- // Only raise an error when the column schema was derived from a known API spec — for
334
- // computed columns (subqueries, CTEs, functions) the inferred schema may be imprecise.
335
- throw new AthenaError(
336
- ATHENA_ERROR,
337
- `UNNEST source column '${fromColumn}' must resolve to an array or map schema`,
338
- ast,
339
- );
340
- } else {
341
- // Non-API source: schema is an estimate; register target columns with unknown schema.
342
- unnestEntry = [
343
- {
344
- name: unnestTableName,
345
- ...apiOperation,
346
- columns: new Map(toColumns.map((toColumn) => [toColumn, [resolvedCol(toColumn, {})]])),
347
- },
348
- ];
349
- }
356
+ const unnestColumns = buildUnnestColumnMap(
357
+ fromColumn,
358
+ toColumns,
359
+ sourceColumns[0]?.schema,
360
+ sourceColumns[0]?.ast,
361
+ ast,
362
+ ownerTable.apiOperation !== undefined,
363
+ );
364
+ const unnestEntry: ResolvedTable[] = [{ name: unnestTableName, ...apiOperation, columns: unnestColumns }];
350
365
  ctx.tables.set(unnestTableName, unnestEntry);
351
366
  if (tableAlias !== undefined) {
352
367
  ctx.tables.set(tableAlias, unnestEntry);
@@ -362,29 +377,16 @@ function applyUnnestPost(mappings: UnnestMapping[], columns: Map<string, Resolve
362
377
  if (sourceColumns === undefined) {
363
378
  throw new AthenaError(ATHENA_ERROR, `UNNEST source column '${fromColumn}' not found in SELECT`, ast);
364
379
  }
365
- const sourceSchema = sourceColumns[0]?.schema;
366
-
367
- if (sourceSchema?.type === 'array') {
368
- const [toColumn] = toColumns;
369
- assert.ok(toColumn !== undefined);
370
- columns.set(toColumn, [resolvedCol(toColumn, sourceSchema.items, sourceColumns[0]?.ast)]);
371
- } else if (sourceSchema?.type === 'object') {
372
- const [keyColumn, valueColumn] = toColumns;
373
- assert.ok(
374
- keyColumn !== undefined && valueColumn !== undefined,
375
- `UNNEST of map column '${fromColumn}' requires exactly two alias columns (key, value)`,
376
- );
377
- const addlProps = (sourceSchema as SchemaObject)['additionalProperties'] as unknown;
378
- const valueSchema: SchemaObject =
379
- typeof addlProps === 'object' && addlProps !== null ? addlProps : { type: 'string' };
380
- columns.set(keyColumn, [resolvedCol(keyColumn, { type: 'string' }, sourceColumns[0]?.ast)]);
381
- columns.set(valueColumn, [resolvedCol(valueColumn, valueSchema, sourceColumns[0]?.ast)]);
382
- } else {
383
- throw new AthenaError(
384
- ATHENA_ERROR,
385
- `UNNEST source column '${fromColumn}' must resolve to an array or map schema`,
386
- ast,
387
- );
380
+ const unnestColumns = buildUnnestColumnMap(
381
+ fromColumn,
382
+ toColumns,
383
+ sourceColumns[0]?.schema,
384
+ sourceColumns[0]?.ast,
385
+ ast,
386
+ true,
387
+ );
388
+ for (const [key, value] of unnestColumns) {
389
+ columns.set(key, value);
388
390
  }
389
391
  }
390
392
  }
@@ -415,19 +417,14 @@ function schemaPropertyHint(resolvedColumns: ResolvedColumn[]): string {
415
417
  if (resolvedColumns.length === 0) {
416
418
  return '';
417
419
  }
418
- const first = JSON.stringify(resolvedColumns[0]?.schema);
419
- if (!resolvedColumns.every((col) => JSON.stringify(col.schema) === first)) {
420
- return '';
421
- }
422
- const schema = resolvedColumns[0]?.schema;
423
- if (schema?.type !== 'object') {
420
+ const firstSchema = resolvedColumns[0]?.schema;
421
+ if (!resolvedColumns.every((col) => JSON.stringify(col.schema) === JSON.stringify(firstSchema))) {
424
422
  return '';
425
423
  }
426
- const properties = schema.properties;
427
- if (properties === undefined) {
424
+ if (firstSchema?.type !== 'object' || firstSchema.properties === undefined) {
428
425
  return '';
429
426
  }
430
- const propNames = Object.keys(properties);
427
+ const propNames = Object.keys(firstSchema.properties);
431
428
  return propNames.length > 0 ? `; available properties: ${propNames.join(', ')}` : '';
432
429
  }
433
430
 
@@ -438,8 +435,7 @@ function resolveSchemaAtPath(
438
435
  ast?: object,
439
436
  ): SchemaObject[] {
440
437
  // Double-dot handles allOf / anyOf / oneOf wrappers that may appear in the schema.
441
- // eslint-disable-next-line prefer-named-capture-group
442
- const adjustedPath = `$.${propertyAccessor.substring(1).replace(/(\.|\[)/gu, '..properties$1')}`;
438
+ const adjustedPath = `$.${propertyAccessor.substring(1).replace(/(?<sep>\.|\[)/gu, '..properties$<sep>')}`;
443
439
  log('adjusted path', adjustedPath);
444
440
 
445
441
  const extractedSchemas = resolvedColumns.flatMap((col) =>
@@ -477,6 +473,28 @@ function navigateSchemaPath(
477
473
  // Pass 2 — Resolve SELECT columns → Map<name, ResolvedColumn[]>
478
474
  // ---------------------------------------------------------------------------
479
475
 
476
+ function throwUnknownTableError(tableRef: string | undefined, colRef: string, ctx: VisitContext, ref: object): never {
477
+ throw new AthenaError(
478
+ ATHENA_ERROR,
479
+ `unknown table or alias '${tableRef ?? colRef}'; known tables: ${[...ctx.tables.keys()].join(', ')}`,
480
+ ref,
481
+ );
482
+ }
483
+
484
+ function resolveReferencedTablesOrThrow(
485
+ tableRef: string | undefined,
486
+ colRef: string,
487
+ allTables: ResolvedTable[],
488
+ ctx: VisitContext,
489
+ ref: object,
490
+ ): ResolvedTable[] {
491
+ const referencedTables = resolveReferencedTables(tableRef, allTables, ctx);
492
+ if (referencedTables.length === 0) {
493
+ throwUnknownTableError(tableRef, colRef, ctx, ref);
494
+ }
495
+ return referencedTables;
496
+ }
497
+
480
498
  function lookupColumnOrThrow(colRef: string, ref: object, referencedTables: ResolvedTable[]): ResolvedColumn[] {
481
499
  const resolvedColumns = referencedTables.flatMap((table) => table.columns.get(colRef) ?? []);
482
500
  if (resolvedColumns.length === 0) {
@@ -503,34 +521,25 @@ function checkColumnRefsExist(
503
521
  return;
504
522
  }
505
523
  for (const ref of extractColumnRefs(ast)) {
506
- const colRef = typeof ref.column === 'string' ? ref.column : undefined;
524
+ const { tableRef, colRef } = resolveColumnRefParts(ref);
507
525
  if (colRef === undefined || colRef === '*') {
508
526
  continue;
509
527
  }
510
- const tableRef = ref.table ?? undefined;
511
528
  if (tableRef === undefined && selectColumns?.has(colRef) === true) {
512
529
  continue;
513
530
  }
514
- const referencedTables = tableRef !== undefined ? lookupTables(tableRef, ctx) : allTables;
515
- if (referencedTables.length === 0) {
516
- throw new AthenaError(
517
- ATHENA_ERROR,
518
- `unknown table or alias '${tableRef ?? colRef}'; known tables: ${[...ctx.tables.keys()].join(', ')}`,
519
- ref,
520
- );
521
- }
531
+ const referencedTables = resolveReferencedTablesOrThrow(tableRef, colRef, allTables, ctx, ref);
522
532
  lookupColumnOrThrow(colRef, ref, referencedTables);
523
533
  }
524
534
  }
525
535
 
526
536
  function validateComplexColumnExpression(columnAST: unknown, allTables: ResolvedTable[], ctx: VisitContext): void {
527
537
  for (const { ref, path, fnNode } of extractJsonExtractCalls(columnAST)) {
528
- const tableRef = ref.table ?? undefined;
529
- const colRef = typeof ref.column === 'string' ? ref.column : undefined;
538
+ const { tableRef, colRef } = resolveColumnRefParts(ref);
530
539
  if (colRef === undefined) {
531
540
  continue;
532
541
  }
533
- const referencedTables = tableRef !== undefined ? lookupTables(tableRef, ctx) : allTables;
542
+ const referencedTables = resolveReferencedTables(tableRef, allTables, ctx);
534
543
  const resolvedColumns = referencedTables.flatMap((table) => table.columns.get(colRef) ?? []);
535
544
  if (resolvedColumns.length > 0) {
536
545
  resolveSchemaAtPath(colRef, path, resolvedColumns, fnNode);
@@ -547,18 +556,10 @@ function resolveSingleColumnRef(
547
556
  ctx: VisitContext,
548
557
  columns: Map<string, ResolvedColumn[]>,
549
558
  ): void {
550
- const tableRef = ref.table ?? undefined;
551
- const colRef = typeof ref.column === 'string' ? ref.column : undefined;
559
+ const { tableRef, colRef } = resolveColumnRefParts(ref);
552
560
  assert.ok(colRef !== undefined, 'column_ref must have a string column name');
553
561
 
554
- const referencedTables = tableRef !== undefined ? lookupTables(tableRef, ctx) : allTables;
555
- if (referencedTables.length === 0) {
556
- throw new AthenaError(
557
- ATHENA_ERROR,
558
- `unknown table or alias '${tableRef ?? colRef}'; known tables: ${[...ctx.tables.keys()].join(', ')}`,
559
- ref,
560
- );
561
- }
562
+ const referencedTables = resolveReferencedTablesOrThrow(tableRef, colRef, allTables, ctx, ref);
562
563
 
563
564
  if (colRef === '*') {
564
565
  expandWildcard(referencedTables, columns);
@@ -581,8 +582,39 @@ function resolveSingleColumnRef(
581
582
  );
582
583
  }
583
584
 
585
+ function applySchemaTypeOverride(
586
+ columnAST: unknown,
587
+ columnAlias: string | null,
588
+ indexedName: string,
589
+ columns: Map<string, ResolvedColumn[]>,
590
+ predicate: (expression: unknown) => boolean,
591
+ schemaType: 'array' | 'object',
592
+ ): void {
593
+ if (!predicate(columnAST)) {
594
+ return;
595
+ }
596
+ const colName = columnAlias ?? indexedName;
597
+ const existing = columns.get(colName);
598
+ if (existing !== undefined && existing[0]?.schema.type !== schemaType) {
599
+ columns.set(
600
+ colName,
601
+ existing.map((col) => resolvedCol(col.name, { type: schemaType }, col.ast)),
602
+ );
603
+ }
604
+ }
605
+
606
+ function validateClauseExpression(
607
+ expression: unknown,
608
+ allTables: ResolvedTable[],
609
+ ctx: VisitContext,
610
+ selectColumns?: Map<string, ResolvedColumn[]>,
611
+ ): void {
612
+ checkColumnRefsExist(expression, allTables, ctx, selectColumns);
613
+ validateComplexColumnExpression(expression, allTables, ctx);
614
+ }
615
+
584
616
  function resolveSelectColumns(select: Select, ctx: VisitContext): Map<string, ResolvedColumn[]> {
585
- const allTables = [...ctx.tables.values()].flat();
617
+ const allTables = flattenTables(ctx);
586
618
  const columns = new Map<string, ResolvedColumn[]>();
587
619
 
588
620
  for (const [index, columnAST] of select.columns.entries()) {
@@ -593,8 +625,7 @@ function resolveSelectColumns(select: Select, ctx: VisitContext): Map<string, Re
593
625
  const columnRefs = extractColumnRefs(columnAST);
594
626
 
595
627
  if (columnRefs.length !== 1) {
596
- checkColumnRefsExist(columnAST, allTables, ctx);
597
- validateComplexColumnExpression(columnAST, allTables, ctx);
628
+ validateClauseExpression(columnAST, allTables, ctx);
598
629
  resolveDefaultSchemaColumn(columnAlias, indexedName, columnAST, columns);
599
630
  } else {
600
631
  const [ref] = columnRefs;
@@ -602,27 +633,8 @@ function resolveSelectColumns(select: Select, ctx: VisitContext): Map<string, Re
602
633
  resolveSingleColumnRef(columnAST, columnAlias, indexedName, ref, allTables, ctx, columns);
603
634
  }
604
635
 
605
- if (containsCastToArray(columnAST)) {
606
- const colName = columnAlias ?? indexedName;
607
- const existing = columns.get(colName);
608
- if (existing !== undefined && existing[0]?.schema.type !== 'array') {
609
- columns.set(
610
- colName,
611
- existing.map((col) => resolvedCol(col.name, { type: 'array' }, col.ast)),
612
- );
613
- }
614
- }
615
-
616
- if (containsCastToMap(columnAST)) {
617
- const colName = columnAlias ?? indexedName;
618
- const existing = columns.get(colName);
619
- if (existing !== undefined && existing[0]?.schema.type !== 'object') {
620
- columns.set(
621
- colName,
622
- existing.map((col) => resolvedCol(col.name, { type: 'object' }, col.ast)),
623
- );
624
- }
625
- }
636
+ applySchemaTypeOverride(columnAST, columnAlias, indexedName, columns, containsCastToArray, 'array');
637
+ applySchemaTypeOverride(columnAST, columnAlias, indexedName, columns, containsCastToMap, 'object');
626
638
  }
627
639
 
628
640
  return columns;
@@ -652,41 +664,34 @@ function checkSelect(
652
664
 
653
665
  log('resolved columns', [...columns.keys()]);
654
666
 
655
- const allTables = [...selectCtx.tables.values()].flat();
667
+ const allTables = flattenTables(selectCtx);
656
668
  for (const item of fromClauseItems(select)) {
657
669
  const onExpr = (item as { on?: unknown }).on;
658
670
  if (onExpr !== undefined) {
659
- checkColumnRefsExist(onExpr, allTables, selectCtx);
660
- validateComplexColumnExpression(onExpr, allTables, selectCtx);
671
+ validateClauseExpression(onExpr, allTables, selectCtx);
661
672
  }
662
673
  }
663
674
  if (select.where !== null) {
664
- checkColumnRefsExist(select.where, allTables, selectCtx);
665
- validateComplexColumnExpression(select.where, allTables, selectCtx);
675
+ validateClauseExpression(select.where, allTables, selectCtx);
666
676
  }
667
677
  if (select.having !== null) {
668
- checkColumnRefsExist(select.having, allTables, selectCtx, columns);
669
- validateComplexColumnExpression(select.having, allTables, selectCtx);
678
+ validateClauseExpression(select.having, allTables, selectCtx, columns);
670
679
  }
671
680
  for (const orderItem of select.orderby ?? []) {
672
- checkColumnRefsExist(orderItem.expr, allTables, selectCtx, columns);
673
- validateComplexColumnExpression(orderItem.expr, allTables, selectCtx);
681
+ validateClauseExpression(orderItem.expr, allTables, selectCtx, columns);
674
682
  }
675
683
  if (select.groupby?.columns !== undefined) {
676
684
  for (const groupCol of select.groupby.columns) {
677
- checkColumnRefsExist(groupCol, allTables, selectCtx, columns);
678
- validateComplexColumnExpression(groupCol, allTables, selectCtx);
685
+ validateClauseExpression(groupCol, allTables, selectCtx, columns);
679
686
  }
680
687
  }
681
688
 
682
689
  if (select._next !== undefined) {
683
690
  const nextColumns = checkSelect(select._next, ctx, withTableName);
684
- const currentNumberOfKeys = columns.size;
685
- const nextNumberOfKeys = nextColumns.size;
686
- if (currentNumberOfKeys !== nextNumberOfKeys) {
691
+ if (columns.size !== nextColumns.size) {
687
692
  throw new AthenaError(
688
693
  ATHENA_ERROR,
689
- `UNION ALL parts have different number of columns: ${currentNumberOfKeys.toString()} vs ${nextNumberOfKeys.toString()}`,
694
+ `UNION ALL parts have different number of columns: ${columns.size.toString()} vs ${nextColumns.size.toString()}`,
690
695
  select._next,
691
696
  );
692
697
  }