@checkdigit/eslint-plugin 7.18.0-PR.143-3aac → 7.18.0-PR.143-1cc5
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/dist-mjs/athena/athena.mjs +92 -20
- package/dist-mjs/athena/visitor.mjs +13 -1
- package/dist-mjs/peggy/athena-peggy.mjs +10 -6
- package/dist-types/athena/visitor.d.ts +2 -0
- package/package.json +1 -1
- package/src/athena/athena.ts +109 -24
- package/src/athena/visitor.ts +13 -0
- package/src/peggy/athena-peggy.ts +5 -1
- package/src/peggy/athena.peggy +6 -2
- package/src/peggy/athena-chat.peggy +0 -608
|
@@ -47,6 +47,8 @@ export declare function isJoin(node: unknown): node is Join;
|
|
|
47
47
|
export declare function isBaseFrom(node: unknown): node is BaseFrom;
|
|
48
48
|
export declare function hasArrayIndex(node: ColumnRefItem): node is ColumnRefWithIndex;
|
|
49
49
|
export declare function walk(node: unknown, visitor: VisitorMap): void;
|
|
50
|
+
/** Return true if the expression contains a lambda (`->`) subexpression. */
|
|
51
|
+
export declare function containsLambda(expr: unknown): boolean;
|
|
50
52
|
/** Collect all column_ref nodes within an expression subtree. */
|
|
51
53
|
export declare function extractColumnRefs(expr: unknown): ColumnRefItem[];
|
|
52
54
|
/** Return the path string from the first json_extract_scalar / json_extract call found. */
|
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-1cc5","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":">=22.18"},"service":{"api":{"root":"src","endpoints":["api/v1"]}},"wallaby":{"env":{"params":{"runner":"--experimental-vm-modules"}}}}
|
package/src/athena/athena.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// athena/athena.ts
|
|
2
2
|
|
|
3
|
+
/* eslint-disable max-lines */
|
|
4
|
+
|
|
3
5
|
/*
|
|
4
6
|
* Copyright (c) 2021-2026 Check Digit, LLC
|
|
5
7
|
*
|
|
@@ -14,7 +16,7 @@ import { AST_NODE_TYPES, ESLintUtils, type TSESTree } from '@typescript-eslint/u
|
|
|
14
16
|
import type { SchemaObject } from 'ajv/dist/2020';
|
|
15
17
|
|
|
16
18
|
import { parse } from '../peggy/athena-peggy.ts';
|
|
17
|
-
import type { AST, From, Select, With } from './types';
|
|
19
|
+
import type { AST, BaseFrom, From, Select, With } from './types';
|
|
18
20
|
import { matchApi } from './api-matcher.ts';
|
|
19
21
|
import { locateApi } from './api-locator.ts';
|
|
20
22
|
import {
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
} from './context.ts';
|
|
27
29
|
import { buildServiceTables } from './service-table.ts';
|
|
28
30
|
import {
|
|
31
|
+
containsLambda,
|
|
29
32
|
extractBracketAccessorPath,
|
|
30
33
|
extractColumnRefs,
|
|
31
34
|
extractJsonExtractCalls,
|
|
@@ -141,6 +144,23 @@ function fromClauseItems(select: Select): From[] {
|
|
|
141
144
|
// Pass 1 — Resolve FROM clause: service tables → ctx.tables + ctx.aliases
|
|
142
145
|
// ---------------------------------------------------------------------------
|
|
143
146
|
|
|
147
|
+
function resolveServiceTable(select: Select, item: BaseFrom, ctx: VisitContext): void {
|
|
148
|
+
const { table: tableName } = item;
|
|
149
|
+
try {
|
|
150
|
+
const apiSchemas = getApiSchemas(tableName, ctx);
|
|
151
|
+
if (apiSchemas.length === 0) {
|
|
152
|
+
throw new AthenaError(ATHENA_ERROR, `service not found: "${tableName}" (no swagger schema located)`, item);
|
|
153
|
+
}
|
|
154
|
+
const operations = matchApi(select, item, apiSchemas) ?? [];
|
|
155
|
+
ctx.tables.set(tableName, buildServiceTables(tableName, operations));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof AthenaError) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
throw new AthenaError(ATHENA_ERROR, error instanceof Error ? error.message : String(error), item);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
144
164
|
function resolveFromClause(select: Select, ctx: VisitContext): void {
|
|
145
165
|
for (const item of fromClauseItems(select)) {
|
|
146
166
|
if (isUnnestFrom(item)) {
|
|
@@ -153,9 +173,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
|
|
|
153
173
|
ctx.aliases.set(alias, tableName);
|
|
154
174
|
}
|
|
155
175
|
if (!ctx.tables.has(tableName)) {
|
|
156
|
-
|
|
157
|
-
const operations = matchApi(select, item, apiSchemas) ?? [];
|
|
158
|
-
ctx.tables.set(tableName, buildServiceTables(tableName, operations));
|
|
176
|
+
resolveServiceTable(select, item, ctx);
|
|
159
177
|
}
|
|
160
178
|
continue;
|
|
161
179
|
}
|
|
@@ -174,11 +192,7 @@ function resolveFromClause(select: Select, ctx: VisitContext): void {
|
|
|
174
192
|
continue; // already resolved (CTE or duplicate)
|
|
175
193
|
}
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
// matchApi throws when no operation matches, so operations is always defined here.
|
|
179
|
-
const apiSchemas = getApiSchemas(tableName, ctx);
|
|
180
|
-
const operations = matchApi(select, item, apiSchemas) ?? [];
|
|
181
|
-
ctx.tables.set(tableName, buildServiceTables(tableName, operations));
|
|
195
|
+
resolveServiceTable(select, item, ctx);
|
|
182
196
|
}
|
|
183
197
|
}
|
|
184
198
|
|
|
@@ -278,6 +292,28 @@ function expandWildcard(referencedTables: ResolvedTable[], columns: Map<string,
|
|
|
278
292
|
}
|
|
279
293
|
}
|
|
280
294
|
|
|
295
|
+
// If every resolved column shares the same object schema, return a hint listing its top-level
|
|
296
|
+
// properties. Returns '' when schemas differ across matched operations or the schema isn't an object.
|
|
297
|
+
function schemaPropertyHint(resolvedColumns: ResolvedColumn[]): string {
|
|
298
|
+
if (resolvedColumns.length === 0) {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
const first = JSON.stringify(resolvedColumns[0]?.schema);
|
|
302
|
+
if (!resolvedColumns.every((col) => JSON.stringify(col.schema) === first)) {
|
|
303
|
+
return '';
|
|
304
|
+
}
|
|
305
|
+
const schema = resolvedColumns[0]?.schema;
|
|
306
|
+
if (schema?.type !== 'object') {
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
const properties = schema.properties;
|
|
310
|
+
if (properties === undefined) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
const propNames = Object.keys(properties);
|
|
314
|
+
return propNames.length > 0 ? `; available properties: ${propNames.join(', ')}` : '';
|
|
315
|
+
}
|
|
316
|
+
|
|
281
317
|
function resolveSchemaAtPath(
|
|
282
318
|
colRef: string,
|
|
283
319
|
propertyAccessor: string,
|
|
@@ -295,7 +331,11 @@ function resolveSchemaAtPath(
|
|
|
295
331
|
log('extracted schemas', extractedSchemas);
|
|
296
332
|
|
|
297
333
|
if (extractedSchemas.length === 0) {
|
|
298
|
-
throw new AthenaError(
|
|
334
|
+
throw new AthenaError(
|
|
335
|
+
ATHENA_ERROR,
|
|
336
|
+
`property not found ${colRef} - ${propertyAccessor}${schemaPropertyHint(resolvedColumns)}`,
|
|
337
|
+
ast,
|
|
338
|
+
);
|
|
299
339
|
}
|
|
300
340
|
return extractedSchemas;
|
|
301
341
|
}
|
|
@@ -321,6 +361,41 @@ function navigateSchemaPath(
|
|
|
321
361
|
// Pass 2 — Resolve SELECT columns → Map<name, ResolvedColumn[]>
|
|
322
362
|
// ---------------------------------------------------------------------------
|
|
323
363
|
|
|
364
|
+
/** Resolve a column by name against `referencedTables`, throwing if not found. */
|
|
365
|
+
function lookupColumnOrThrow(
|
|
366
|
+
colRef: string,
|
|
367
|
+
ref: object,
|
|
368
|
+
referencedTables: ResolvedTable[],
|
|
369
|
+
ctx: VisitContext,
|
|
370
|
+
): ResolvedColumn[] {
|
|
371
|
+
const resolvedColumns = referencedTables.flatMap((table) => table.columns.get(colRef) ?? []);
|
|
372
|
+
if (resolvedColumns.length === 0) {
|
|
373
|
+
const tableNames = [...ctx.tables.keys()].join(', ');
|
|
374
|
+
const availableCols = [...new Set(referencedTables.flatMap((table) => [...table.columns.keys()]))].join(', ');
|
|
375
|
+
throw new AthenaError(
|
|
376
|
+
ATHENA_ERROR,
|
|
377
|
+
`can't found column ${colRef} in tables: ${tableNames}; available columns: ${availableCols}`,
|
|
378
|
+
ref,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return resolvedColumns;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Check all column_refs in `ast` exist in scope. Skips lambda expressions (lambda params look like column_refs). */
|
|
385
|
+
function checkColumnRefsExist(ast: unknown, allTables: ResolvedTable[], ctx: VisitContext): void {
|
|
386
|
+
if (containsLambda(ast)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const ref of extractColumnRefs(ast)) {
|
|
390
|
+
const colRef = typeof ref.column === 'string' ? ref.column : undefined;
|
|
391
|
+
if (colRef === undefined || colRef === '*') {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const tableRef = ref.table ?? undefined;
|
|
395
|
+
lookupColumnOrThrow(colRef, ref, tableRef !== undefined ? lookupTables(tableRef, ctx) : allTables, ctx);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
324
399
|
/** Validate all json_extract / json_extract_scalar paths in a complex column expression. */
|
|
325
400
|
function validateComplexColumnExpression(columnAST: unknown, allTables: ResolvedTable[], ctx: VisitContext): void {
|
|
326
401
|
for (const { ref, path, fnNode } of extractJsonExtractCalls(columnAST)) {
|
|
@@ -361,12 +436,7 @@ function resolveSingleColumnRef(
|
|
|
361
436
|
|
|
362
437
|
const withFunctions = hasFunctionCalls(columnAST);
|
|
363
438
|
const colName = columnAlias ?? (withFunctions ? indexedName : colRef);
|
|
364
|
-
const resolvedColumns =
|
|
365
|
-
|
|
366
|
-
if (resolvedColumns.length === 0) {
|
|
367
|
-
const tableNames = [...ctx.tables.keys()].join(', ');
|
|
368
|
-
throw new AthenaError(ATHENA_ERROR, `can't found column ${colRef} in tables: ${tableNames}`, columnAST as object);
|
|
369
|
-
}
|
|
439
|
+
const resolvedColumns = lookupColumnOrThrow(colRef, ref, referencedTables, ctx);
|
|
370
440
|
|
|
371
441
|
const propertyAccessor = extractJsonExtractPath(columnAST) ?? extractBracketAccessorPath(columnAST);
|
|
372
442
|
if (propertyAccessor !== undefined) {
|
|
@@ -392,6 +462,7 @@ function resolveSelectColumns(select: Select, ctx: VisitContext): Map<string, Re
|
|
|
392
462
|
const columnRefs = extractColumnRefs(columnAST);
|
|
393
463
|
|
|
394
464
|
if (columnRefs.length !== 1) {
|
|
465
|
+
checkColumnRefsExist(columnAST, allTables, ctx);
|
|
395
466
|
validateComplexColumnExpression(columnAST, allTables, ctx);
|
|
396
467
|
resolveDefaultSchemaColumn(columnAlias, indexedName, columnAST, columns);
|
|
397
468
|
continue;
|
|
@@ -499,21 +570,33 @@ const rule: ESLintUtils.RuleModule<typeof SYNTEXT_ERROR | typeof ATHENA_ERROR> =
|
|
|
499
570
|
return;
|
|
500
571
|
}
|
|
501
572
|
|
|
573
|
+
const sqlMapping = buildSqlMapping(sqlNode);
|
|
502
574
|
let ast: AST;
|
|
503
575
|
try {
|
|
504
576
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
505
577
|
({ ast } = parse(sql, { includeLocations: true }));
|
|
506
578
|
} catch (error) {
|
|
507
|
-
log('error
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
579
|
+
log('error parsing Athena SQL', { error, sql });
|
|
580
|
+
const pegLoc = (error as { location?: { start: { offset: number }; end: { offset: number } } }).location;
|
|
581
|
+
if (pegLoc !== undefined) {
|
|
582
|
+
const sourceText = context.sourceCode.getText();
|
|
583
|
+
context.report({
|
|
584
|
+
loc: {
|
|
585
|
+
start: offsetToLoc(sourceText, sqlOffsetToSource(pegLoc.start.offset, sqlMapping)),
|
|
586
|
+
end: offsetToLoc(sourceText, sqlOffsetToSource(pegLoc.end.offset, sqlMapping)),
|
|
587
|
+
},
|
|
588
|
+
messageId: SYNTEXT_ERROR,
|
|
589
|
+
data: { errorMessage: (error as Error).message },
|
|
590
|
+
});
|
|
591
|
+
} else {
|
|
592
|
+
context.report({
|
|
593
|
+
node: sqlNode,
|
|
594
|
+
messageId: SYNTEXT_ERROR,
|
|
595
|
+
data: { errorMessage: (error as Error).message },
|
|
596
|
+
});
|
|
597
|
+
}
|
|
513
598
|
return;
|
|
514
599
|
}
|
|
515
|
-
|
|
516
|
-
const sqlMapping = buildSqlMapping(sqlNode);
|
|
517
600
|
const athenaCtx = createRootContext();
|
|
518
601
|
try {
|
|
519
602
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
@@ -573,3 +656,5 @@ const rule: ESLintUtils.RuleModule<typeof SYNTEXT_ERROR | typeof ATHENA_ERROR> =
|
|
|
573
656
|
});
|
|
574
657
|
|
|
575
658
|
export default rule;
|
|
659
|
+
|
|
660
|
+
/* eslint-enable max-lines */
|
package/src/athena/visitor.ts
CHANGED
|
@@ -264,6 +264,19 @@ export function walk(node: unknown, visitor: VisitorMap): void {
|
|
|
264
264
|
// Convenience extractors — replace ad-hoc JSONPath queries.
|
|
265
265
|
// -------------------------------------------------------------------
|
|
266
266
|
|
|
267
|
+
/** Return true if the expression contains a lambda (`->`) subexpression. */
|
|
268
|
+
export function containsLambda(expr: unknown): boolean {
|
|
269
|
+
let found = false;
|
|
270
|
+
walkExpr(expr, {
|
|
271
|
+
visitBinary(node) {
|
|
272
|
+
if (node.operator === '->') {
|
|
273
|
+
found = true;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
return found;
|
|
278
|
+
}
|
|
279
|
+
|
|
267
280
|
/** Collect all column_ref nodes within an expression subtree. */
|
|
268
281
|
export function extractColumnRefs(expr: unknown): ColumnRefItem[] {
|
|
269
282
|
const refs: ColumnRefItem[] = [];
|
|
@@ -1553,12 +1553,13 @@ function peg$parse(input: any, options: any): any {
|
|
|
1553
1553
|
var peg$f107 = function (t, alias) {
|
|
1554
1554
|
if (t.type === 'var') {
|
|
1555
1555
|
t.as = alias;
|
|
1556
|
-
return t;
|
|
1556
|
+
return { ...t, ...getLocationObject() };
|
|
1557
1557
|
} else {
|
|
1558
1558
|
return {
|
|
1559
1559
|
db: t.db,
|
|
1560
1560
|
table: t.table,
|
|
1561
1561
|
as: alias,
|
|
1562
|
+
...getLocationObject(),
|
|
1562
1563
|
};
|
|
1563
1564
|
}
|
|
1564
1565
|
};
|
|
@@ -2035,6 +2036,7 @@ function peg$parse(input: any, options: any): any {
|
|
|
2035
2036
|
table: tbl[3],
|
|
2036
2037
|
column: col[3],
|
|
2037
2038
|
collate: ce && ce[1],
|
|
2039
|
+
...getLocationObject(),
|
|
2038
2040
|
};
|
|
2039
2041
|
};
|
|
2040
2042
|
var peg$f187 = function (tbl, col, ce) {
|
|
@@ -2044,6 +2046,7 @@ function peg$parse(input: any, options: any): any {
|
|
|
2044
2046
|
table: tbl,
|
|
2045
2047
|
column: col,
|
|
2046
2048
|
collate: ce && ce[1],
|
|
2049
|
+
...getLocationObject(),
|
|
2047
2050
|
};
|
|
2048
2051
|
};
|
|
2049
2052
|
var peg$f188 = function (col, ce) {
|
|
@@ -2053,6 +2056,7 @@ function peg$parse(input: any, options: any): any {
|
|
|
2053
2056
|
table: null,
|
|
2054
2057
|
column: col,
|
|
2055
2058
|
collate: ce && ce[1],
|
|
2059
|
+
...getLocationObject(),
|
|
2056
2060
|
};
|
|
2057
2061
|
};
|
|
2058
2062
|
var peg$f189 = function (head, tail) {
|
package/src/peggy/athena.peggy
CHANGED
|
@@ -1239,12 +1239,13 @@ table_base
|
|
|
1239
1239
|
/ t:table_name __ alias:alias_clause? {
|
|
1240
1240
|
if (t.type === 'var') {
|
|
1241
1241
|
t.as = alias;
|
|
1242
|
-
return t;
|
|
1242
|
+
return { ...t, ...getLocationObject() };
|
|
1243
1243
|
} else {
|
|
1244
1244
|
return {
|
|
1245
1245
|
db: t.db,
|
|
1246
1246
|
table: t.table,
|
|
1247
|
-
as: alias
|
|
1247
|
+
as: alias,
|
|
1248
|
+
...getLocationObject(),
|
|
1248
1249
|
};
|
|
1249
1250
|
}
|
|
1250
1251
|
}
|
|
@@ -1906,6 +1907,7 @@ column_ref
|
|
|
1906
1907
|
table: tbl[3],
|
|
1907
1908
|
column: col[3],
|
|
1908
1909
|
collate: ce && ce[1],
|
|
1910
|
+
...getLocationObject(),
|
|
1909
1911
|
};
|
|
1910
1912
|
}
|
|
1911
1913
|
/ tbl:ident __ DOT __ col:column ce:(__ collate_expr)? {
|
|
@@ -1915,6 +1917,7 @@ column_ref
|
|
|
1915
1917
|
table: tbl,
|
|
1916
1918
|
column: col,
|
|
1917
1919
|
collate: ce && ce[1],
|
|
1920
|
+
...getLocationObject(),
|
|
1918
1921
|
};
|
|
1919
1922
|
}
|
|
1920
1923
|
/ col:column ce:(__ collate_expr)? {
|
|
@@ -1924,6 +1927,7 @@ column_ref
|
|
|
1924
1927
|
table: null,
|
|
1925
1928
|
column: col,
|
|
1926
1929
|
collate: ce && ce[1],
|
|
1930
|
+
...getLocationObject(),
|
|
1927
1931
|
};
|
|
1928
1932
|
}
|
|
1929
1933
|
|