hana-linter 1.2.0 → 1.4.0
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/CHANGELOG.md +16 -0
- package/README.md +60 -8
- package/dist/assets/.hana-linter.json +48 -0
- package/dist/config.js +9 -1
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +55 -0
- package/dist/content-lint.js +16 -0
- package/dist/parsers/hdbindex/__tests__/extractIndexName.test.d.ts +1 -0
- package/dist/parsers/hdbindex/__tests__/extractIndexName.test.js +207 -0
- package/dist/parsers/hdbindex/index.d.ts +25 -0
- package/dist/parsers/hdbindex/index.js +44 -0
- package/dist/parsers/hdbindex/lexer.d.ts +29 -0
- package/dist/parsers/hdbindex/lexer.js +109 -0
- package/dist/parsers/hdbindex/parser.d.ts +32 -0
- package/dist/parsers/hdbindex/parser.js +129 -0
- package/dist/parsers/hdbindex/visitor.d.ts +31 -0
- package/dist/parsers/hdbindex/visitor.js +58 -0
- package/dist/parsers/hdbschedulerjob/__tests__/extractSchedulerJobAction.test.d.ts +1 -0
- package/dist/parsers/hdbschedulerjob/__tests__/extractSchedulerJobAction.test.js +243 -0
- package/dist/parsers/hdbschedulerjob/index.d.ts +22 -0
- package/dist/parsers/hdbschedulerjob/index.js +43 -0
- package/dist/parsers/hdbschedulerjob/lexer.d.ts +24 -0
- package/dist/parsers/hdbschedulerjob/lexer.js +102 -0
- package/dist/parsers/hdbschedulerjob/parser.d.ts +41 -0
- package/dist/parsers/hdbschedulerjob/parser.js +129 -0
- package/dist/parsers/hdbschedulerjob/visitor.d.ts +35 -0
- package/dist/parsers/hdbschedulerjob/visitor.js +53 -0
- package/dist/parsers/hdbsequence/__tests__/extractSequenceName.test.d.ts +1 -0
- package/dist/parsers/hdbsequence/__tests__/extractSequenceName.test.js +236 -0
- package/dist/parsers/hdbsequence/index.d.ts +27 -0
- package/dist/parsers/hdbsequence/index.js +46 -0
- package/dist/parsers/hdbsequence/lexer.d.ts +60 -0
- package/dist/parsers/hdbsequence/lexer.js +203 -0
- package/dist/parsers/hdbsequence/parser.d.ts +39 -0
- package/dist/parsers/hdbsequence/parser.js +258 -0
- package/dist/parsers/hdbsequence/visitor.d.ts +39 -0
- package/dist/parsers/hdbsequence/visitor.js +70 -0
- package/dist/parsers/hdbtrigger/__tests__/extractTriggerName.test.d.ts +1 -0
- package/dist/parsers/hdbtrigger/__tests__/extractTriggerName.test.js +344 -0
- package/dist/parsers/hdbtrigger/index.d.ts +28 -0
- package/dist/parsers/hdbtrigger/index.js +47 -0
- package/dist/parsers/hdbtrigger/lexer.d.ts +41 -0
- package/dist/parsers/hdbtrigger/lexer.js +145 -0
- package/dist/parsers/hdbtrigger/parser.d.ts +51 -0
- package/dist/parsers/hdbtrigger/parser.js +298 -0
- package/dist/parsers/hdbtrigger/visitor.d.ts +58 -0
- package/dist/parsers/hdbtrigger/visitor.js +90 -0
- package/dist/types/issues.d.ts +1 -1
- package/dist/types/rules.d.ts +1 -1
- package/docs/chevrotain-hdbindex-parser/prd.md +321 -0
- package/docs/chevrotain-hdbindex-parser/spec.md +840 -0
- package/docs/chevrotain-hdbschedulerjob-parser/prd.md +366 -0
- package/docs/chevrotain-hdbschedulerjob-parser/spec.md +830 -0
- package/docs/chevrotain-hdbsequence-parser/prd.md +251 -0
- package/docs/chevrotain-hdbsequence-parser/spec.md +868 -0
- package/docs/chevrotain-hdbtrigger-parser/prd.md +271 -0
- package/docs/chevrotain-hdbtrigger-parser/spec.md +1164 -0
- package/package.json +1 -1
- package/src/assets/.hana-linter.json +48 -0
- package/src/config.test.ts +58 -0
- package/src/config.ts +11 -1
- package/src/content-lint.ts +20 -0
- package/src/parsers/hdbindex/__tests__/extractIndexName.test.ts +248 -0
- package/src/parsers/hdbindex/index.ts +46 -0
- package/src/parsers/hdbindex/lexer.ts +122 -0
- package/src/parsers/hdbindex/parser.ts +162 -0
- package/src/parsers/hdbindex/visitor.ts +66 -0
- package/src/parsers/hdbschedulerjob/__tests__/extractSchedulerJobAction.test.ts +278 -0
- package/src/parsers/hdbschedulerjob/index.ts +46 -0
- package/src/parsers/hdbschedulerjob/lexer.ts +116 -0
- package/src/parsers/hdbschedulerjob/parser.ts +139 -0
- package/src/parsers/hdbschedulerjob/visitor.ts +57 -0
- package/src/parsers/hdbsequence/__tests__/extractSequenceName.test.ts +274 -0
- package/src/parsers/hdbsequence/index.ts +48 -0
- package/src/parsers/hdbsequence/lexer.ts +219 -0
- package/src/parsers/hdbsequence/parser.ts +340 -0
- package/src/parsers/hdbsequence/visitor.ts +79 -0
- package/src/parsers/hdbtrigger/__tests__/extractTriggerName.test.ts +408 -0
- package/src/parsers/hdbtrigger/index.ts +49 -0
- package/src/parsers/hdbtrigger/lexer.ts +159 -0
- package/src/parsers/hdbtrigger/parser.ts +361 -0
- package/src/parsers/hdbtrigger/visitor.ts +100 -0
- package/src/types/issues.ts +11 -1
- package/src/types/rules.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
### 1.4.0
|
|
8
|
+
|
|
9
|
+
- Bumped package version metadata to `1.4.0` (`package.json` and `package-lock.json`)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### 1.3.0
|
|
14
|
+
|
|
15
|
+
- Added Chevrotain-based `.hdbtrigger` parser; content-linting of trigger names is now fully supported
|
|
16
|
+
- The `.hdbtrigger` parser extracts the trigger name from `CREATE TRIGGER … ON …` DDL, handling all HANA timing/event variants (`BEFORE`, `AFTER`, `INSTEAD OF`), `INSERT`, `UPDATE [OF …]`, and `DELETE` events, `REFERENCING` clauses, `FOR EACH ROW|STATEMENT`, `WHEN (…)` clauses, and nested `BEGIN … END` bodies — all without false-positive extractions from body content
|
|
17
|
+
- Trigger names may be schema-qualified (`schema.triggerName`); the parser correctly extracts only the unqualified name
|
|
18
|
+
- Added `triggerName` as a valid `target` in content rule sets (configuration key: `triggerName`)
|
|
19
|
+
- Default config (`.hana-linter.json`) now includes a `triggerName` rule set enforcing uppercase snake case for `.hdbtrigger` files
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
7
23
|
### 1.1.0
|
|
8
24
|
|
|
9
25
|
- Console output is now grouped by file, then by failed rule, reducing noise when multiple violations exist in the same file
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Rule groups per extension:
|
|
|
40
40
|
|
|
41
41
|
You can define `extension: "*"` as a shared rule set. Its rules are applied to every file extension and are merged with any extension-specific rule set.
|
|
42
42
|
|
|
43
|
-
Content-based linting uses purpose-built parsers to reliably extract identifiers from HANA artifact files. SQL DDL artifact types (`.hdbtable`, `.hdbview`, `.hdbprocedure`, `.hdbfunction`, `.hdbtabletype`, `.hdbrole`) use [Chevrotain](https://chevrotain.io)-powered lexers and CST parsers that correctly handle block and line comments, multi-line definitions, quoted identifiers, and HANA-specific DDL constructs. The XML-based `.hdbcalculationview` format uses [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) to navigate the logical model tree — without the false positives and false negatives that ad-hoc regex scanning produces.
|
|
43
|
+
Content-based linting uses purpose-built parsers to reliably extract identifiers from HANA artifact files. SQL DDL artifact types (`.hdbtable`, `.hdbview`, `.hdbprocedure`, `.hdbfunction`, `.hdbtabletype`, `.hdbrole`, `.hdbsequence`, `.hdbindex`, `.hdbtrigger`) use [Chevrotain](https://chevrotain.io)-powered lexers and CST parsers that correctly handle block and line comments, multi-line definitions, quoted identifiers, and HANA-specific DDL constructs. The JSON-like `.hdbschedulerjob` format is also parsed with Chevrotain, tolerating C-style comments (`//` and `/* … */`) and trailing commas that standard `JSON.parse()` rejects. The XML-based `.hdbcalculationview` format uses [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) to navigate the logical model tree — without the false positives and false negatives that ad-hoc regex scanning produces.
|
|
44
44
|
|
|
45
45
|
## Parser Status
|
|
46
46
|
|
|
@@ -56,14 +56,14 @@ Content-based linting uses purpose-built parsers to reliably extract identifiers
|
|
|
56
56
|
| `.hdbrole` | Chevrotain lexer + CST | ✅ Migrated |
|
|
57
57
|
| `.hdbcalculationview` | fast-xml-parser (XML) | ✅ Migrated |
|
|
58
58
|
| `.hdbanalyticalprivilege` | — | ❌ Not yet implemented |
|
|
59
|
-
| `.hdbsequence` |
|
|
59
|
+
| `.hdbsequence` | Chevrotain lexer + CST | ✅ Migrated |
|
|
60
60
|
| `.hdbconstraint` | — | ❌ Not yet implemented |
|
|
61
|
-
| `.hdbschedulerjob` |
|
|
62
|
-
| `.hdbindex` |
|
|
63
|
-
| `.hdbtrigger` |
|
|
61
|
+
| `.hdbschedulerjob` | Chevrotain lexer + CST | ✅ Migrated |
|
|
62
|
+
| `.hdbindex` | Chevrotain lexer + CST | ✅ Migrated |
|
|
63
|
+
| `.hdbtrigger` | Chevrotain lexer + CST | ✅ Migrated |
|
|
64
64
|
|
|
65
|
-
- The regex **file name** validations for all of the above listed extensions **are implemented**.
|
|
66
|
-
- **Not implemented**: `contentRuleSets` targeting
|
|
65
|
+
- The regex **file name** validations for all of the above listed extensions **are implemented**.
|
|
66
|
+
- **Not implemented**: `contentRuleSets` targeting `.hdbanalyticalprivilege` or `.hdbconstraint` will silently return no results — no identifiers are extracted and no content issues are raised.
|
|
67
67
|
|
|
68
68
|
## Install
|
|
69
69
|
|
|
@@ -172,7 +172,7 @@ When `folderName` is omitted, no folder-location enforcement is applied.
|
|
|
172
172
|
Each `contentRuleSets` item contains:
|
|
173
173
|
|
|
174
174
|
- `extension` (string): target extension, for example `.hdbtable`; use `*` to target all extensions
|
|
175
|
-
- `target` (string): extracted identifier type to validate; one of `field`, `inputParameter`, `outputParameter`
|
|
175
|
+
- `target` (string): extracted identifier type to validate; one of `field`, `inputParameter`, `outputParameter`, `roleName`, `grantedRoleName`, `sequenceName`, `jobAction`, `indexName`, `triggerName`
|
|
176
176
|
- `groups.all` (optional array): all rules must match
|
|
177
177
|
- `groups.any` (optional array): at least one rule must match
|
|
178
178
|
|
|
@@ -190,6 +190,10 @@ Supported extractors in this version:
|
|
|
190
190
|
| `grantedRoleName` | `.hdbrole` | Each role listed in `extends roles { ... }` |
|
|
191
191
|
| `field` | `.hdbcalculationview` | Output attributes, calculated attributes, base/calculated/restricted measures |
|
|
192
192
|
| `inputParameter` | `.hdbcalculationview` | Input parameters (`variable[@parameter="true"]`) |
|
|
193
|
+
| `sequenceName` | `.hdbsequence` | The sequence name declared in the file |
|
|
194
|
+
| `jobAction` | `.hdbschedulerjob` | The procedure/function reference in the `action` field |
|
|
195
|
+
| `indexName` | `.hdbindex` | The index name declared in the file |
|
|
196
|
+
| `triggerName` | `.hdbtrigger` | The trigger name declared in the file |
|
|
193
197
|
|
|
194
198
|
### Default Config Example
|
|
195
199
|
|
|
@@ -350,6 +354,54 @@ Supported extractors in this version:
|
|
|
350
354
|
}
|
|
351
355
|
]
|
|
352
356
|
}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"extension": ".hdbsequence",
|
|
360
|
+
"target": "sequenceName",
|
|
361
|
+
"groups": {
|
|
362
|
+
"all": [
|
|
363
|
+
{
|
|
364
|
+
"description": "Sequence names in uppercase snake case",
|
|
365
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"extension": ".hdbschedulerjob",
|
|
372
|
+
"target": "jobAction",
|
|
373
|
+
"groups": {
|
|
374
|
+
"all": [
|
|
375
|
+
{
|
|
376
|
+
"description": "Job action must reference a package-qualified procedure (contains ::)",
|
|
377
|
+
"pattern": "::"
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"extension": ".hdbindex",
|
|
384
|
+
"target": "indexName",
|
|
385
|
+
"groups": {
|
|
386
|
+
"all": [
|
|
387
|
+
{
|
|
388
|
+
"description": "Index names in uppercase snake case",
|
|
389
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
390
|
+
}
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"extension": ".hdbtrigger",
|
|
396
|
+
"target": "triggerName",
|
|
397
|
+
"groups": {
|
|
398
|
+
"all": [
|
|
399
|
+
{
|
|
400
|
+
"description": "Trigger names in uppercase snake case",
|
|
401
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
}
|
|
353
405
|
}
|
|
354
406
|
]
|
|
355
407
|
}
|
|
@@ -291,6 +291,54 @@
|
|
|
291
291
|
}
|
|
292
292
|
]
|
|
293
293
|
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"extension": ".hdbsequence",
|
|
297
|
+
"target": "sequenceName",
|
|
298
|
+
"groups": {
|
|
299
|
+
"all": [
|
|
300
|
+
{
|
|
301
|
+
"description": "Sequence names in uppercase snake case",
|
|
302
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"extension": ".hdbschedulerjob",
|
|
309
|
+
"target": "jobAction",
|
|
310
|
+
"groups": {
|
|
311
|
+
"all": [
|
|
312
|
+
{
|
|
313
|
+
"description": "Job action must reference a package-qualified procedure (contains ::)",
|
|
314
|
+
"pattern": "::"
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"extension": ".hdbindex",
|
|
321
|
+
"target": "indexName",
|
|
322
|
+
"groups": {
|
|
323
|
+
"all": [
|
|
324
|
+
{
|
|
325
|
+
"description": "Index names in uppercase snake case",
|
|
326
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"extension": ".hdbtrigger",
|
|
333
|
+
"target": "triggerName",
|
|
334
|
+
"groups": {
|
|
335
|
+
"all": [
|
|
336
|
+
{
|
|
337
|
+
"description": "Trigger names in uppercase snake case",
|
|
338
|
+
"pattern": "^[A-Z0-9]+(?:_[A-Z0-9]+)*$"
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
}
|
|
294
342
|
}
|
|
295
343
|
]
|
|
296
344
|
}
|
package/dist/config.js
CHANGED
|
@@ -160,7 +160,15 @@ function isJsonLintConfig(value) {
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
function isContentTarget(target) {
|
|
163
|
-
return target === 'field' ||
|
|
163
|
+
return (target === 'field' ||
|
|
164
|
+
target === 'inputParameter' ||
|
|
165
|
+
target === 'outputParameter' ||
|
|
166
|
+
target === 'roleName' ||
|
|
167
|
+
target === 'grantedRoleName' ||
|
|
168
|
+
target === 'sequenceName' ||
|
|
169
|
+
target === 'jobAction' ||
|
|
170
|
+
target === 'indexName' ||
|
|
171
|
+
target === 'triggerName');
|
|
164
172
|
}
|
|
165
173
|
function compileRuleGroup(groups, contextRoot) {
|
|
166
174
|
const allRules = (groups.all ?? []).map((rule, index) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const vitest_1 = require("vitest");
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
const supportedTargets = [
|
|
12
|
+
'field',
|
|
13
|
+
'inputParameter',
|
|
14
|
+
'outputParameter',
|
|
15
|
+
'roleName',
|
|
16
|
+
'grantedRoleName',
|
|
17
|
+
'sequenceName',
|
|
18
|
+
'jobAction',
|
|
19
|
+
'indexName',
|
|
20
|
+
'triggerName'
|
|
21
|
+
];
|
|
22
|
+
(0, vitest_1.describe)('readJsonConfig', () => {
|
|
23
|
+
(0, vitest_1.it)('accepts the bundled default configuration', async () => {
|
|
24
|
+
const configPath = node_path_1.default.resolve(process.cwd(), 'src/assets/.hana-linter.json');
|
|
25
|
+
await (0, vitest_1.expect)((0, config_1.readJsonConfig)(configPath)).resolves.toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
vitest_1.it.each(supportedTargets)('accepts content target "%s"', async (target) => {
|
|
28
|
+
const tempDir = await node_fs_1.promises.mkdtemp(node_path_1.default.join((0, node_os_1.tmpdir)(), 'hana-linter-config-'));
|
|
29
|
+
const configPath = node_path_1.default.join(tempDir, '.hana-linter.json');
|
|
30
|
+
const config = {
|
|
31
|
+
rootDir: 'db',
|
|
32
|
+
ignoredDirectories: ['node_modules'],
|
|
33
|
+
extensionRuleSets: [
|
|
34
|
+
{
|
|
35
|
+
extension: '*',
|
|
36
|
+
groups: {
|
|
37
|
+
all: [{ description: 'Any name', pattern: '.+' }]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
contentRuleSets: [
|
|
42
|
+
{
|
|
43
|
+
extension: '.hdbtable',
|
|
44
|
+
target,
|
|
45
|
+
groups: {
|
|
46
|
+
all: [{ description: 'Any name', pattern: '.+' }]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
await node_fs_1.promises.writeFile(configPath, JSON.stringify(config), 'utf-8');
|
|
52
|
+
await (0, vitest_1.expect)((0, config_1.readJsonConfig)(configPath)).resolves.toEqual(config);
|
|
53
|
+
await node_fs_1.promises.rm(tempDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
});
|
package/dist/content-lint.js
CHANGED
|
@@ -13,6 +13,10 @@ const index_4 = require("./parsers/hdbfunction/index");
|
|
|
13
13
|
const index_5 = require("./parsers/hdbtabletype/index");
|
|
14
14
|
const index_6 = require("./parsers/hdbrole/index");
|
|
15
15
|
const index_7 = require("./parsers/hdbcalculationview/index");
|
|
16
|
+
const index_8 = require("./parsers/hdbsequence/index");
|
|
17
|
+
const index_9 = require("./parsers/hdbschedulerjob/index");
|
|
18
|
+
const index_10 = require("./parsers/hdbindex/index");
|
|
19
|
+
const index_11 = require("./parsers/hdbtrigger/index");
|
|
16
20
|
/**
|
|
17
21
|
* Run content-based naming lint for a file.
|
|
18
22
|
*
|
|
@@ -70,6 +74,18 @@ function extractSubjects(extension, fileContent) {
|
|
|
70
74
|
if (extension === '.hdbcalculationview') {
|
|
71
75
|
return (0, index_7.extractCalculationViewOutputs)(fileContent);
|
|
72
76
|
}
|
|
77
|
+
if (extension === '.hdbsequence') {
|
|
78
|
+
return (0, index_8.extractSequenceName)(fileContent);
|
|
79
|
+
}
|
|
80
|
+
if (extension === '.hdbschedulerjob') {
|
|
81
|
+
return (0, index_9.extractSchedulerJobAction)(fileContent);
|
|
82
|
+
}
|
|
83
|
+
if (extension === '.hdbindex') {
|
|
84
|
+
return (0, index_10.extractIndexName)(fileContent);
|
|
85
|
+
}
|
|
86
|
+
if (extension === '.hdbtrigger') {
|
|
87
|
+
return (0, index_11.extractTriggerName)(fileContent);
|
|
88
|
+
}
|
|
73
89
|
return [];
|
|
74
90
|
}
|
|
75
91
|
function evaluateAllRules(filePath, extension, subject, rules) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function names(ddl) {
|
|
9
|
+
return (0, index_1.extractIndexName)(ddl)
|
|
10
|
+
.filter((s) => s.type === 'indexName')
|
|
11
|
+
.map((s) => s.name);
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// AC-1 Bare INDEX syntax — unquoted name
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
(0, vitest_1.describe)('AC-1: bare INDEX syntax, unquoted name', () => {
|
|
17
|
+
(0, vitest_1.it)('extracts the index name as an indexName subject', () => {
|
|
18
|
+
const ddl = `INDEX MY_INDEX ON MY_TABLE (COL1, COL2);`;
|
|
19
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl)).toEqual([{ type: 'indexName', name: 'MY_INDEX', lineNumber: 1 }]);
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)('handles a file without a trailing semicolon', () => {
|
|
22
|
+
const ddl = `INDEX MY_INDEX ON MY_TABLE (COL1)`;
|
|
23
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)('all results carry type "indexName"', () => {
|
|
26
|
+
const ddl = `INDEX MY_IDX ON T (C1)`;
|
|
27
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl).every((r) => r.type === 'indexName')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// AC-2 Full CREATE INDEX syntax — unquoted name
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
(0, vitest_1.describe)('AC-2: CREATE INDEX syntax, unquoted name', () => {
|
|
34
|
+
(0, vitest_1.it)('extracts the index name from a full CREATE INDEX statement', () => {
|
|
35
|
+
const ddl = `CREATE INDEX MY_INDEX ON MY_TABLE (COL1 ASC, COL2 DESC);`;
|
|
36
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl)).toEqual([{ type: 'indexName', name: 'MY_INDEX', lineNumber: 1 }]);
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.it)('handles multi-line formatting', () => {
|
|
39
|
+
const ddl = `CREATE INDEX\n MY_INDEX\nON MY_TABLE\n (COL1);`;
|
|
40
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// AC-3 CREATE UNIQUE INDEX syntax
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
(0, vitest_1.describe)('AC-3: CREATE UNIQUE INDEX syntax', () => {
|
|
47
|
+
(0, vitest_1.it)('extracts the index name from a CREATE UNIQUE INDEX statement', () => {
|
|
48
|
+
const ddl = `CREATE UNIQUE INDEX MY_UNIQUE_INDEX ON MY_TABLE (COL1);`;
|
|
49
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl)).toEqual([{ type: 'indexName', name: 'MY_UNIQUE_INDEX', lineNumber: 1 }]);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.it)('handles UNIQUE without CREATE prefix', () => {
|
|
52
|
+
const ddl = `UNIQUE INDEX MY_UNIQUE_INDEX ON MY_TABLE (COL1);`;
|
|
53
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_UNIQUE_INDEX']);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// AC-4 Index type keyword variants consumed without error
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
(0, vitest_1.describe)('AC-4: index type keyword variants', () => {
|
|
60
|
+
const cases = [
|
|
61
|
+
['BTREE', `CREATE BTREE INDEX MY_IDX ON MY_TABLE (COL1);`],
|
|
62
|
+
['CPBTREE', `CREATE CPBTREE INDEX MY_IDX ON MY_TABLE (COL1);`],
|
|
63
|
+
['INVERTED HASH', `CREATE INVERTED HASH INDEX MY_IDX ON MY_TABLE (COL1);`],
|
|
64
|
+
['INVERTED VALUE', `CREATE INVERTED VALUE INDEX MY_IDX ON MY_TABLE (COL1);`],
|
|
65
|
+
['INVERTED INDIVIDUAL', `CREATE INVERTED INDIVIDUAL INDEX MY_IDX ON MY_TABLE (COL1);`]
|
|
66
|
+
];
|
|
67
|
+
for (const [label, ddl] of cases) {
|
|
68
|
+
(0, vitest_1.it)(`extracts the index name when type is ${label}`, () => {
|
|
69
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl)).toEqual([{ type: 'indexName', name: 'MY_IDX', lineNumber: 1 }]);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
(0, vitest_1.it)('accepts UNIQUE combined with INVERTED HASH', () => {
|
|
73
|
+
const ddl = `CREATE UNIQUE INVERTED HASH INDEX MY_IDX ON MY_TABLE (COL1);`;
|
|
74
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_IDX']);
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)('accepts UNIQUE combined with INVERTED VALUE', () => {
|
|
77
|
+
const ddl = `CREATE UNIQUE INVERTED VALUE INDEX MY_IDX ON MY_TABLE (COL1);`;
|
|
78
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_IDX']);
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)('accepts UNIQUE combined with INVERTED INDIVIDUAL', () => {
|
|
81
|
+
const ddl = `CREATE UNIQUE INVERTED INDIVIDUAL INDEX MY_IDX ON MY_TABLE (COL1);`;
|
|
82
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_IDX']);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// AC-5 Quoted identifier normalisation
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
(0, vitest_1.describe)('AC-5: quoted identifier normalisation', () => {
|
|
89
|
+
(0, vitest_1.it)('strips double-quotes from the index name', () => {
|
|
90
|
+
const ddl = `CREATE INDEX "MY_INDEX" ON "MY_TABLE" ("COL1", "COL2");`;
|
|
91
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl)).toEqual([{ type: 'indexName', name: 'MY_INDEX', lineNumber: 1 }]);
|
|
92
|
+
});
|
|
93
|
+
(0, vitest_1.it)('handles a mixed-quote file (quoted index name, unquoted table)', () => {
|
|
94
|
+
const ddl = `CREATE INDEX "MY_INDEX" ON MY_TABLE (COL1);`;
|
|
95
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)('handles a fully unquoted file', () => {
|
|
98
|
+
const ddl = `CREATE INDEX MY_INDEX ON MY_TABLE (COL1);`;
|
|
99
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// AC-6 Schema-qualified index name — local name extracted
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
(0, vitest_1.describe)('AC-6: schema-qualified index name', () => {
|
|
106
|
+
(0, vitest_1.it)('extracts only the local name (after the dot)', () => {
|
|
107
|
+
const ddl = `CREATE INDEX "MY_SCHEMA"."MY_INDEX" ON "MY_SCHEMA"."MY_TABLE" ("COL1");`;
|
|
108
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)(ddl)).toEqual([{ type: 'indexName', name: 'MY_INDEX', lineNumber: 1 }]);
|
|
109
|
+
});
|
|
110
|
+
(0, vitest_1.it)('does not include the schema prefix as a subject', () => {
|
|
111
|
+
const ddl = `CREATE INDEX "MY_SCHEMA"."MY_INDEX" ON "MY_TABLE" (COL1);`;
|
|
112
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('MY_SCHEMA');
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)('handles unquoted schema-qualified name', () => {
|
|
115
|
+
const ddl = `CREATE INDEX MY_SCHEMA.MY_INDEX ON MY_TABLE (COL1);`;
|
|
116
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// AC-7 Column identifiers not extracted
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
(0, vitest_1.describe)('AC-7: column identifiers not extracted', () => {
|
|
123
|
+
(0, vitest_1.it)('does not include column names in the result', () => {
|
|
124
|
+
const ddl = `CREATE INDEX MY_INDEX ON MY_TABLE (FIRST_COLUMN ASC, SECOND_COLUMN DESC);`;
|
|
125
|
+
const result = (0, index_1.extractIndexName)(ddl);
|
|
126
|
+
(0, vitest_1.expect)(result).toEqual([{ type: 'indexName', name: 'MY_INDEX', lineNumber: 1 }]);
|
|
127
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('FIRST_COLUMN');
|
|
128
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('SECOND_COLUMN');
|
|
129
|
+
});
|
|
130
|
+
(0, vitest_1.it)('does not include the table name in the result', () => {
|
|
131
|
+
const ddl = `CREATE INDEX MY_INDEX ON MY_TABLE (COL1);`;
|
|
132
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('MY_TABLE');
|
|
133
|
+
});
|
|
134
|
+
(0, vitest_1.it)('handles a multi-column list without extracting any column name', () => {
|
|
135
|
+
const ddl = `INDEX MY_INDEX ON T (A, B, C, D);`;
|
|
136
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// AC-8 Block comment exclusion
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
(0, vitest_1.describe)('AC-8: block comment exclusion', () => {
|
|
143
|
+
(0, vitest_1.it)('does not extract an index name wrapped in a block comment', () => {
|
|
144
|
+
const ddl = `
|
|
145
|
+
/* CREATE INDEX OLD_INDEX ON MY_TABLE (COL1); */
|
|
146
|
+
CREATE INDEX MY_INDEX ON MY_TABLE (COL1);
|
|
147
|
+
`;
|
|
148
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('OLD_INDEX');
|
|
149
|
+
(0, vitest_1.expect)(names(ddl)).toContain('MY_INDEX');
|
|
150
|
+
});
|
|
151
|
+
(0, vitest_1.it)('handles a block comment spanning multiple lines before the real statement', () => {
|
|
152
|
+
const ddl = `
|
|
153
|
+
/*
|
|
154
|
+
CREATE INDEX COMMENTED_INDEX ON MY_TABLE (COL1);
|
|
155
|
+
*/
|
|
156
|
+
INDEX MY_INDEX ON MY_TABLE (COL1);
|
|
157
|
+
`;
|
|
158
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('COMMENTED_INDEX');
|
|
159
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// AC-9 Line comment exclusion
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
(0, vitest_1.describe)('AC-9: line comment exclusion', () => {
|
|
166
|
+
(0, vitest_1.it)('does not extract an index name on a -- comment line', () => {
|
|
167
|
+
const ddl = `
|
|
168
|
+
-- CREATE INDEX OLD_INDEX ON MY_TABLE (COL1);
|
|
169
|
+
CREATE INDEX MY_INDEX ON MY_TABLE (COL1);
|
|
170
|
+
`;
|
|
171
|
+
(0, vitest_1.expect)(names(ddl)).not.toContain('OLD_INDEX');
|
|
172
|
+
(0, vitest_1.expect)(names(ddl)).toContain('MY_INDEX');
|
|
173
|
+
});
|
|
174
|
+
(0, vitest_1.it)('ignores a -- comment after the statement', () => {
|
|
175
|
+
const ddl = `CREATE INDEX MY_INDEX ON MY_TABLE (COL1); -- production index`;
|
|
176
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// AC-10 Optional semicolon
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
(0, vitest_1.describe)('AC-10: optional semicolon', () => {
|
|
183
|
+
(0, vitest_1.it)('produces the same result with and without a trailing semicolon', () => {
|
|
184
|
+
const withSemicolon = `CREATE INDEX MY_INDEX ON MY_TABLE (COL1);`;
|
|
185
|
+
const withoutSemicolon = `CREATE INDEX MY_INDEX ON MY_TABLE (COL1)`;
|
|
186
|
+
(0, vitest_1.expect)(names(withSemicolon)).toEqual(names(withoutSemicolon));
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// AC-11 Graceful error on unparseable file
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
(0, vitest_1.describe)('AC-11: graceful error handling', () => {
|
|
193
|
+
(0, vitest_1.it)('does not throw on completely invalid input', () => {
|
|
194
|
+
(0, vitest_1.expect)(() => (0, index_1.extractIndexName)('THIS IS NOT VALID DDL !!!')).not.toThrow();
|
|
195
|
+
});
|
|
196
|
+
(0, vitest_1.it)('does not throw on an empty string', () => {
|
|
197
|
+
(0, vitest_1.expect)(() => (0, index_1.extractIndexName)('')).not.toThrow();
|
|
198
|
+
(0, vitest_1.expect)((0, index_1.extractIndexName)('')).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
(0, vitest_1.it)('does not throw when INDEX keyword is missing', () => {
|
|
201
|
+
(0, vitest_1.expect)(() => (0, index_1.extractIndexName)('CREATE MY_INDEX ON MY_TABLE (COL1)')).not.toThrow();
|
|
202
|
+
});
|
|
203
|
+
(0, vitest_1.it)('handles CRLF line endings', () => {
|
|
204
|
+
const ddl = 'CREATE INDEX MY_INDEX ON MY_TABLE (COL1);\r\n';
|
|
205
|
+
(0, vitest_1.expect)(names(ddl)).toEqual(['MY_INDEX']);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtractedSubject } from '../../types/issues';
|
|
2
|
+
/**
|
|
3
|
+
* Extract the index name from the content of an `.hdbindex` DDL file.
|
|
4
|
+
*
|
|
5
|
+
* Uses a Chevrotain lexer and CstParser to correctly handle all SAP HANA
|
|
6
|
+
* index DDL variants:
|
|
7
|
+
* - bare `INDEX <name> ON <table> (<columns>)`
|
|
8
|
+
* - `CREATE INDEX <name> ON <table> (<columns>)`
|
|
9
|
+
* - `CREATE UNIQUE INDEX <name> ON <table> (<columns>)`
|
|
10
|
+
* - `CREATE [UNIQUE] BTREE|CPBTREE|INVERTED HASH|INVERTED VALUE|INVERTED INDIVIDUAL INDEX ...`
|
|
11
|
+
*
|
|
12
|
+
* The parser handles block/line comments, quoted and unquoted index names,
|
|
13
|
+
* schema-qualified index names (schema prefix excluded from result),
|
|
14
|
+
* and optional trailing semicolons.
|
|
15
|
+
*
|
|
16
|
+
* Column names and sort-order keywords (ASC/DESC) inside the column list
|
|
17
|
+
* are consumed structurally and never extracted.
|
|
18
|
+
*
|
|
19
|
+
* Gracefully returns a partial or empty result on invalid input — does
|
|
20
|
+
* not throw.
|
|
21
|
+
*
|
|
22
|
+
* @param fileContent - Raw UTF-8 file content (LF or CRLF).
|
|
23
|
+
* @returns Array containing at most one ExtractedSubject with type 'indexName'.
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractIndexName(fileContent: string): ExtractedSubject[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractIndexName = extractIndexName;
|
|
4
|
+
const lexer_1 = require("./lexer");
|
|
5
|
+
const parser_1 = require("./parser");
|
|
6
|
+
const visitor_1 = require("./visitor");
|
|
7
|
+
/**
|
|
8
|
+
* Extract the index name from the content of an `.hdbindex` DDL file.
|
|
9
|
+
*
|
|
10
|
+
* Uses a Chevrotain lexer and CstParser to correctly handle all SAP HANA
|
|
11
|
+
* index DDL variants:
|
|
12
|
+
* - bare `INDEX <name> ON <table> (<columns>)`
|
|
13
|
+
* - `CREATE INDEX <name> ON <table> (<columns>)`
|
|
14
|
+
* - `CREATE UNIQUE INDEX <name> ON <table> (<columns>)`
|
|
15
|
+
* - `CREATE [UNIQUE] BTREE|CPBTREE|INVERTED HASH|INVERTED VALUE|INVERTED INDIVIDUAL INDEX ...`
|
|
16
|
+
*
|
|
17
|
+
* The parser handles block/line comments, quoted and unquoted index names,
|
|
18
|
+
* schema-qualified index names (schema prefix excluded from result),
|
|
19
|
+
* and optional trailing semicolons.
|
|
20
|
+
*
|
|
21
|
+
* Column names and sort-order keywords (ASC/DESC) inside the column list
|
|
22
|
+
* are consumed structurally and never extracted.
|
|
23
|
+
*
|
|
24
|
+
* Gracefully returns a partial or empty result on invalid input — does
|
|
25
|
+
* not throw.
|
|
26
|
+
*
|
|
27
|
+
* @param fileContent - Raw UTF-8 file content (LF or CRLF).
|
|
28
|
+
* @returns Array containing at most one ExtractedSubject with type 'indexName'.
|
|
29
|
+
*/
|
|
30
|
+
function extractIndexName(fileContent) {
|
|
31
|
+
const lexResult = lexer_1.HdbIndexLexer.tokenize(fileContent);
|
|
32
|
+
// Feed the token stream to the singleton parser.
|
|
33
|
+
parser_1.hdbIndexParser.input = lexResult.tokens;
|
|
34
|
+
const cst = parser_1.hdbIndexParser.indexStatement();
|
|
35
|
+
// Lex/parse errors are intentionally not re-thrown — the CST visitor
|
|
36
|
+
// will extract whatever could be parsed from the partial tree.
|
|
37
|
+
// Guard against a completely unrecoverable parse (cst may be undefined).
|
|
38
|
+
if (!cst) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const visitor = new visitor_1.HdbIndexVisitor();
|
|
42
|
+
visitor.visit(cst);
|
|
43
|
+
return visitor.subjects;
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Lexer, type TokenType } from 'chevrotain';
|
|
2
|
+
export declare const BlockComment: TokenType;
|
|
3
|
+
export declare const LineComment: TokenType;
|
|
4
|
+
export declare const WhiteSpace: TokenType;
|
|
5
|
+
export declare const Identifier: TokenType;
|
|
6
|
+
export declare const QuotedIdentifier: TokenType;
|
|
7
|
+
export declare const CreateKw: TokenType;
|
|
8
|
+
export declare const UniqueKw: TokenType;
|
|
9
|
+
export declare const IndexKw: TokenType;
|
|
10
|
+
export declare const OnKw: TokenType;
|
|
11
|
+
export declare const AscKw: TokenType;
|
|
12
|
+
export declare const DescKw: TokenType;
|
|
13
|
+
export declare const BtreeKw: TokenType;
|
|
14
|
+
export declare const CpbtreeKw: TokenType;
|
|
15
|
+
export declare const InvertedKw: TokenType;
|
|
16
|
+
export declare const HashKw: TokenType;
|
|
17
|
+
export declare const ValueKw: TokenType;
|
|
18
|
+
export declare const IndividualKw: TokenType;
|
|
19
|
+
export declare const LParen: TokenType;
|
|
20
|
+
export declare const RParen: TokenType;
|
|
21
|
+
export declare const Comma: TokenType;
|
|
22
|
+
export declare const Semicolon: TokenType;
|
|
23
|
+
export declare const Dot: TokenType;
|
|
24
|
+
export declare const allTokens: TokenType[];
|
|
25
|
+
/**
|
|
26
|
+
* Singleton Chevrotain Lexer for `.hdbindex` DDL files.
|
|
27
|
+
* Instantiated once at module load time per Chevrotain best practices.
|
|
28
|
+
*/
|
|
29
|
+
export declare const HdbIndexLexer: Lexer;
|