vouchington-tooling 0.0.2 → 0.0.4
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/README.md +4 -0
- package/dist/cli/commands/gha-runtime-audit.d.mts +3 -0
- package/dist/cli/commands/gha-runtime-audit.mjs +23 -0
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.mjs +15 -2
- package/dist/cli/parse-gha-runtime-audit.d.mts +13 -0
- package/dist/cli/parse-gha-runtime-audit.mjs +44 -0
- package/dist/cli/parse.d.mts +2 -1
- package/dist/cli/parse.mjs +3 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +7 -0
- package/dist/gha-runtime-audit/audit.d.mts +3 -0
- package/dist/gha-runtime-audit/audit.mjs +106 -0
- package/dist/gha-runtime-audit/index.d.mts +5 -0
- package/dist/gha-runtime-audit/index.mjs +3 -0
- package/dist/gha-runtime-audit/index.test-helpers.d.mts +14 -0
- package/dist/gha-runtime-audit/index.test-helpers.mjs +63 -0
- package/dist/gha-runtime-audit/model.d.mts +60 -0
- package/dist/gha-runtime-audit/model.mjs +83 -0
- package/dist/gha-runtime-audit/results.d.mts +15 -0
- package/dist/gha-runtime-audit/results.mjs +48 -0
- package/dist/gha-runtime-audit/scope.d.mts +29 -0
- package/dist/gha-runtime-audit/scope.mjs +39 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +3 -1
- package/dist/sql-ast/add-column.d.mts +1 -0
- package/dist/sql-ast/add-column.mjs +22 -0
- package/dist/sql-ast/constraint-do-block.d.mts +11 -0
- package/dist/sql-ast/constraint-do-block.mjs +83 -0
- package/dist/sql-ast/constraint-shared.d.mts +19 -0
- package/dist/sql-ast/constraint-shared.mjs +20 -0
- package/dist/sql-ast/constraint.d.mts +2 -0
- package/dist/sql-ast/constraint.mjs +63 -0
- package/dist/sql-ast/create-table.d.mts +25 -0
- package/dist/sql-ast/create-table.mjs +79 -0
- package/dist/sql-ast/default-function.d.mts +18 -0
- package/dist/sql-ast/default-function.mjs +53 -0
- package/dist/sql-ast/drop-index.d.mts +5 -0
- package/dist/sql-ast/drop-index.mjs +31 -0
- package/dist/sql-ast/implicit-indexes.d.mts +3 -0
- package/dist/sql-ast/implicit-indexes.mjs +66 -0
- package/dist/sql-ast/index-metadata.d.mts +19 -0
- package/dist/sql-ast/index-metadata.mjs +100 -0
- package/dist/sql-ast/index.d.mts +12 -12
- package/dist/sql-ast/index.mjs +8 -79
- package/dist/sql-ast/line-of-offset.d.mts +1 -0
- package/dist/sql-ast/line-of-offset.mjs +12 -0
- package/dist/sql-ast/parser.d.mts +11 -0
- package/dist/sql-ast/parser.mjs +46 -0
- package/dist/sql-ast/unknown-record.d.mts +1 -0
- package/dist/sql-ast/unknown-record.mjs +3 -0
- package/dist/sql-scanner/index.d.mts +7 -0
- package/dist/sql-scanner/index.mjs +172 -0
- package/dist/sql-scanner/literals.d.mts +17 -0
- package/dist/sql-scanner/literals.mjs +158 -0
- package/package.json +12 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { collectCreateStmtImplicitIndexes } from './implicit-indexes.mjs';
|
|
2
|
+
import { parseSql } from './parser.mjs';
|
|
3
|
+
import { isRecord } from './unknown-record.mjs';
|
|
4
|
+
function indexElemName(param) {
|
|
5
|
+
if (!isRecord(param) || !isRecord(param.IndexElem))
|
|
6
|
+
return null;
|
|
7
|
+
const name = param.IndexElem.name;
|
|
8
|
+
return typeof name === 'string' ? name : null;
|
|
9
|
+
}
|
|
10
|
+
function indexElemOpclass(param) {
|
|
11
|
+
if (!isRecord(param) || !isRecord(param.IndexElem))
|
|
12
|
+
return null;
|
|
13
|
+
const opclass = param.IndexElem.opclass;
|
|
14
|
+
if (!Array.isArray(opclass))
|
|
15
|
+
return null;
|
|
16
|
+
const names = opclass.flatMap((node) => {
|
|
17
|
+
const sval = isRecord(node) && isRecord(node.String) ? node.String.sval : undefined;
|
|
18
|
+
/* v8 ignore next */
|
|
19
|
+
return typeof sval === 'string' ? [sval] : [];
|
|
20
|
+
});
|
|
21
|
+
return names.length > 0 ? names.join('.') : null;
|
|
22
|
+
}
|
|
23
|
+
/** Sort direction and NULLS placement together — PostgreSQL resolves both from the same `IndexElem`. */
|
|
24
|
+
function indexElemSortSpec(param) {
|
|
25
|
+
if (!isRecord(param) || !isRecord(param.IndexElem))
|
|
26
|
+
return { ordering: null, nullsOrdering: null };
|
|
27
|
+
const { ordering, nulls_ordering: nullsOrdering } = param.IndexElem;
|
|
28
|
+
return {
|
|
29
|
+
ordering: typeof ordering === 'string' ? ordering : null,
|
|
30
|
+
nullsOrdering: typeof nullsOrdering === 'string' ? nullsOrdering : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function indexParamDetail(param) {
|
|
34
|
+
return {
|
|
35
|
+
name: indexElemName(param),
|
|
36
|
+
opclass: indexElemOpclass(param),
|
|
37
|
+
...indexElemSortSpec(param),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Deep-clones a parser AST node with `location` fields stripped, so two
|
|
42
|
+
* predicates that are byte-identical apart from source position compare
|
|
43
|
+
* equal via JSON.stringify.
|
|
44
|
+
*/
|
|
45
|
+
function stripLocations(value) {
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return value.map(stripLocations);
|
|
48
|
+
if (!isRecord(value))
|
|
49
|
+
return value;
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const [key, child] of Object.entries(value)) {
|
|
52
|
+
if (key === 'location')
|
|
53
|
+
continue;
|
|
54
|
+
result[key] = stripLocations(child);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function whereClauseKey(whereClause) {
|
|
59
|
+
if (whereClause === undefined || whereClause === null)
|
|
60
|
+
return null;
|
|
61
|
+
return JSON.stringify(stripLocations(whereClause));
|
|
62
|
+
}
|
|
63
|
+
export function extractCreateIndexMetadata(content) {
|
|
64
|
+
const indexes = [];
|
|
65
|
+
const parseResult = parseSql(content);
|
|
66
|
+
for (const rawStmt of parseResult.stmts ?? []) {
|
|
67
|
+
const node = rawStmt.stmt;
|
|
68
|
+
/* v8 ignore next */
|
|
69
|
+
if (!node)
|
|
70
|
+
continue;
|
|
71
|
+
if ('CreateStmt' in node) {
|
|
72
|
+
collectCreateStmtImplicitIndexes(node.CreateStmt, rawStmt.stmt_location ?? 0, indexes);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
/* v8 ignore next */
|
|
76
|
+
if (!('IndexStmt' in node))
|
|
77
|
+
continue;
|
|
78
|
+
const indexStmt = node.IndexStmt;
|
|
79
|
+
const relname = indexStmt.relation?.relname;
|
|
80
|
+
/* v8 ignore next */
|
|
81
|
+
if (!relname)
|
|
82
|
+
continue;
|
|
83
|
+
const indexParams = indexStmt.indexParams ?? [];
|
|
84
|
+
indexes.push({
|
|
85
|
+
relname,
|
|
86
|
+
idxname: indexStmt.idxname ?? null,
|
|
87
|
+
unique: indexStmt.unique === true,
|
|
88
|
+
indexParams: indexParams.map(indexElemName),
|
|
89
|
+
indexParamDetails: indexParams.map(indexParamDetail),
|
|
90
|
+
includeParams: (indexStmt.indexIncludingParams ?? [])
|
|
91
|
+
.map(indexElemName)
|
|
92
|
+
.filter((name) => name !== null),
|
|
93
|
+
whereClause: indexStmt.whereClause ?? null,
|
|
94
|
+
whereClauseKey: whereClauseKey(indexStmt.whereClause),
|
|
95
|
+
accessMethod: indexStmt.accessMethod ?? 'btree',
|
|
96
|
+
location: rawStmt.stmt_location ?? 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return indexes;
|
|
100
|
+
}
|
package/dist/sql-ast/index.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export {};
|
|
1
|
+
export { extractAlterTableAddColumnLocations } from './add-column.mts';
|
|
2
|
+
export { extractMigrationConstraintMetadata } from './constraint.mts';
|
|
3
|
+
export type { ForeignKey, SqlMigrationConstraintMetadata } from './constraint-shared.mts';
|
|
4
|
+
export { extractCreateTableMetadata } from './create-table.mts';
|
|
5
|
+
export type { SqlCreateTableColumn, SqlCreateTableColumnConstraint, SqlCreateTableMetadata, } from './create-table.mts';
|
|
6
|
+
export { extractDefaultFunction, extractFuncCallArgColumnNames } from './default-function.mts';
|
|
7
|
+
export { extractDropIndexMetadata } from './drop-index.mts';
|
|
8
|
+
export type { SqlDropIndexMetadata } from './drop-index.mts';
|
|
9
|
+
export { extractCreateIndexMetadata } from './index-metadata.mts';
|
|
10
|
+
export type { SqlCreateIndexMetadata, SqlIndexParam } from './index-metadata.mts';
|
|
11
|
+
export { lineOfUtf8ByteOffset } from './line-of-offset.mts';
|
|
12
|
+
export { initSqlAst, MissingSqlAstParserError, parseSql } from './parser.mts';
|
package/dist/sql-ast/index.mjs
CHANGED
|
@@ -1,79 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
export function extractAlterTableAddColumnLocations(content) {
|
|
10
|
-
const locations = [];
|
|
11
|
-
const parseResult = requireParser().parseSync(content);
|
|
12
|
-
for (const rawStmt of parseResult.stmts ?? []) {
|
|
13
|
-
const node = rawStmt.stmt;
|
|
14
|
-
if (!node || !('AlterTableStmt' in node))
|
|
15
|
-
continue;
|
|
16
|
-
for (const rawCommand of node.AlterTableStmt.cmds ?? []) {
|
|
17
|
-
if (!rawCommand || !('AlterTableCmd' in rawCommand))
|
|
18
|
-
continue;
|
|
19
|
-
const command = rawCommand.AlterTableCmd;
|
|
20
|
-
if (command.subtype !== 'AT_AddColumn')
|
|
21
|
-
continue;
|
|
22
|
-
const definition = command.def;
|
|
23
|
-
if (!definition || !('ColumnDef' in definition))
|
|
24
|
-
continue;
|
|
25
|
-
locations.push(rawStmt.stmt_location ?? 0);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return locations;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Ensures the @libpg-query/parser WASM module is loaded.
|
|
32
|
-
* Must be awaited once before any synchronous parseSync() calls.
|
|
33
|
-
*/
|
|
34
|
-
export function initSqlAst(importer = () => import('@libpg-query/parser')) {
|
|
35
|
-
return (moduleLoad ??= loadParser(importer)
|
|
36
|
-
.then(async (loaded) => {
|
|
37
|
-
await loaded.loadModule();
|
|
38
|
-
parser = loaded;
|
|
39
|
-
})
|
|
40
|
-
.catch((error) => {
|
|
41
|
-
moduleLoad = undefined;
|
|
42
|
-
parser = undefined;
|
|
43
|
-
throw error;
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
export function lineOfUtf8ByteOffset(content, byteOffset) {
|
|
47
|
-
const buffer = typeof content === 'string' ? Buffer.from(content, 'utf8') : content;
|
|
48
|
-
if (byteOffset > buffer.length) {
|
|
49
|
-
throw new RangeError(`byteOffset ${byteOffset} is out of range for buffer length ${buffer.length}`);
|
|
50
|
-
}
|
|
51
|
-
let line = 1;
|
|
52
|
-
for (let i = 0; i < byteOffset; i++) {
|
|
53
|
-
if (buffer[i] === 10)
|
|
54
|
-
line++;
|
|
55
|
-
}
|
|
56
|
-
return line;
|
|
57
|
-
}
|
|
58
|
-
function requireParser() {
|
|
59
|
-
if (!parser) {
|
|
60
|
-
throw new Error('initSqlAst() must be awaited before calling parse helpers');
|
|
61
|
-
}
|
|
62
|
-
return parser;
|
|
63
|
-
}
|
|
64
|
-
async function loadParser(importer) {
|
|
65
|
-
try {
|
|
66
|
-
return await importer();
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
if (isModuleNotFound(error))
|
|
70
|
-
throw new MissingSqlAstParserError();
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function isModuleNotFound(error) {
|
|
75
|
-
return (typeof error === 'object' &&
|
|
76
|
-
error !== null &&
|
|
77
|
-
'code' in error &&
|
|
78
|
-
error.code === 'ERR_MODULE_NOT_FOUND');
|
|
79
|
-
}
|
|
1
|
+
export { extractAlterTableAddColumnLocations } from './add-column.mjs';
|
|
2
|
+
export { extractMigrationConstraintMetadata } from './constraint.mjs';
|
|
3
|
+
export { extractCreateTableMetadata } from './create-table.mjs';
|
|
4
|
+
export { extractDefaultFunction, extractFuncCallArgColumnNames } from './default-function.mjs';
|
|
5
|
+
export { extractDropIndexMetadata } from './drop-index.mjs';
|
|
6
|
+
export { extractCreateIndexMetadata } from './index-metadata.mjs';
|
|
7
|
+
export { lineOfUtf8ByteOffset } from './line-of-offset.mjs';
|
|
8
|
+
export { initSqlAst, MissingSqlAstParserError, parseSql } from './parser.mjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function lineOfUtf8ByteOffset(content: string | Buffer, byteOffset: number): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function lineOfUtf8ByteOffset(content, byteOffset) {
|
|
2
|
+
const buffer = typeof content === 'string' ? Buffer.from(content, 'utf8') : content;
|
|
3
|
+
if (byteOffset > buffer.length) {
|
|
4
|
+
throw new RangeError(`byteOffset ${byteOffset} is out of range for buffer length ${buffer.length}`);
|
|
5
|
+
}
|
|
6
|
+
let line = 1;
|
|
7
|
+
for (let i = 0; i < byteOffset; i++) {
|
|
8
|
+
if (buffer[i] === 10)
|
|
9
|
+
line++;
|
|
10
|
+
}
|
|
11
|
+
return line;
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type LibPgQuery = typeof import('@libpg-query/parser');
|
|
2
|
+
export declare class MissingSqlAstParserError extends Error {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Ensures the @libpg-query/parser WASM module is loaded.
|
|
7
|
+
* Must be awaited once before any synchronous parseSql() calls.
|
|
8
|
+
*/
|
|
9
|
+
export declare function initSqlAst(importer?: () => Promise<LibPgQuery>): Promise<void>;
|
|
10
|
+
export declare function parseSql(content: string): ReturnType<LibPgQuery['parseSync']>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
let parser;
|
|
2
|
+
let moduleLoad;
|
|
3
|
+
export class MissingSqlAstParserError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('vouchington-tooling/sql-ast requires the optional dependency @libpg-query/parser. Install it in the consuming package.');
|
|
6
|
+
this.name = 'MissingSqlAstParserError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Ensures the @libpg-query/parser WASM module is loaded.
|
|
11
|
+
* Must be awaited once before any synchronous parseSql() calls.
|
|
12
|
+
*/
|
|
13
|
+
export function initSqlAst(importer = () => import('@libpg-query/parser')) {
|
|
14
|
+
return (moduleLoad ??= loadParser(importer)
|
|
15
|
+
.then(async (loaded) => {
|
|
16
|
+
await loaded.loadModule();
|
|
17
|
+
parser = loaded;
|
|
18
|
+
})
|
|
19
|
+
.catch((error) => {
|
|
20
|
+
moduleLoad = undefined;
|
|
21
|
+
parser = undefined;
|
|
22
|
+
throw error;
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
export function parseSql(content) {
|
|
26
|
+
if (!parser) {
|
|
27
|
+
throw new Error('initSqlAst() must be awaited before calling parse helpers');
|
|
28
|
+
}
|
|
29
|
+
return parser.parseSync(content);
|
|
30
|
+
}
|
|
31
|
+
async function loadParser(importer) {
|
|
32
|
+
try {
|
|
33
|
+
return await importer();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (isModuleNotFound(error))
|
|
37
|
+
throw new MissingSqlAstParserError();
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isModuleNotFound(error) {
|
|
42
|
+
return (typeof error === 'object' &&
|
|
43
|
+
error !== null &&
|
|
44
|
+
'code' in error &&
|
|
45
|
+
error.code === 'ERR_MODULE_NOT_FOUND');
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { dollarQuoteEnd, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, sqlFragments, } from './literals.mts';
|
|
2
|
+
export declare function lineOf(content: string, index: number): number;
|
|
3
|
+
export declare function stripSqlComments(content: string): string;
|
|
4
|
+
export declare function splitSqlStatements(content: string): {
|
|
5
|
+
text: string;
|
|
6
|
+
index: number;
|
|
7
|
+
}[];
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readDollarQuoteDelimiter, stringLiteralQuoteStart } from './literals.mjs';
|
|
2
|
+
export { dollarQuoteEnd, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, sqlFragments, } from './literals.mjs';
|
|
3
|
+
function maskNonNewlines(value) {
|
|
4
|
+
return value.replace(/[^\n]/g, ' ');
|
|
5
|
+
}
|
|
6
|
+
export function lineOf(content, index) {
|
|
7
|
+
return content.slice(0, index).split('\n').length;
|
|
8
|
+
}
|
|
9
|
+
function lineCommentEnd(content, start) {
|
|
10
|
+
let end = start;
|
|
11
|
+
while (end < content.length && content[end] !== '\n')
|
|
12
|
+
end++;
|
|
13
|
+
return end;
|
|
14
|
+
}
|
|
15
|
+
function blockCommentEnd(content, start) {
|
|
16
|
+
let depth = 1;
|
|
17
|
+
let end = start + 2;
|
|
18
|
+
while (end < content.length) {
|
|
19
|
+
if (content[end] === '/' && content[end + 1] === '*') {
|
|
20
|
+
depth++;
|
|
21
|
+
end += 2;
|
|
22
|
+
}
|
|
23
|
+
else if (content[end] === '*' && content[end + 1] === '/') {
|
|
24
|
+
depth--;
|
|
25
|
+
if (depth === 0)
|
|
26
|
+
return end + 2;
|
|
27
|
+
end += 2;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
end++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return content.length;
|
|
34
|
+
}
|
|
35
|
+
export function stripSqlComments(content) {
|
|
36
|
+
let stripped = '';
|
|
37
|
+
let inSingleQuote = false;
|
|
38
|
+
let inEscapeString = false;
|
|
39
|
+
let dollarQuoteDelimiter = null;
|
|
40
|
+
for (let i = 0; i < content.length; i++) {
|
|
41
|
+
if (dollarQuoteDelimiter) {
|
|
42
|
+
if (content.startsWith(dollarQuoteDelimiter, i)) {
|
|
43
|
+
stripped += dollarQuoteDelimiter;
|
|
44
|
+
i += dollarQuoteDelimiter.length - 1;
|
|
45
|
+
dollarQuoteDelimiter = null;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
stripped += content[i];
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (inSingleQuote) {
|
|
53
|
+
stripped += content[i];
|
|
54
|
+
if (inEscapeString && content[i] === '\\' && i + 1 < content.length) {
|
|
55
|
+
stripped += content[i + 1];
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (content[i] === "'") {
|
|
60
|
+
if (content[i + 1] === "'") {
|
|
61
|
+
stripped += content[i + 1];
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
inSingleQuote = false;
|
|
66
|
+
inEscapeString = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const dollarQuote = readDollarQuoteDelimiter(content, i);
|
|
72
|
+
if (dollarQuote) {
|
|
73
|
+
dollarQuoteDelimiter = dollarQuote;
|
|
74
|
+
stripped += dollarQuote;
|
|
75
|
+
i += dollarQuote.length - 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const stringStart = stringLiteralQuoteStart(content, i);
|
|
79
|
+
if (stringStart) {
|
|
80
|
+
inSingleQuote = true;
|
|
81
|
+
inEscapeString = stringStart.escapeString;
|
|
82
|
+
stripped += content.slice(i, stringStart.quoteStart + 1);
|
|
83
|
+
i = stringStart.quoteStart;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (content[i] === '-' && content[i + 1] === '-') {
|
|
87
|
+
const end = lineCommentEnd(content, i);
|
|
88
|
+
if (end === content.length) {
|
|
89
|
+
stripped += ' '.repeat(content.length - i);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
stripped += `${' '.repeat(end - i)}\n`;
|
|
93
|
+
i = end;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (content[i] === '/' && content[i + 1] === '*') {
|
|
97
|
+
const end = blockCommentEnd(content, i);
|
|
98
|
+
stripped += maskNonNewlines(content.slice(i, end));
|
|
99
|
+
i = end - 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
stripped += content[i];
|
|
103
|
+
}
|
|
104
|
+
return stripped;
|
|
105
|
+
}
|
|
106
|
+
export function splitSqlStatements(content) {
|
|
107
|
+
const statements = [];
|
|
108
|
+
let currentStmt = '';
|
|
109
|
+
let stmtStart = 0;
|
|
110
|
+
let inSingleQuote = false;
|
|
111
|
+
let inEscapeString = false;
|
|
112
|
+
let dollarQuoteDelimiter = null;
|
|
113
|
+
for (let i = 0; i < content.length; i++) {
|
|
114
|
+
const ch = content[i];
|
|
115
|
+
if (dollarQuoteDelimiter) {
|
|
116
|
+
if (content.startsWith(dollarQuoteDelimiter, i)) {
|
|
117
|
+
currentStmt += dollarQuoteDelimiter;
|
|
118
|
+
i += dollarQuoteDelimiter.length - 1;
|
|
119
|
+
dollarQuoteDelimiter = null;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
currentStmt += ch;
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (inSingleQuote) {
|
|
127
|
+
currentStmt += ch;
|
|
128
|
+
if (inEscapeString && ch === '\\' && i + 1 < content.length) {
|
|
129
|
+
currentStmt += content[i + 1];
|
|
130
|
+
i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (ch === "'") {
|
|
134
|
+
if (content[i + 1] === "'") {
|
|
135
|
+
currentStmt += content[i + 1];
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
inSingleQuote = false;
|
|
140
|
+
inEscapeString = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const dollarQuote = readDollarQuoteDelimiter(content, i);
|
|
146
|
+
if (dollarQuote) {
|
|
147
|
+
dollarQuoteDelimiter = dollarQuote;
|
|
148
|
+
currentStmt += dollarQuote;
|
|
149
|
+
i += dollarQuote.length - 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const stringStart = stringLiteralQuoteStart(content, i);
|
|
153
|
+
if (stringStart) {
|
|
154
|
+
inSingleQuote = true;
|
|
155
|
+
inEscapeString = stringStart.escapeString;
|
|
156
|
+
currentStmt += content.slice(i, stringStart.quoteStart + 1);
|
|
157
|
+
i = stringStart.quoteStart;
|
|
158
|
+
}
|
|
159
|
+
else if (ch === ';') {
|
|
160
|
+
statements.push({ text: currentStmt, index: stmtStart });
|
|
161
|
+
currentStmt = '';
|
|
162
|
+
stmtStart = i + 1;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
currentStmt += ch;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (currentStmt.trim()) {
|
|
169
|
+
statements.push({ text: currentStmt, index: stmtStart });
|
|
170
|
+
}
|
|
171
|
+
return statements;
|
|
172
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SqlStringLiteralStart = {
|
|
2
|
+
quoteStart: number;
|
|
3
|
+
escapeString: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function readDollarQuoteDelimiter(content: string, index: number): string | null;
|
|
6
|
+
export declare function stringLiteralQuoteStart(content: string, index: number): SqlStringLiteralStart | null;
|
|
7
|
+
export declare function maskSqlQuotedText(content: string): string;
|
|
8
|
+
export declare function readStringLiteral(content: string, index: number): {
|
|
9
|
+
text: string;
|
|
10
|
+
index: number;
|
|
11
|
+
end: number;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function dollarQuoteEnd(content: string, start: number, delimiter: string): number;
|
|
14
|
+
export declare function sqlFragments(content: string): {
|
|
15
|
+
text: string;
|
|
16
|
+
index: number;
|
|
17
|
+
}[];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export function readDollarQuoteDelimiter(content, index) {
|
|
2
|
+
if (content[index] !== '$')
|
|
3
|
+
return null;
|
|
4
|
+
/* v8 ignore next */
|
|
5
|
+
if (/[A-Za-z0-9_$]/.test(content[index - 1] ?? ''))
|
|
6
|
+
return null;
|
|
7
|
+
const dollarQuoteRe = /\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/y;
|
|
8
|
+
dollarQuoteRe.lastIndex = index;
|
|
9
|
+
const match = dollarQuoteRe.exec(content);
|
|
10
|
+
/* v8 ignore next */
|
|
11
|
+
return match?.[0] ?? null;
|
|
12
|
+
}
|
|
13
|
+
function isEscapeStringStart(content, index) {
|
|
14
|
+
/* v8 ignore next */
|
|
15
|
+
return /[Ee]/.test(content[index] ?? '') && content[index + 1] === "'";
|
|
16
|
+
}
|
|
17
|
+
export function stringLiteralQuoteStart(content, index) {
|
|
18
|
+
if (isEscapeStringStart(content, index))
|
|
19
|
+
return { quoteStart: index + 1, escapeString: true };
|
|
20
|
+
if (/[Uu]/.test(content[index] ?? '') &&
|
|
21
|
+
content[index + 1] === '&' &&
|
|
22
|
+
content[index + 2] === "'") {
|
|
23
|
+
return { quoteStart: index + 2, escapeString: false };
|
|
24
|
+
}
|
|
25
|
+
if (content[index] === "'")
|
|
26
|
+
return { quoteStart: index, escapeString: false };
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function singleQuoteEnd(content, start, escapeString) {
|
|
30
|
+
for (let i = start + 1; i < content.length; i++) {
|
|
31
|
+
if (escapeString && content[i] === '\\') {
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (content[i] !== "'")
|
|
36
|
+
continue;
|
|
37
|
+
if (content[i + 1] === "'") {
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return i;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return content.length;
|
|
45
|
+
}
|
|
46
|
+
function decodeSqlStringBody(content, escapeString) {
|
|
47
|
+
let decoded = '';
|
|
48
|
+
for (let i = 0; i < content.length; i++) {
|
|
49
|
+
if (escapeString && content[i] === '\\' && i + 1 < content.length) {
|
|
50
|
+
const escaped = content[i + 1];
|
|
51
|
+
decoded += escaped === 'n' ? '\n' : escaped === 'r' ? '\r' : escaped === 't' ? '\t' : escaped;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
else if (content[i] === "'" && content[i + 1] === "'") {
|
|
55
|
+
decoded += "'";
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
decoded += content[i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return decoded;
|
|
63
|
+
}
|
|
64
|
+
export function maskSqlQuotedText(content) {
|
|
65
|
+
let masked = '';
|
|
66
|
+
let inSingleQuote = false;
|
|
67
|
+
let inEscapeString = false;
|
|
68
|
+
let dollarQuoteDelimiter = null;
|
|
69
|
+
for (let i = 0; i < content.length; i++) {
|
|
70
|
+
if (dollarQuoteDelimiter) {
|
|
71
|
+
if (content.startsWith(dollarQuoteDelimiter, i)) {
|
|
72
|
+
masked += ' '.repeat(dollarQuoteDelimiter.length);
|
|
73
|
+
i += dollarQuoteDelimiter.length - 1;
|
|
74
|
+
dollarQuoteDelimiter = null;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
masked += content[i] === '\n' ? '\n' : ' ';
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (inSingleQuote) {
|
|
82
|
+
masked += ' ';
|
|
83
|
+
if (inEscapeString && content[i] === '\\' && i + 1 < content.length) {
|
|
84
|
+
masked += ' ';
|
|
85
|
+
i++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (content[i] === "'") {
|
|
89
|
+
if (content[i + 1] === "'") {
|
|
90
|
+
masked += ' ';
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
inSingleQuote = false;
|
|
95
|
+
inEscapeString = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const dollarQuote = readDollarQuoteDelimiter(content, i);
|
|
101
|
+
if (dollarQuote) {
|
|
102
|
+
dollarQuoteDelimiter = dollarQuote;
|
|
103
|
+
masked += ' '.repeat(dollarQuote.length);
|
|
104
|
+
i += dollarQuote.length - 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const stringStart = stringLiteralQuoteStart(content, i);
|
|
108
|
+
if (stringStart) {
|
|
109
|
+
inSingleQuote = true;
|
|
110
|
+
inEscapeString = stringStart.escapeString;
|
|
111
|
+
masked += ' '.repeat(stringStart.quoteStart - i + 1);
|
|
112
|
+
i = stringStart.quoteStart;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
masked += content[i];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return masked;
|
|
119
|
+
}
|
|
120
|
+
export function readStringLiteral(content, index) {
|
|
121
|
+
const start = stringLiteralQuoteStart(content, index);
|
|
122
|
+
if (!start)
|
|
123
|
+
return null;
|
|
124
|
+
const bodyEnd = singleQuoteEnd(content, start.quoteStart, start.escapeString);
|
|
125
|
+
return {
|
|
126
|
+
text: decodeSqlStringBody(content.slice(start.quoteStart + 1, bodyEnd), start.escapeString),
|
|
127
|
+
index: start.quoteStart + 1,
|
|
128
|
+
end: bodyEnd + 1,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function dollarQuoteEnd(content, start, delimiter) {
|
|
132
|
+
for (let i = start; i < content.length; i++) {
|
|
133
|
+
if (content.startsWith(delimiter, i))
|
|
134
|
+
return i;
|
|
135
|
+
}
|
|
136
|
+
return -1;
|
|
137
|
+
}
|
|
138
|
+
export function sqlFragments(content) {
|
|
139
|
+
const fragments = [];
|
|
140
|
+
for (let i = 0; i < content.length; i++) {
|
|
141
|
+
const dollarQuote = readDollarQuoteDelimiter(content, i);
|
|
142
|
+
if (dollarQuote) {
|
|
143
|
+
const bodyStart = i + dollarQuote.length;
|
|
144
|
+
const bodyEnd = dollarQuoteEnd(content, bodyStart, dollarQuote);
|
|
145
|
+
if (bodyEnd === -1)
|
|
146
|
+
break;
|
|
147
|
+
fragments.push({ text: content.slice(bodyStart, bodyEnd), index: bodyStart });
|
|
148
|
+
i = bodyEnd + dollarQuote.length - 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const literal = readStringLiteral(content, i);
|
|
152
|
+
if (literal) {
|
|
153
|
+
fragments.push({ text: literal.text, index: literal.index });
|
|
154
|
+
i = literal.end - 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return fragments;
|
|
158
|
+
}
|