@checkdigit/eslint-plugin 7.18.0-PR.143-b6d4 → 7.18.0-PR.143-c222
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 +1 -1
- package/package.json +1 -1
- package/src/athena/ATHENA.md +0 -387
- package/src/athena/PLAN.md +0 -355
package/LICENSE.txt
CHANGED
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-
|
|
1
|
+
{"name":"@checkdigit/eslint-plugin","version":"7.18.0-PR.143-c222","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-mjs","!src/**/test/**","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/test/**","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-mjs/**/test/**","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 .","lint:fix":"eslint --max-warnings 0 --fix .","peggy":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format es --output \"${file%.peggy}-peggy.ts\"; done","peggy-watch":"for file in ./src/peggy/*.peggy; do peggy \"$file\" --format=es --watch --output=\"${file%.peggy}-peggy.ts\"; done","prepare":"","prepublishOnly":"npm run build:dist-types && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"dependencies":{"@apidevtools/json-schema-ref-parser":"^15.3.5","@typescript-eslint/type-utils":"^8.60.1","@typescript-eslint/utils":"^8.60.1","ajv":"^8.20.0","debug":"^4.4.3","glob":"^13.0.6","http-status-codes":"^2.3.0","js-yaml":"^4.2.0","json-pointer":"^0.6.2","jsonpath-plus":"^10.4.0","ts-api-utils":"^2.5.0"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^6.1.0","@checkdigit/typescript-config":"10.0.0","@eslint/js":"^9.37.0","@types/debug":"^4.1.13","@types/eslint":"^9.6.1","@types/eslint-config-prettier":"^6.11.3","@types/js-yaml":"^4.0.9","@types/json-pointer":"^1.0.34","@typescript-eslint/parser":"^8.60.1","@typescript-eslint/rule-tester":"^8.60.1","eslint":"^9.37.0","eslint-config-prettier":"^10.1.8","eslint-import-resolver-typescript":"^4.4.5","eslint-plugin-eslint-plugin":"^6.4.0","eslint-plugin-import":"^2.32.0","eslint-plugin-no-only-tests":"^3.4.0","eslint-plugin-no-secrets":"^2.3.3","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"^1.0.4","openapi-types":"^12.1.3","peggy":"^4.2.0","rimraf":"^6.1.3","typescript-eslint":"^8.60.1"},"peerDependencies":{"eslint":">=9 <10"},"engines":{"node":">=24.14.1"},"service":{"api":{"root":"src","endpoints":["api/v1"]}}}
|
package/src/athena/ATHENA.md
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
# Athena Rule — Design & Implementation Reference
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
The `athena` ESLint rule validates AWS Athena SQL strings embedded in TypeScript source code (as template literals or string literals beginning with `SELECT` or `WITH`) against OpenAPI schemas at lint time. It catches references to non-existent response/request body properties before the query is ever run in production.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Trigger Conditions
|
|
10
|
-
|
|
11
|
-
The rule is activated on:
|
|
12
|
-
|
|
13
|
-
- **Template literals** — every `` `...` `` in the file is inspected; the quasis (the static parts between `${...}` interpolations) are joined and trimmed.
|
|
14
|
-
- **String literals** — plain `'...'` or `"..."` strings are inspected if the value is a string type.
|
|
15
|
-
|
|
16
|
-
In both cases the rule is a no-op unless the resulting string starts with `SELECT ` or `WITH ` (case-insensitive).
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## File Structure
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
src/athena/
|
|
24
|
-
athena.ts Entry point: ESLint rule, SQL parse, checkAthenaAst
|
|
25
|
-
visitor.ts VisitorMap interface, walk() dispatcher, type guards, extractors
|
|
26
|
-
context.ts VisitContext, ResolvedTable, ResolvedColumn, context factories
|
|
27
|
-
service-table.ts buildServiceTables() — maps MatchedOperation[] → ResolvedTable[]
|
|
28
|
-
api-locator.ts locateApi() — finds swagger.schema.deref.json on disk
|
|
29
|
-
api-matcher.ts matchApi() — filters OpenAPI operations by WHERE-clause conditions
|
|
30
|
-
types.ts TypeScript types for the PEG parser AST (based on node-sql-parser)
|
|
31
|
-
athena.spec.ts Integration tests via RuleTester
|
|
32
|
-
zzz.spec.ts Unit tests for walk() and the extractor helpers
|
|
33
|
-
json.spec.ts Dev scratch tests for JSONPath exploration (all skipped)
|
|
34
|
-
PLAN.md Architecture planning document
|
|
35
|
-
ATHENA.md This file
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## End-to-End Data Flow
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
TypeScript source
|
|
44
|
-
│
|
|
45
|
-
│ TemplateLiteral / Literal ESLint visitor
|
|
46
|
-
▼
|
|
47
|
-
sql: string (quasis joined, interpolation placeholders stripped)
|
|
48
|
-
│
|
|
49
|
-
│ parse(sql, { includeLocations: true }) ← src/peggy/athena-peggy.ts
|
|
50
|
-
▼
|
|
51
|
-
AST: Select | With[]...
|
|
52
|
-
│
|
|
53
|
-
│ checkAthenaAst(ast, ctx)
|
|
54
|
-
▼
|
|
55
|
-
WITH items processed first (each CTE → checkSelect → ctx.tables)
|
|
56
|
-
│
|
|
57
|
-
│ checkSelect(select, ctx)
|
|
58
|
-
▼
|
|
59
|
-
┌────────────────────────────────────────────┐
|
|
60
|
-
│ Pass 1 — resolveFromClause │
|
|
61
|
-
│ BaseFrom nodes → locateApi + matchApi │
|
|
62
|
-
│ → buildServiceTables → ctx.tables │
|
|
63
|
-
│ │
|
|
64
|
-
│ UNNEST pre-pass — applyUnnestPre │
|
|
65
|
-
│ source is a service-table column │
|
|
66
|
-
│ → synthetic ctx.tables[name:<unnested>] │
|
|
67
|
-
│ │
|
|
68
|
-
│ Pass 2 — resolveSelectColumns │
|
|
69
|
-
│ for each SELECT column: │
|
|
70
|
-
│ extractColumnRefs → find ref │
|
|
71
|
-
│ → lookup in ctx.tables │
|
|
72
|
-
│ → extractJsonExtractPath / bracket │
|
|
73
|
-
│ → JSONPath into schema │
|
|
74
|
-
│ │
|
|
75
|
-
│ UNNEST post-pass — applyUnnestPost │
|
|
76
|
-
│ source is a computed SELECT column │
|
|
77
|
-
│ │
|
|
78
|
-
│ _next (UNION ALL) → recurse checkSelect │
|
|
79
|
-
│ │
|
|
80
|
-
│ register result in ctx.tables[cteTableName]│
|
|
81
|
-
└────────────────────────────────────────────┘
|
|
82
|
-
│
|
|
83
|
-
AthenaError thrown → context.report(AthenaError)
|
|
84
|
-
parse error thrown → context.report(SyntextError)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## PEG Grammar
|
|
90
|
-
|
|
91
|
-
The SQL is parsed by `src/peggy/athena.peggy`, a hand-written PEG grammar compiled to `src/peggy/athena-peggy.ts` via `npm run peggy` (`peggy --format es`). The grammar is an extension of the `node-sql-parser` grammar with Athena/Trino-specific syntax (MAP, ARRAY, TRY_CAST, UNNEST, bracket array access, etc.).
|
|
92
|
-
|
|
93
|
-
The second grammar `src/peggy/athena-chat.peggy` is an experimental rewrite with a cleaner AST format; it is **not** used by the rule.
|
|
94
|
-
|
|
95
|
-
The compiled parser's `parse(sql, opts)` returns `{ ast: AST | AST[] }`. When the input is a single statement the result is a single `Select` node; when there are multiple (e.g. `;`-separated) it is an array.
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## AST Node Shapes (relevant subset)
|
|
100
|
-
|
|
101
|
-
All types are in `src/athena/types.ts`.
|
|
102
|
-
|
|
103
|
-
| Shape | Key fields |
|
|
104
|
-
| ------------------------------ | -------------------------------------------------------------------------- |
|
|
105
|
-
| `Select` | `type:'select'`, `with`, `columns`, `from`, `where`, `_next` (UNION chain) |
|
|
106
|
-
| `With` | `name.value` (CTE name), `stmt.ast` (inner Select) |
|
|
107
|
-
| `BaseFrom` | `table` (string), `as` (alias or null) |
|
|
108
|
-
| `Join` extends `BaseFrom` | `join` (join type string), `on` |
|
|
109
|
-
| `TableExpr` | `expr.ast` (sub-Select), `as` |
|
|
110
|
-
| `UnnestFrom` (not in types.ts) | `type:'unnest'`, `expr` (ColumnRefItem), `as` (func_call alias) |
|
|
111
|
-
| `Column` | `type:'expr'`, `expr` (ExpressionValue), `as` (alias) |
|
|
112
|
-
| `ColumnRefItem` | `type:'column_ref'`, `table`, `column`, optionally `array_index` |
|
|
113
|
-
| `Function` | `type:'function'`, `name.name[0].value` (fn name), `args.value[]` |
|
|
114
|
-
| `Binary` | `type:'binary_expr'`, `operator`, `left`, `right` |
|
|
115
|
-
| `Cast` | `type:'cast'`, `expr`, `target.dataType` |
|
|
116
|
-
| `Case` | `type:'case'`, `args[]` (when/else arms) |
|
|
117
|
-
|
|
118
|
-
`ColumnRefItem.array_index` is added by the parser for bracket access (`col['key']`); it is not in the TypeScript type definition and is accessed via the `ColumnRefWithIndex` interface in `visitor.ts`.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Visitor Pattern (`visitor.ts`)
|
|
123
|
-
|
|
124
|
-
### `VisitorMap`
|
|
125
|
-
|
|
126
|
-
An interface with optional typed hooks, one per AST node kind:
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
interface VisitorMap {
|
|
130
|
-
visitSelect?(node: Select): void;
|
|
131
|
-
visitWith?(node: With): void;
|
|
132
|
-
visitBaseFrom?(node: BaseFrom): void;
|
|
133
|
-
visitJoin?(node: Join): void;
|
|
134
|
-
visitTableExpr?(node: TableExpr): void;
|
|
135
|
-
visitUnnest?(node: UnnestFrom): void;
|
|
136
|
-
visitColumn?(node: Column): void;
|
|
137
|
-
visitColumnRef?(node: ColumnRefItem): void;
|
|
138
|
-
visitFunction?(node: Function): void;
|
|
139
|
-
visitBinary?(node: Binary): void;
|
|
140
|
-
visitAggrFunc?(node: AggrFunc): void;
|
|
141
|
-
visitCast?(node: Cast): void;
|
|
142
|
-
visitCase?(node: Case): void;
|
|
143
|
-
visitExprList?(node: ExprList): void;
|
|
144
|
-
visitValue?(node: ExpressionValue): void;
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### `walk(node, visitor)`
|
|
149
|
-
|
|
150
|
-
The main entry point. For a `Select` node it:
|
|
151
|
-
|
|
152
|
-
1. Calls `visitSelect`
|
|
153
|
-
2. Walks `with[]` items (calls `visitWith`, recurses into CTE body)
|
|
154
|
-
3. Walks `from[]` items via `walkFrom`
|
|
155
|
-
4. Walks `columns[]` items via `walkExpr`
|
|
156
|
-
5. Walks `where` via `walkExpr`
|
|
157
|
-
6. Walks `_next` (UNION) recursively via `walk`
|
|
158
|
-
|
|
159
|
-
`walkFrom` dispatches to `visitUnnest`, `visitTableExpr` (+ recurses into subquery), `visitJoin`, or `visitBaseFrom` based on type guards.
|
|
160
|
-
|
|
161
|
-
`walkExpr` handles expression-level nodes: `binary_expr`, `column_ref`, `function`, `aggr_func`, `cast`, `case`, `expr_list`, `expr` (column wrapper), or `visitValue` for literal/value nodes.
|
|
162
|
-
|
|
163
|
-
### Type Guards
|
|
164
|
-
|
|
165
|
-
All accept `unknown` input so callers don't need intermediate casts:
|
|
166
|
-
|
|
167
|
-
| Guard | Identifies |
|
|
168
|
-
| --------------------- | ------------------------------------------------- |
|
|
169
|
-
| `isUnnestFrom(node)` | `type === 'unnest'` |
|
|
170
|
-
| `isDual(node)` | `type === 'dual'` |
|
|
171
|
-
| `isTableExpr(node)` | has `expr.ast` sub-select |
|
|
172
|
-
| `isJoin(node)` | has `join` property, is not unnest/dual/tableExpr |
|
|
173
|
-
| `isBaseFrom(node)` | has `table` property, is none of the above |
|
|
174
|
-
| `hasArrayIndex(node)` | `ColumnRefItem` with `array_index[]` |
|
|
175
|
-
|
|
176
|
-
### Extractor Helpers
|
|
177
|
-
|
|
178
|
-
These replace the ad-hoc JSONPath queries that appeared in the original monolith:
|
|
179
|
-
|
|
180
|
-
| Helper | Returns |
|
|
181
|
-
| ---------------------------------- | ------------------------------------------------------------- |
|
|
182
|
-
| `extractColumnRefs(expr)` | All `ColumnRefItem` nodes in the subtree |
|
|
183
|
-
| `extractJsonExtractPath(expr)` | Path arg of first `json_extract_scalar` / `json_extract` call |
|
|
184
|
-
| `extractBracketAccessorPath(expr)` | `$["key"]` string from first bracket accessor (`col['key']`) |
|
|
185
|
-
| `hasFunctionCalls(expr)` | `true` if any `function` or `aggr_func` node is present |
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## Resolution Context (`context.ts`)
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
interface VisitContext {
|
|
193
|
-
tables: Map<string, ResolvedTable[]>; // name → one entry per matched API operation
|
|
194
|
-
aliases: Map<string, string>; // alias → canonical table name
|
|
195
|
-
apiSchemas: Map<string, ApiSchemas[]>; // disk-read cache, shared across the whole query
|
|
196
|
-
parent?: VisitContext; // parent scope (currently stored but not queried)
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
**`createRootContext()`** — creates an empty context for the top-level query.
|
|
201
|
-
|
|
202
|
-
**`createChildContext(parent)`** — creates a child context that pre-populates `tables` from the parent (so CTE results defined in the `WITH` clause are visible to subsequent CTEs and the final `SELECT`). The `aliases` map is fresh (each SELECT has its own alias scope). The `apiSchemas` map is shared by reference.
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
interface ResolvedTable {
|
|
206
|
-
name?: string;
|
|
207
|
-
columns: Map<string, ResolvedColumn[]>; // column name → one per matched operation
|
|
208
|
-
apiOperation?: MatchedOperation[];
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
interface ResolvedColumn {
|
|
212
|
-
name: string;
|
|
213
|
-
schema: v3.SchemaObject;
|
|
214
|
-
ast?: object; // originating AST node (for error location, future use)
|
|
215
|
-
}
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## Service Table Columns (`service-table.ts`)
|
|
221
|
-
|
|
222
|
-
Every Athena service table (Kinesis stream) exposes eleven fixed top-level columns:
|
|
223
|
-
|
|
224
|
-
| Column | Schema |
|
|
225
|
-
| ----------------- | ------------------------------- |
|
|
226
|
-
| `method` | `{ type: 'string' }` |
|
|
227
|
-
| `started` | `{ type: 'string' }` |
|
|
228
|
-
| `ended` | `{ type: 'string' }` |
|
|
229
|
-
| `url` | `{ type: 'string' }` |
|
|
230
|
-
| `requestbody` | OpenAPI request body schema |
|
|
231
|
-
| `requestheaders` | OpenAPI request headers schema |
|
|
232
|
-
| `responsestatus` | `{ type: 'string' }` |
|
|
233
|
-
| `responsemessage` | `{ type: 'string' }` |
|
|
234
|
-
| `responsetype` | `{ type: 'string' }` |
|
|
235
|
-
| `responsebody` | OpenAPI response body schema |
|
|
236
|
-
| `responseheaders` | OpenAPI response headers schema |
|
|
237
|
-
|
|
238
|
-
`requestbody` / `requestheaders` come from `operation.request['properties'].body/headers`; `responsebody` / `responseheaders` from `operation.response['properties'].body/headers`. Both fall back to `{ type: 'object' }` if the field is absent.
|
|
239
|
-
|
|
240
|
-
`buildServiceTables(tableName, operations)` returns one `ResolvedTable` per `MatchedOperation` (multiple operations may match, e.g. GET and POST on the same path).
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## API Location (`api-locator.ts`)
|
|
245
|
-
|
|
246
|
-
`locateApi(serviceName)` converts a kebab-case service name (the SQL table name) to camelCase and globs for `src/services/<camelCase>/*/swagger.schema.deref.json`. All matched files are parsed and returned as `ApiSchemas[]`.
|
|
247
|
-
|
|
248
|
-
The deref'd schema is used (not the raw `swagger.schema.json`) so `$ref` pointers are already inlined.
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## API Matching (`api-matcher.ts`)
|
|
253
|
-
|
|
254
|
-
`matchApi(selectAST, tableAST, apiSchemas)` builds a set of `Matcher` functions from WHERE-clause conditions then filters all operations in `apiSchemas` against them.
|
|
255
|
-
|
|
256
|
-
Current matchers (each is optional — if the WHERE clause doesn't contain the relevant condition, the matcher is skipped):
|
|
257
|
-
|
|
258
|
-
| Matcher | Condition pattern in WHERE |
|
|
259
|
-
| --------------- | ---------------------------------------------------------------- |
|
|
260
|
-
| Method | `method = 'GET'` / `'POST'` / etc. |
|
|
261
|
-
| Path part count | `cardinality(split(url, '/')) = N` |
|
|
262
|
-
| Path part value | `split(url, '/')[N] = 'value'` |
|
|
263
|
-
| Response status | `responsestatus = '200'` (applied post-filter on response codes) |
|
|
264
|
-
|
|
265
|
-
The function throws an `Error` if no operations match (not `AthenaError` — this propagates as an unexpected error through `checkAthenaAst` and is caught as a non-`AthenaError`, reported with the raw message).
|
|
266
|
-
|
|
267
|
-
---
|
|
268
|
-
|
|
269
|
-
## Two-Pass Column Resolution
|
|
270
|
-
|
|
271
|
-
### Pass 1 — `resolveFromClause`
|
|
272
|
-
|
|
273
|
-
Iterates `select.from[]`. For each `BaseFrom` item:
|
|
274
|
-
|
|
275
|
-
- Registers the alias (if any) in `ctx.aliases`
|
|
276
|
-
- Skips if the table name is already in `ctx.tables` (CTE reference or duplicate in comma-join)
|
|
277
|
-
- Otherwise calls `locateApi` + `matchApi` + `buildServiceTables` and stores the result in `ctx.tables`
|
|
278
|
-
|
|
279
|
-
JOINs (`isJoin`), UNNEST items, subqueries (`isTableExpr`), and DUAL are skipped in pass 1.
|
|
280
|
-
|
|
281
|
-
### UNNEST Pre-Pass — `applyUnnestPre`
|
|
282
|
-
|
|
283
|
-
`extractUnnestMappings` extracts `{ fromColumn, toColumn }` pairs from UNNEST items in `from[]`.
|
|
284
|
-
|
|
285
|
-
`applyUnnestPre` then tries to resolve each mapping immediately: if the `fromColumn` exists in a service table currently in `ctx.tables`, it verifies the schema is `array`, unwraps the `.items` schema, and registers a synthetic transient table `"<ownerTable>:<unnested>"` in `ctx.tables` with a single column `toColumn` holding the item schema.
|
|
286
|
-
|
|
287
|
-
Any mappings where `fromColumn` is not yet found (because it will be a computed SELECT column) are returned as `deferred`.
|
|
288
|
-
|
|
289
|
-
### Pass 2 — `resolveSelectColumns`
|
|
290
|
-
|
|
291
|
-
Iterates `select.columns[]`. For each column:
|
|
292
|
-
|
|
293
|
-
1. **Count column refs** (`extractColumnRefs`). If 0 or > 1, fall back to `{ type: 'string' }` under the alias or `_colN` name.
|
|
294
|
-
2. **Look up the single ref** in `ctx.tables` (using `ctx.aliases` to resolve table qualifiers).
|
|
295
|
-
3. **Wildcard (`*`)** — expand all columns from the referenced tables into `columns`.
|
|
296
|
-
4. **Resolve the column name** in each candidate table's `columns` map. Throw `AthenaError` if not found.
|
|
297
|
-
5. **Check for property access**: first try `extractJsonExtractPath` (json_extract_scalar/json_extract), then `extractBracketAccessorPath` (bracket accessor). If neither, use the resolved schema directly.
|
|
298
|
-
6. **Navigate into the JSON schema** using the accessor path. The path is rewritten to use double-dot (`$..`) to handle intermediate `allOf`/`anyOf`/`oneOf` wrappers. Throw `AthenaError` if the path resolves to nothing.
|
|
299
|
-
|
|
300
|
-
The column name used in the result map is: alias > bare column name (no function) > `_colN` (function present, no alias).
|
|
301
|
-
|
|
302
|
-
### UNNEST Post-Pass — `applyUnnestPost`
|
|
303
|
-
|
|
304
|
-
Resolves the deferred UNNEST mappings against the `columns` map built in pass 2. The `fromColumn` is now a computed column; its schema must be `array`.
|
|
305
|
-
|
|
306
|
-
### UNION ALL
|
|
307
|
-
|
|
308
|
-
`select._next` chains the next SELECT in a UNION. After resolving the current SELECT, `checkSelect` recurses on `_next` passing the same parent context and the same `withTableName`. The column schemas from UNION branches are not currently compared against each other (this is a known gap — see PLAN.md).
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## CTE / WITH Handling
|
|
313
|
-
|
|
314
|
-
`checkAthenaAst` iterates `select.with[]` before calling `checkSelect` on the main query. For each WITH item it calls `checkSelect(withItem.stmt.ast, ctx, withItem.name.value)`. At the end of `checkSelect`, if `withTableName` is set, the resolved columns are registered as a `ResolvedTable` in `ctx.tables` under that name.
|
|
315
|
-
|
|
316
|
-
`createChildContext(parent)` copies `parent.tables` so each nested CTE can see tables defined by earlier CTEs in the same WITH clause.
|
|
317
|
-
|
|
318
|
-
After processing all CTEs, `checkAthenaAst` sets `select.with = null` (mutates the AST) to avoid double-processing in the subsequent `checkSelect` call on the outer select.
|
|
319
|
-
|
|
320
|
-
---
|
|
321
|
-
|
|
322
|
-
## Property Accessor Schema Navigation
|
|
323
|
-
|
|
324
|
-
When a column expression contains a JSON path accessor, the path is converted to a JSONPath expression that navigates into the OpenAPI schema:
|
|
325
|
-
|
|
326
|
-
```
|
|
327
|
-
Input: $.foo.bar
|
|
328
|
-
Rewrite: $...properties.foo..properties.bar
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
The double-dot (`..`) at each segment boundary lets JSONPath skip through intermediate `allOf`, `anyOf`, `oneOf`, or `properties` wrappers that OpenAPI schemas commonly use.
|
|
332
|
-
|
|
333
|
-
`JSONPath({ json: column.schema, path: adjustedPath })` is called against the resolved column's schema. If no values are returned, `AthenaError` is thrown with `property not found <colRef> - <accessor>`.
|
|
334
|
-
|
|
335
|
-
---
|
|
336
|
-
|
|
337
|
-
## Error Reporting
|
|
338
|
-
|
|
339
|
-
Two message IDs are registered on the ESLint rule:
|
|
340
|
-
|
|
341
|
-
| ID | Cause |
|
|
342
|
-
| -------------- | --------------------------------------------- |
|
|
343
|
-
| `SyntextError` | PEG parser threw (SQL syntax error) |
|
|
344
|
-
| `AthenaError` | `AthenaError` thrown during schema resolution |
|
|
345
|
-
|
|
346
|
-
Any other `Error` (e.g. thrown by `matchApi`, `locateApi`, `assert.ok`) is caught and reported as `AthenaError` with the error's string representation, plus a `console.error` to stderr. This is a catch-all for unexpected runtime failures.
|
|
347
|
-
|
|
348
|
-
Errors are thrown at the first failure encountered (not collected). Multiple errors in one query are reported as a single lint diagnostic for the template literal node.
|
|
349
|
-
|
|
350
|
-
---
|
|
351
|
-
|
|
352
|
-
## Schema Files
|
|
353
|
-
|
|
354
|
-
`src/services/<camelCaseServiceName>/<version>/swagger.schema.deref.json`
|
|
355
|
-
|
|
356
|
-
Generated by:
|
|
357
|
-
|
|
358
|
-
1. `src/openapi/generate-schema.ts` — reads `swagger.yml` and produces `swagger.schema.json` with typed request/response schemas keyed by `apis[path][method]`
|
|
359
|
-
2. `src/openapi/deref-schema.ts` — resolves `$ref` pointers inline to produce `swagger.schema.deref.json`
|
|
360
|
-
|
|
361
|
-
The `ApiSchemas` type (from `generate-schema.ts`) has shape:
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
{
|
|
365
|
-
apis: Record<string, Record<string, OperationSchemas>>;
|
|
366
|
-
// apis['/path/v1']['GET'] → { request: SchemaObject, responses: { '200': SchemaObject, ... } }
|
|
367
|
-
}
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
---
|
|
371
|
-
|
|
372
|
-
## Debug Logging
|
|
373
|
-
|
|
374
|
-
Set `DEBUG=eslint-plugin:athena` (or `eslint-plugin:athena:*`) to see verbose resolution logs. Uses the `debug` npm package.
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
## Known Limitations (as of current implementation)
|
|
379
|
-
|
|
380
|
-
- Only `BaseFrom` tables are resolved in pass 1; `JOIN ... ON`, subqueries (`TableExpr`), and `DUAL` are silently ignored.
|
|
381
|
-
- Multiple column refs in one SELECT column fall back to `{ type: 'string' }` without error.
|
|
382
|
-
- `CAST` type information is not propagated — a `CAST(col AS ARRAY<VARCHAR>)` is still treated as the underlying column's schema.
|
|
383
|
-
- `CASE` expressions always fall back to `{ type: 'string' }`.
|
|
384
|
-
- UNION branches are not checked for column-count or type compatibility.
|
|
385
|
-
- Multiple matched API operations for the same table produce multiple `ResolvedTable` entries but conflicts are not reported.
|
|
386
|
-
- `OR` conditions in WHERE are not used by `api-matcher` for path/method matching — only AND chains are examined.
|
|
387
|
-
- `ast.json` is written to disk on every linted SQL string (debug artifact, should be removed for production use).
|
package/src/athena/PLAN.md
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
# Athena Rule — Visitor Pattern Refactor Plan
|
|
2
|
-
|
|
3
|
-
## Current State
|
|
4
|
-
|
|
5
|
-
`athena.ts` implements `checkAthenaAst` / `checkSelect` as a single monolithic recursive function (~270 lines, flagged with `sonarjs/cognitive-complexity`). It processes the SQL AST imperatively using JSONPath queries scattered throughout:
|
|
6
|
-
|
|
7
|
-
- `$.from..[?(@ && @.table && !@.column)]` — extract table refs
|
|
8
|
-
- `"$..from[?(@ && @.type === 'unnest')]"` — extract UNNEST
|
|
9
|
-
- `"$..[?(@ && @.type === 'column_ref' && @.column)]"` — find column refs per selected column
|
|
10
|
-
- `"$..[?(@ && @.type === 'function' && …)]…"` — find `json_extract_scalar` / `json_extract`
|
|
11
|
-
- `"$..[?(@ && @.type === 'column_ref' && @.array_index)]…"` — find JSON-bracket accessors
|
|
12
|
-
|
|
13
|
-
These ad-hoc JSONPath walks make the control flow hard to follow and will only get harder as new SQL constructs are added (JOINs, subqueries, CASE expressions, etc.).
|
|
14
|
-
|
|
15
|
-
## What the Visitor Pattern Should Do
|
|
16
|
-
|
|
17
|
-
A visitor walks the typed AST once, dispatching to typed handlers per node kind. The goal is:
|
|
18
|
-
|
|
19
|
-
1. **Separation of concerns** — each handler knows exactly one AST node shape.
|
|
20
|
-
2. **Typed dispatch** — no `as unknown`, no runtime `@.type === 'foo'` predicates.
|
|
21
|
-
3. **Composability** — multiple passes can reuse the same visitor infrastructure.
|
|
22
|
-
4. **Testability** — individual visitors can be unit-tested against small AST fixtures.
|
|
23
|
-
|
|
24
|
-
## AST Node Taxonomy (from `types.ts`)
|
|
25
|
-
|
|
26
|
-
The PEG grammar produces nodes that fall into these families:
|
|
27
|
-
|
|
28
|
-
| Node type (`.type` field) | Interface | Visitor hook needed |
|
|
29
|
-
| --------------------------------- | ----------------------- | ------------------- |
|
|
30
|
-
| `"select"` | `Select` | `visitSelect` |
|
|
31
|
-
| `"binary_expr"` | `Binary` | `visitBinary` |
|
|
32
|
-
| `"column_ref"` | `ColumnRefItem` | `visitColumnRef` |
|
|
33
|
-
| `"function"` | `Function` | `visitFunction` |
|
|
34
|
-
| `"aggr_func"` | `AggrFunc` | `visitAggrFunc` |
|
|
35
|
-
| `"cast"` | `Cast` | `visitCast` |
|
|
36
|
-
| `"case"` | `Case` | `visitCase` |
|
|
37
|
-
| `"expr_list"` | `ExprList` | `visitExprList` |
|
|
38
|
-
| `"unnest"` (virtual, in `from`) | `ColumnRefItem` subtype | `visitUnnest` |
|
|
39
|
-
| literal / value nodes | `ValueExpr` | `visitValue` |
|
|
40
|
-
| `BaseFrom` / `Join` / `TableExpr` | `From` union | `visitFrom` |
|
|
41
|
-
| `With` item | `With` | `visitWith` |
|
|
42
|
-
|
|
43
|
-
`Select._next` links UNION branches; those also need traversal via `visitSelect`.
|
|
44
|
-
|
|
45
|
-
## What Is Missing in `visitor.ts`
|
|
46
|
-
|
|
47
|
-
The file currently has 5 lines:
|
|
48
|
-
|
|
49
|
-
```ts
|
|
50
|
-
export default function (ast: unknown, context: Context) {
|
|
51
|
-
if (typeof ast !== 'object' || ast === null) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Everything is missing:
|
|
58
|
-
|
|
59
|
-
### 1. Visitor interface / type map
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
// Minimal shape needed
|
|
63
|
-
type VisitorMap = {
|
|
64
|
-
visitSelect?(node: Select, ctx: VisitContext): void;
|
|
65
|
-
visitWith?(node: With, ctx: VisitContext): void;
|
|
66
|
-
visitFrom?(node: From, ctx: VisitContext): void;
|
|
67
|
-
visitColumn?(node: Column, ctx: VisitContext): void;
|
|
68
|
-
visitColumnRef?(node: ColumnRefItem, ctx: VisitContext): void;
|
|
69
|
-
visitFunction?(node: Function, ctx: VisitContext): void;
|
|
70
|
-
visitBinary?(node: Binary, ctx: VisitContext): void;
|
|
71
|
-
visitValue?(node: ValueExpr, ctx: VisitContext): void;
|
|
72
|
-
// … etc.
|
|
73
|
-
};
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
All hooks should be optional so callers only implement what they care about.
|
|
77
|
-
|
|
78
|
-
### 2. Typed `VisitContext`
|
|
79
|
-
|
|
80
|
-
`context.ts` currently defines a `Table` interface (not a context class). We need a mutable context that accumulates resolution state **per SELECT scope**:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
interface VisitContext {
|
|
84
|
-
// resolved service tables keyed by name/alias
|
|
85
|
-
tables: Map<string, ResolvedTable>;
|
|
86
|
-
// columns resolved so far in this SELECT
|
|
87
|
-
columns: Map<string, ResolvedColumn[]>;
|
|
88
|
-
// alias → real-name mapping
|
|
89
|
-
aliases: Map<string, string>;
|
|
90
|
-
// schemas loaded from disk, shared across the whole query
|
|
91
|
-
apiSchemas: Map<string, ApiSchemas[]>;
|
|
92
|
-
// parent context (for CTE / subquery scoping)
|
|
93
|
-
parent?: VisitContext;
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`context.ts` and `column.ts` have started down this road but are incomplete and have incorrect imports (`.ts` extensions in import paths, broken `Context` base class usage).
|
|
98
|
-
|
|
99
|
-
### 3. `visit(node, ctx)` dispatcher
|
|
100
|
-
|
|
101
|
-
The core dispatcher needs to:
|
|
102
|
-
|
|
103
|
-
- Accept `unknown` input (PEG output is not statically typed at boundaries)
|
|
104
|
-
- Read `node.type` and call the appropriate handler
|
|
105
|
-
- Recursively descend into child nodes
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
function visit(node: unknown, ctx: VisitContext, visitor: VisitorMap): void {
|
|
109
|
-
if (typeof node !== 'object' || node === null) return;
|
|
110
|
-
const typed = node as { type?: string };
|
|
111
|
-
switch (typed.type) {
|
|
112
|
-
case 'select':
|
|
113
|
-
return visitor.visitSelect?.(node as Select, ctx);
|
|
114
|
-
case 'binary_expr':
|
|
115
|
-
return visitor.visitBinary?.(node as Binary, ctx);
|
|
116
|
-
case 'column_ref':
|
|
117
|
-
return visitor.visitColumnRef?.(node as ColumnRefItem, ctx);
|
|
118
|
-
case 'function':
|
|
119
|
-
return visitor.visitFunction?.(node as Function, ctx);
|
|
120
|
-
// … etc.
|
|
121
|
-
}
|
|
122
|
-
// fallback: recurse into all object values
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### 4. Per-node child traversal helpers
|
|
127
|
-
|
|
128
|
-
Each handler should call `visitChildren` (or per-kind helpers) to recurse. Missing helpers:
|
|
129
|
-
|
|
130
|
-
- `visitSelectChildren(node: Select, ctx, visitor)` — visits `from[]`, `columns[]`, `where`, `groupby`, `_next`
|
|
131
|
-
- `visitFromChildren(node: From, ctx, visitor)` — handles `BaseFrom`, `Join` (recurse into `.on`), `TableExpr` (recurse into subquery `.expr.ast`)
|
|
132
|
-
- `visitBinaryChildren(node: Binary, ctx, visitor)` — visits `.left` and `.right`
|
|
133
|
-
- `visitFunctionChildren(node: Function, ctx, visitor)` — visits `.args.value[]`
|
|
134
|
-
|
|
135
|
-
### 5. Concrete resolution visitor for `checkSelect` logic
|
|
136
|
-
|
|
137
|
-
The current `checkSelect` logic should be decomposed into a `ResolutionVisitor` that implements `VisitorMap`:
|
|
138
|
-
|
|
139
|
-
- `visitWith` → calls `checkSelect` on the CTE body, registers the result in `ctx.tables`
|
|
140
|
-
- `visitFrom` → resolves service table → calls `locateApi` + `matchApi` → populates `ctx.tables`
|
|
141
|
-
- `visitUnnest` → maps unnested array columns
|
|
142
|
-
- `visitColumnRef` inside a `SELECT` column → looks up the column in `ctx.tables`, applies property accessor, records in `ctx.columns`
|
|
143
|
-
- `visitFunction` → detects `json_extract_scalar` / `json_extract`, extracts the path argument
|
|
144
|
-
|
|
145
|
-
### 6. `service-table.ts` needs to be completed
|
|
146
|
-
|
|
147
|
-
The file duplicates large chunks of `athena.ts` but is incomplete (references `MatchedOperation`, `ResolvedColumn`, `AthenaContext`, `log`, etc. that are not imported). It was apparently started as the extraction destination for service-table logic. Missing:
|
|
148
|
-
|
|
149
|
-
- Import `MatchedOperation` from `api-matcher`
|
|
150
|
-
- Import `ResolvedColumn` (define locally or re-export from `athena.ts`)
|
|
151
|
-
- Import `log`, `SCHEMA_STRING`, `SCHEMA_OBJECT`
|
|
152
|
-
- Export `buildServiceTable(tableAST, selectAST, apiSchemas): ServiceTable` as the single factory function
|
|
153
|
-
- Remove the duplicate `checkSelect` / `checkAthenaAst` body currently pasted in
|
|
154
|
-
|
|
155
|
-
### 7. `index.ts` needs its commented-out code resolved
|
|
156
|
-
|
|
157
|
-
`index.ts` has the full `ServiceEndpoint` / `Service` types but all the context/resolver interfaces are commented out. Once `visitor.ts` and `service-table.ts` are solid, these should either be deleted or promoted to the proper types.
|
|
158
|
-
|
|
159
|
-
### 8. `zzz.spec.ts` is a broken scratch file
|
|
160
|
-
|
|
161
|
-
`zzz.spec.ts` has syntax errors (`type Table =` with no RHS, calling `visit` instead of `visitSelect`, missing `context` arg). It was clearly a scratch experiment for the visitor idea. It should either become a proper unit test of the visitor once it exists, or be deleted.
|
|
162
|
-
|
|
163
|
-
## Suggested File Structure After Refactor
|
|
164
|
-
|
|
165
|
-
```
|
|
166
|
-
src/athena/
|
|
167
|
-
types.ts — unchanged (PEG AST types)
|
|
168
|
-
visitor.ts — IMPLEMENT: VisitorMap, VisitContext, visit(), visitChildren helpers
|
|
169
|
-
service-table.ts — COMPLETE: buildServiceTable() factory, ServiceTable type
|
|
170
|
-
context.ts — REWRITE: VisitContext (replace broken Table/Column class tree)
|
|
171
|
-
column.ts — REWRITE: ResolvedColumn value type (not a class extending Context)
|
|
172
|
-
index.ts — CLEAN UP: remove commented-out code once visitor types exist
|
|
173
|
-
athena.ts — REFACTOR: replace checkSelect monolith with visitor composition
|
|
174
|
-
athena.spec.ts — unchanged (integration tests)
|
|
175
|
-
zzz.spec.ts — FIX or DELETE: make it a real visitor unit test
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Recommended Implementation Order
|
|
179
|
-
|
|
180
|
-
1. **Define `VisitContext` in `context.ts`** — the mutable scope object passed through the tree.
|
|
181
|
-
2. **Define `VisitorMap` and `visit()` dispatcher in `visitor.ts`** — untyped input → typed dispatch.
|
|
182
|
-
3. **Add `visitSelectChildren` and other traversal helpers** — recursive descent wiring.
|
|
183
|
-
4. **Refactor `service-table.ts`** — `buildServiceTable()` extracts the FROM-resolution block out of `checkSelect`.
|
|
184
|
-
5. **Build `ResolutionVisitor`** — implements `VisitorMap`, accumulates `ctx.columns`, replaces current `checkSelect` body.
|
|
185
|
-
6. **Wire into `athena.ts`** — `checkAthenaAst` creates a root `VisitContext`, runs `visit(ast, ctx, resolutionVisitor)`.
|
|
186
|
-
7. **Fix `zzz.spec.ts`** into a real test of the visitor dispatch.
|
|
187
|
-
|
|
188
|
-
## Key Design Decisions To Resolve
|
|
189
|
-
|
|
190
|
-
- **One visitor pass or two?** The current code does a single forward pass (resolves tables, then processes columns). A visitor architecture could do two passes (first pass builds the table map, second pass resolves columns), which would simplify handling forward references between CTEs — but is not required yet.
|
|
191
|
-
- **Array of tables vs. Map** — the current `allResolvedTables: Record<string, Table[]>` uses arrays to represent ambiguity (multiple matching API endpoints). The new `VisitContext` should preserve this, but consider whether `Map<string, ResolvedTable[]>` is cleaner.
|
|
192
|
-
- **Error handling** — `AthenaError` is currently thrown and caught at the top level. With a visitor, errors should either propagate the same way, or be collected in `ctx.errors[]` so all errors in a query are reported at once.
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Future Improvements — Supporting More Complex SQL
|
|
197
|
-
|
|
198
|
-
The following gaps exist in the current implementation. Each item describes what SQL pattern is not yet handled, why it matters, and what work is needed.
|
|
199
|
-
|
|
200
|
-
### 1. JOIN Resolution
|
|
201
|
-
|
|
202
|
-
**What fails today:** `JOIN` tables (`isJoin`) are silently skipped in `resolveFromClause`. Any column from a joined table that is not also reachable through a plain `BaseFrom` causes a "can't found column" error or silently produces a wrong result.
|
|
203
|
-
|
|
204
|
-
**Affected patterns:**
|
|
205
|
-
|
|
206
|
-
```sql
|
|
207
|
-
SELECT req.message, res.message
|
|
208
|
-
FROM request_message AS req
|
|
209
|
-
JOIN response_message AS res ON json_extract_scalar(res.message, '$.id') = json_extract_scalar(req.message, '$.id')
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**Work needed:**
|
|
213
|
-
|
|
214
|
-
- In `resolveFromClause`, handle `isJoin` nodes: the `Join` interface extends `BaseFrom`, so the `table` / `as` fields are available. Register the joined table in `ctx.tables` the same way as a plain `BaseFrom`.
|
|
215
|
-
- Add `visitJoin` hook in `visitor.ts`'s resolution path.
|
|
216
|
-
- The `ON` clause is an expression and cannot be used to narrow the API match (unlike WHERE). The join condition should be walked for column reference validation only.
|
|
217
|
-
|
|
218
|
-
### 2. Subquery / Inline View Resolution (`TableExpr`)
|
|
219
|
-
|
|
220
|
-
**What fails today:** `TableExpr` nodes (subqueries in the FROM clause) are skipped. Columns from subqueries cannot be resolved.
|
|
221
|
-
|
|
222
|
-
**Affected patterns:**
|
|
223
|
-
|
|
224
|
-
```sql
|
|
225
|
-
SELECT t.url FROM (SELECT url, method FROM link) AS t
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
**Work needed:**
|
|
229
|
-
|
|
230
|
-
- In `resolveFromClause`, detect `isTableExpr` items and recursively call `checkSelect` on `item.expr.ast`, capturing the resulting columns as a new `ResolvedTable` in `ctx.tables[alias]`.
|
|
231
|
-
- The child SELECT needs its own `createChildContext(ctx)` so its own alias map doesn't pollute the outer scope.
|
|
232
|
-
|
|
233
|
-
### 3. CAST Type Propagation
|
|
234
|
-
|
|
235
|
-
**What fails today:** `CAST(expr AS type)` is walked but the target type is ignored. The column receives the schema of the inner expression, not the cast type.
|
|
236
|
-
|
|
237
|
-
**Affected patterns:**
|
|
238
|
-
|
|
239
|
-
```sql
|
|
240
|
-
SELECT CAST(json_extract(responsebody, '$.links') AS ARRAY<VARCHAR>) AS linkages
|
|
241
|
-
FROM link CROSS JOIN UNNEST(linkages) AS t(linkage)
|
|
242
|
-
-- linkages has schema { type:'object' } instead of { type:'array', items:{ type:'string' } }
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
This means UNNEST pre-pass correctly finds `linkages` is `array` only when the JSON schema already says so, not when it is produced by a CAST. Two tests in `athena.spec.ts` carry a `[TODO:] handle array item type extraction using schema dereferencing` comment on exactly this case.
|
|
246
|
-
|
|
247
|
-
**Work needed:**
|
|
248
|
-
|
|
249
|
-
- In `resolveSelectColumns`, detect that `hasFunctionCalls` found a `cast` node.
|
|
250
|
-
- Use `extractCastTarget(expr)` (new helper) to get `target.dataType` (e.g. `'ARRAY'`, `'MAP'`, `'VARCHAR'`, `'JSON'`).
|
|
251
|
-
- Map Athena/Trino type names to JSON Schema equivalents and substitute the schema before storing the column.
|
|
252
|
-
- Extend `ColumnRefWithIndex` or add a new `CastExpr` shape to make the type accessible without `unknown` casts.
|
|
253
|
-
|
|
254
|
-
### 4. CASE Expression Schema
|
|
255
|
-
|
|
256
|
-
**What fails today:** A column whose expression is a `case` node has 0 `column_ref` items at the top level (each arm has refs, but the rule counts refs at the outer level). It falls into the "multiple refs → string" bucket even when all arms resolve to the same type.
|
|
257
|
-
|
|
258
|
-
**Affected patterns:**
|
|
259
|
-
|
|
260
|
-
```sql
|
|
261
|
-
SELECT CASE WHEN method = 'GET' THEN responsebody ELSE requestbody END AS body FROM link
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**Work needed:**
|
|
265
|
-
|
|
266
|
-
- Special-case `case` nodes in pass 2: collect refs from each arm's `result`, intersect the resolved schemas, and emit that as the output schema.
|
|
267
|
-
- Add `extractCaseArms(expr)` helper in `visitor.ts`.
|
|
268
|
-
|
|
269
|
-
### 5. OR Conditions in API Matcher
|
|
270
|
-
|
|
271
|
-
**What fails today:** `api-matcher.ts` extracts path/method conditions only from AND-chains in the WHERE clause. SQL that uses `OR` to express version-specific URL patterns is handled by the rule passing multiple matched operations — but only when the condition can be matched by each alternative independently. A disjunction like `(split(url,'/')[3]='v1' AND ...) OR (split(url,'/')[3]='v2' AND ...)` produces no match for either branch individually, so `matchApi` throws.
|
|
272
|
-
|
|
273
|
-
**Affected patterns:**
|
|
274
|
-
|
|
275
|
-
```sql
|
|
276
|
-
WHERE method = 'PUT'
|
|
277
|
-
AND (
|
|
278
|
-
(split(url, '/')[4] = 'card' AND cardinality(split(url, '/')) = 5)
|
|
279
|
-
OR
|
|
280
|
-
(split(url, '/')[4] = 'card' AND split(url, '/')[6] = 'number' AND cardinality(split(url, '/')) = 6)
|
|
281
|
-
)
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
The test case `'AND/OR'` in `athena.spec.ts` is currently in the `invalid` set with a misleading error message — it is expected to be valid, but fails today.
|
|
285
|
-
|
|
286
|
-
**Work needed:**
|
|
287
|
-
|
|
288
|
-
- Teach `api-matcher.ts` to recognise OR-of-AND patterns: extract each AND branch as a separate matcher set, union all matched operations across branches.
|
|
289
|
-
- Alternatively, fall back to "no path constraint" when an OR is detected at the top level of the WHERE, accepting a potentially larger set of matched operations.
|
|
290
|
-
|
|
291
|
-
### 6. UNION Column Compatibility Check
|
|
292
|
-
|
|
293
|
-
**What fails today:** UNION ALL branches are resolved independently; their column count and schemas are never compared. A UNION where one branch has a different column name or an incompatible type silently passes.
|
|
294
|
-
|
|
295
|
-
**Work needed:**
|
|
296
|
-
|
|
297
|
-
- After resolving `select._next`, compare the column maps (names and schemas) between the current SELECT and the next SELECT.
|
|
298
|
-
- Report `AthenaError` if column counts differ or if a column in the next branch does not exist in the current branch.
|
|
299
|
-
- Consider whether schema compatibility checking (e.g. `string` vs `object`) should also be enforced, or just name-level matching.
|
|
300
|
-
|
|
301
|
-
### 7. Multiple Matched Operations — Conflict Reporting
|
|
302
|
-
|
|
303
|
-
**What fails today:** When multiple API operations match for the same service table (e.g. two different response status codes, or GET and POST), `buildServiceTables` silently creates multiple `ResolvedTable` entries. Column resolution then collects schemas from all of them. Conflicting schemas (e.g. different response body shapes for 200 vs 204) are silently merged.
|
|
304
|
-
|
|
305
|
-
**Work needed:**
|
|
306
|
-
|
|
307
|
-
- After `buildServiceTables`, if `operations.length > 1`, emit a warning (not an error, since it may be intentional) listing the matched operations.
|
|
308
|
-
- In `resolveSelectColumns`, when `resolvedColumns.length > 1`, check whether all schemas are identical. If not, either report a warning or fall back to `{ type: 'object' }`.
|
|
309
|
-
|
|
310
|
-
### 8. Error Collection (All Errors Per Query)
|
|
311
|
-
|
|
312
|
-
**What fails today:** The first `AthenaError` thrown immediately exits resolution, so only one error per SQL string is reported even if the query has multiple bad property accesses.
|
|
313
|
-
|
|
314
|
-
**Work needed:**
|
|
315
|
-
|
|
316
|
-
- Add `errors: AthenaError[]` to `VisitContext`.
|
|
317
|
-
- Replace `throw new AthenaError(...)` with `ctx.errors.push(...); continue` in pass 2.
|
|
318
|
-
- After `checkAthenaAst` returns, report each collected error as a separate `context.report(...)` call (or a single report with all messages joined).
|
|
319
|
-
|
|
320
|
-
### 9. Remove `ast.json` Debug Write
|
|
321
|
-
|
|
322
|
-
**What fails today (non-functional):** `athena.ts` writes `ast.json` to disk on every linted SQL string:
|
|
323
|
-
|
|
324
|
-
```typescript
|
|
325
|
-
fs.writeFileSync('ast.json', JSON.stringify(ast, undefined, 2));
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
This is a development artifact. In production it creates a file in the working directory on every lint run and makes the rule non-deterministic in parallel lint runs.
|
|
329
|
-
|
|
330
|
-
**Work needed:** Delete the `fs.writeFileSync` call and the `import fs from 'node:fs'` import. If debug AST output is needed, gate it behind `DEBUG=eslint-plugin:athena` using the `log` function already available.
|
|
331
|
-
|
|
332
|
-
### 10. Error Location in Reports
|
|
333
|
-
|
|
334
|
-
**What fails today:** The rule reports errors at the template literal node level (the whole `` `...` `` span). `ResolvedColumn.ast` stores the originating column AST node with `loc` information, but this is never used.
|
|
335
|
-
|
|
336
|
-
**Work needed:**
|
|
337
|
-
|
|
338
|
-
- The PEG parser already attaches `loc: { start: { line, column, offset }, end: {...} }` to AST nodes (when `includeLocations: true`).
|
|
339
|
-
- Map `loc` back to ESLint's `TSESTree.SourceLocation` and use `context.report({ loc: ... })` to pinpoint the exact column expression that failed rather than the whole template literal.
|
|
340
|
-
- The helper `getErrorLocation(ast)` previously existed in the old `athena.ts` (removed in the refactor) as a reference implementation.
|
|
341
|
-
|
|
342
|
-
### 11. `parameters` CTE Pattern — FROM-less SELECT
|
|
343
|
-
|
|
344
|
-
**What fails today:** CTEs with no FROM clause (e.g. `SELECT '' AS p_from, '' AS p_to`) produce a `Select` with `from: null`. These currently pass through `resolveFromClause` without registering any table, and then `resolveSelectColumns` tries to iterate zero tables. Because the columns are literal strings (0 `column_ref`s), they fall into the "no column references → use string default" branch and pass. However if someone writes `SELECT p_from FROM parameters` in a later CTE, the `parameters` table is looked up and resolved correctly because `checkSelect` registers its output in `ctx.tables` regardless. **This case already works**, but it is worth documenting explicitly and adding a dedicated test case to guard against regressions.
|
|
345
|
-
|
|
346
|
-
### 12. Quoted Table Names
|
|
347
|
-
|
|
348
|
-
**What fails today (partially):** Table names enclosed in double quotes (e.g. `FROM "payment-card"`) are parsed by the PEG grammar but the quotes are preserved in the `table` field: `table: '"payment-card"'`. `locateApi` then looks for a service named `"payment-card"` including the quotes, which fails to match the camelCase folder name `paymentCard`.
|
|
349
|
-
|
|
350
|
-
The test case `'SELECT with FROM - table name with double quotes'` is currently in the valid set, which suggests the quoted-table fallback path (no matching service → no error) is silently accepted. If schemas exist for `payment-card`, the rule would fail to find them.
|
|
351
|
-
|
|
352
|
-
**Work needed:**
|
|
353
|
-
|
|
354
|
-
- Strip leading/trailing `"` or `'` from `tableName` before calling `locateApi` in `resolveFromClause`.
|
|
355
|
-
- Add a test case that actually has schemas for a quoted-name service and verifies column resolution succeeds.
|