vouchington-tooling 0.0.2 → 0.0.3

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.
Files changed (34) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.mts +3 -1
  3. package/dist/index.mjs +2 -1
  4. package/dist/sql-ast/add-column.d.mts +1 -0
  5. package/dist/sql-ast/add-column.mjs +22 -0
  6. package/dist/sql-ast/constraint-do-block.d.mts +11 -0
  7. package/dist/sql-ast/constraint-do-block.mjs +83 -0
  8. package/dist/sql-ast/constraint-shared.d.mts +19 -0
  9. package/dist/sql-ast/constraint-shared.mjs +20 -0
  10. package/dist/sql-ast/constraint.d.mts +2 -0
  11. package/dist/sql-ast/constraint.mjs +63 -0
  12. package/dist/sql-ast/create-table.d.mts +25 -0
  13. package/dist/sql-ast/create-table.mjs +79 -0
  14. package/dist/sql-ast/default-function.d.mts +18 -0
  15. package/dist/sql-ast/default-function.mjs +53 -0
  16. package/dist/sql-ast/drop-index.d.mts +5 -0
  17. package/dist/sql-ast/drop-index.mjs +31 -0
  18. package/dist/sql-ast/implicit-indexes.d.mts +3 -0
  19. package/dist/sql-ast/implicit-indexes.mjs +66 -0
  20. package/dist/sql-ast/index-metadata.d.mts +19 -0
  21. package/dist/sql-ast/index-metadata.mjs +100 -0
  22. package/dist/sql-ast/index.d.mts +12 -12
  23. package/dist/sql-ast/index.mjs +8 -79
  24. package/dist/sql-ast/line-of-offset.d.mts +1 -0
  25. package/dist/sql-ast/line-of-offset.mjs +12 -0
  26. package/dist/sql-ast/parser.d.mts +11 -0
  27. package/dist/sql-ast/parser.mjs +46 -0
  28. package/dist/sql-ast/unknown-record.d.mts +1 -0
  29. package/dist/sql-ast/unknown-record.mjs +3 -0
  30. package/dist/sql-scanner/index.d.mts +7 -0
  31. package/dist/sql-scanner/index.mjs +172 -0
  32. package/dist/sql-scanner/literals.d.mts +17 -0
  33. package/dist/sql-scanner/literals.mjs +158 -0
  34. package/package.json +7 -2
package/README.md CHANGED
@@ -35,4 +35,6 @@ import {
35
35
  listenOnRunnerUnreservedEphemeralPort,
36
36
  runnerPortPolicy,
37
37
  } from 'vouchington-tooling/runner-port-policy'
38
+ import { initSqlAst, extractCreateTableMetadata } from 'vouchington-tooling/sql-ast'
39
+ import { splitSqlStatements, stripSqlComments } from 'vouchington-tooling/sql-scanner'
38
40
  ```
package/dist/index.d.mts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mts';
2
2
  export type { EphemeralListenerOptions, RunnerPortPolicy } from './runner-port-policy/index.mts';
3
- export { extractAlterTableAddColumnLocations, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, } from './sql-ast/index.mts';
3
+ export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mts';
4
+ export type { ForeignKey, SqlCreateIndexMetadata, SqlCreateTableColumn, SqlCreateTableMetadata, SqlDropIndexMetadata, SqlIndexParam, SqlMigrationConstraintMetadata, } from './sql-ast/index.mts';
5
+ export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mts';
package/dist/index.mjs CHANGED
@@ -1,2 +1,3 @@
1
1
  export { EphemeralListenerAttemptsExhaustedError, isRunnerReservedPort, listenOnRunnerUnreservedEphemeralPort, loadRunnerPortPolicy, runnerPortPolicy, validateRunnerPortPolicy, } from './runner-port-policy/index.mjs';
2
- export { extractAlterTableAddColumnLocations, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, } from './sql-ast/index.mjs';
2
+ export { extractAlterTableAddColumnLocations, extractCreateIndexMetadata, extractCreateTableMetadata, extractDefaultFunction, extractDropIndexMetadata, extractFuncCallArgColumnNames, extractMigrationConstraintMetadata, initSqlAst, lineOfUtf8ByteOffset, MissingSqlAstParserError, parseSql, } from './sql-ast/index.mjs';
3
+ export { dollarQuoteEnd, lineOf, maskSqlQuotedText, readDollarQuoteDelimiter, readStringLiteral, splitSqlStatements, sqlFragments, stripSqlComments, } from './sql-scanner/index.mjs';
@@ -0,0 +1 @@
1
+ export declare function extractAlterTableAddColumnLocations(content: string): number[];
@@ -0,0 +1,22 @@
1
+ import { parseSql } from './parser.mjs';
2
+ export function extractAlterTableAddColumnLocations(content) {
3
+ const locations = [];
4
+ const parseResult = parseSql(content);
5
+ for (const rawStmt of parseResult.stmts ?? []) {
6
+ const node = rawStmt.stmt;
7
+ if (!node || !('AlterTableStmt' in node))
8
+ continue;
9
+ for (const rawCommand of node.AlterTableStmt.cmds ?? []) {
10
+ if (!rawCommand || !('AlterTableCmd' in rawCommand))
11
+ continue;
12
+ const command = rawCommand.AlterTableCmd;
13
+ if (command.subtype !== 'AT_AddColumn')
14
+ continue;
15
+ const definition = command.def;
16
+ if (!definition || !('ColumnDef' in definition))
17
+ continue;
18
+ locations.push(rawStmt.stmt_location ?? 0);
19
+ }
20
+ }
21
+ return locations;
22
+ }
@@ -0,0 +1,11 @@
1
+ import type { AlterTableStmt, DoStmt } from '@libpg-query/parser';
2
+ import { type SqlMigrationConstraintMetadata } from './constraint-shared.mts';
3
+ /**
4
+ * Applies AT_AddConstraint / AT_ValidateConstraint commands from one ALTER TABLE to `result`.
5
+ *
6
+ * `trackConstraintValidation` gates NOT VALID/VALIDATE pairing bookkeeping. Callers that
7
+ * parse ALTER TABLE text out of a DO body should pass false so only `foreignKeys` is filled.
8
+ */
9
+ export declare function processAlterTableStmt(statement: AlterTableStmt, baseOffset: number, result: SqlMigrationConstraintMetadata, trackConstraintValidation?: boolean): void;
10
+ /** Idempotent migrations wrap `ALTER TABLE ... ADD CONSTRAINT` in `DO $$ ... $$` catalog checks. */
11
+ export declare function collectDoStmtConstraints(content: string, statement: DoStmt, stmtLocation: number, result: SqlMigrationConstraintMetadata): void;
@@ -0,0 +1,83 @@
1
+ import { fkAttrNames, foreignKeyFromConstraint, } from './constraint-shared.mjs';
2
+ import { parseSql } from './parser.mjs';
3
+ /**
4
+ * Applies AT_AddConstraint / AT_ValidateConstraint commands from one ALTER TABLE to `result`.
5
+ *
6
+ * `trackConstraintValidation` gates NOT VALID/VALIDATE pairing bookkeeping. Callers that
7
+ * parse ALTER TABLE text out of a DO body should pass false so only `foreignKeys` is filled.
8
+ */
9
+ export function processAlterTableStmt(statement, baseOffset, result, trackConstraintValidation = true) {
10
+ const tableName = statement.relation?.relname;
11
+ /* v8 ignore next */
12
+ if (!tableName)
13
+ return;
14
+ for (const rawCommand of statement.cmds ?? []) {
15
+ /* v8 ignore next */
16
+ if (!rawCommand || !('AlterTableCmd' in rawCommand))
17
+ continue;
18
+ const command = rawCommand.AlterTableCmd;
19
+ if (command.subtype === 'AT_ValidateConstraint' && command.name) {
20
+ if (trackConstraintValidation)
21
+ result.validatedConstraints.add(`${tableName}.${command.name}`);
22
+ continue;
23
+ }
24
+ /* v8 ignore next */
25
+ if (command.subtype !== 'AT_AddConstraint')
26
+ continue;
27
+ const definition = command.def;
28
+ /* v8 ignore next */
29
+ if (!definition || !('Constraint' in definition))
30
+ continue;
31
+ const constraint = definition.Constraint;
32
+ /* v8 ignore next */
33
+ if (!constraint)
34
+ continue;
35
+ if (constraint.contype === 'CONSTR_FOREIGN') {
36
+ const constraintRecord = constraint;
37
+ result.foreignKeys.push(foreignKeyFromConstraint(constraintRecord, tableName, fkAttrNames(constraintRecord), baseOffset));
38
+ }
39
+ if (!trackConstraintValidation)
40
+ continue;
41
+ /* v8 ignore next */
42
+ if (constraint.contype !== 'CONSTR_CHECK' && constraint.contype !== 'CONSTR_FOREIGN')
43
+ continue;
44
+ result.addedConstraints.push({
45
+ /* v8 ignore next 3 */
46
+ constraintType: constraint.contype ?? null,
47
+ location: baseOffset + (constraint.location ?? 0),
48
+ name: constraint.conname ?? null,
49
+ tableName,
50
+ });
51
+ }
52
+ }
53
+ const ALTER_TABLE_STATEMENT_RE = /ALTER\s+TABLE\b[^;]*;/gi;
54
+ /** Idempotent migrations wrap `ALTER TABLE ... ADD CONSTRAINT` in `DO $$ ... $$` catalog checks. */
55
+ /* v8 ignore start -- DO-body regex/parse fallbacks */
56
+ export function collectDoStmtConstraints(content, statement, stmtLocation, result) {
57
+ for (const arg of statement.args ?? []) {
58
+ const defElem = arg && 'DefElem' in arg ? arg.DefElem : undefined;
59
+ const stringArg = defElem?.arg && 'String' in defElem.arg ? defElem.arg.String : undefined;
60
+ const body = typeof stringArg?.sval === 'string' ? stringArg.sval : '';
61
+ for (const match of body.matchAll(ALTER_TABLE_STATEMENT_RE)) {
62
+ const baseOffset = content.indexOf(match[0], stmtLocation);
63
+ /* v8 ignore next */
64
+ if (baseOffset === -1)
65
+ continue;
66
+ let nested;
67
+ try {
68
+ nested = parseSql(match[0]);
69
+ }
70
+ catch {
71
+ /* v8 ignore next */
72
+ continue;
73
+ }
74
+ for (const nestedStmt of nested.stmts ?? []) {
75
+ const node = nestedStmt.stmt;
76
+ if (node && 'AlterTableStmt' in node) {
77
+ processAlterTableStmt(node.AlterTableStmt, baseOffset, result, false);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ /* v8 ignore stop */
@@ -0,0 +1,19 @@
1
+ export type SqlMigrationConstraintMetadata = {
2
+ addedConstraints: Array<{
3
+ constraintType: string | null;
4
+ location: number;
5
+ name: string | null;
6
+ tableName: string;
7
+ }>;
8
+ foreignKeys: Array<{
9
+ columnNames: string[];
10
+ deleteAction: string | null;
11
+ location: number;
12
+ referencedTableName: string | null;
13
+ tableName: string | null;
14
+ }>;
15
+ validatedConstraints: Set<string>;
16
+ };
17
+ export type ForeignKey = SqlMigrationConstraintMetadata['foreignKeys'][number];
18
+ export declare function fkAttrNames(constraint: Record<string, unknown>): string[];
19
+ export declare function foreignKeyFromConstraint(constraint: Record<string, unknown>, tableName: string | null, columnNames: string[], baseOffset?: number): ForeignKey;
@@ -0,0 +1,20 @@
1
+ import { isRecord } from './unknown-record.mjs';
2
+ export function fkAttrNames(constraint) {
3
+ const attrs = constraint.fk_attrs;
4
+ if (!Array.isArray(attrs))
5
+ return [];
6
+ return attrs.flatMap((attr) => {
7
+ const sval = isRecord(attr) && isRecord(attr.String) ? attr.String.sval : undefined;
8
+ return typeof sval === 'string' ? [sval] : [];
9
+ });
10
+ }
11
+ export function foreignKeyFromConstraint(constraint, tableName, columnNames, baseOffset = 0) {
12
+ const pktable = constraint.pktable;
13
+ return {
14
+ columnNames,
15
+ deleteAction: typeof constraint.fk_del_action === 'string' ? constraint.fk_del_action : null,
16
+ location: baseOffset + (typeof constraint.location === 'number' ? constraint.location : 0),
17
+ referencedTableName: isRecord(pktable) && typeof pktable.relname === 'string' ? pktable.relname : null,
18
+ tableName,
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ import { type SqlMigrationConstraintMetadata } from './constraint-shared.mts';
2
+ export declare function extractMigrationConstraintMetadata(content: string): SqlMigrationConstraintMetadata;
@@ -0,0 +1,63 @@
1
+ import { collectDoStmtConstraints, processAlterTableStmt } from './constraint-do-block.mjs';
2
+ import { fkAttrNames, foreignKeyFromConstraint, } from './constraint-shared.mjs';
3
+ import { parseSql } from './parser.mjs';
4
+ import { isRecord } from './unknown-record.mjs';
5
+ /** Inline `col UUID REFERENCES ...` and table-level `FOREIGN KEY (...) REFERENCES ...` forms. */
6
+ /* v8 ignore start -- defensive parse-tree walks */
7
+ function collectCreateStmtForeignKeys(statement, foreignKeys) {
8
+ const relation = statement.relation;
9
+ const tableName = isRecord(relation) && typeof relation.relname === 'string' ? relation.relname : null;
10
+ const tableElts = statement.tableElts;
11
+ if (!Array.isArray(tableElts))
12
+ return;
13
+ for (const element of tableElts) {
14
+ if (!isRecord(element))
15
+ continue;
16
+ if (isRecord(element.ColumnDef)) {
17
+ const column = element.ColumnDef;
18
+ const colname = typeof column.colname === 'string' ? column.colname : null;
19
+ for (const constraintNode of column.constraints ?? []) {
20
+ /* v8 ignore next 3 */
21
+ if (!isRecord(constraintNode) || !isRecord(constraintNode.Constraint))
22
+ continue;
23
+ const constraint = constraintNode.Constraint;
24
+ if (constraint.contype !== 'CONSTR_FOREIGN')
25
+ continue;
26
+ foreignKeys.push(foreignKeyFromConstraint(constraint, tableName, colname ? [colname] : []));
27
+ }
28
+ continue;
29
+ }
30
+ if (isRecord(element.Constraint) && element.Constraint.contype === 'CONSTR_FOREIGN') {
31
+ foreignKeys.push(foreignKeyFromConstraint(element.Constraint, tableName, fkAttrNames(element.Constraint)));
32
+ }
33
+ }
34
+ }
35
+ /* v8 ignore stop */
36
+ export function extractMigrationConstraintMetadata(content) {
37
+ const result = {
38
+ addedConstraints: [],
39
+ foreignKeys: [],
40
+ validatedConstraints: new Set(),
41
+ };
42
+ const parseResult = parseSql(content);
43
+ for (const rawStmt of parseResult.stmts ?? []) {
44
+ const node = rawStmt.stmt;
45
+ /* v8 ignore next */
46
+ if (!node)
47
+ continue;
48
+ const stmtLocation = rawStmt.stmt_location ?? 0;
49
+ if ('CreateStmt' in node) {
50
+ collectCreateStmtForeignKeys(node.CreateStmt, result.foreignKeys);
51
+ continue;
52
+ }
53
+ if ('DoStmt' in node) {
54
+ collectDoStmtConstraints(content, node.DoStmt, stmtLocation, result);
55
+ continue;
56
+ }
57
+ /* v8 ignore next */
58
+ if (!('AlterTableStmt' in node))
59
+ continue;
60
+ processAlterTableStmt(node.AlterTableStmt, 0, result);
61
+ }
62
+ return result;
63
+ }
@@ -0,0 +1,25 @@
1
+ export type SqlCreateTableColumnConstraint = {
2
+ contype: string | null;
3
+ };
4
+ export type SqlCreateTableColumn = {
5
+ name: string;
6
+ location: number | null;
7
+ constraints: SqlCreateTableColumnConstraint[];
8
+ isPrimaryKey: boolean;
9
+ defaultFunction: string | null;
10
+ /** Function name of a CONSTR_GENERATED expression (e.g. `uuid_extract_timestamp`), null if not generated or unrecognized. */
11
+ generatedFunction: string | null;
12
+ /** Lowercased argument column names of a CONSTR_GENERATED expression's function call (e.g. `['id']`). */
13
+ generatedFunctionArgColumns: string[];
14
+ };
15
+ export type SqlCreateTableMetadata = {
16
+ tableName: string;
17
+ columns: SqlCreateTableColumn[];
18
+ };
19
+ /**
20
+ * A column is a primary key via either the inline `col TYPE PRIMARY KEY` constraint
21
+ * (nested inside its ColumnDef) or the table-level `PRIMARY KEY (col)` constraint
22
+ * (a bare `Constraint` sibling in tableElts, referencing the column by name in `keys`).
23
+ * Both forms are checked so table-level-PK migrations aren't silently mis-detected.
24
+ */
25
+ export declare function extractCreateTableMetadata(content: string): SqlCreateTableMetadata[];
@@ -0,0 +1,79 @@
1
+ import { extractDefaultFunction, extractFuncCallArgColumnNames } from './default-function.mjs';
2
+ import { parseSql } from './parser.mjs';
3
+ /**
4
+ * A column is a primary key via either the inline `col TYPE PRIMARY KEY` constraint
5
+ * (nested inside its ColumnDef) or the table-level `PRIMARY KEY (col)` constraint
6
+ * (a bare `Constraint` sibling in tableElts, referencing the column by name in `keys`).
7
+ * Both forms are checked so table-level-PK migrations aren't silently mis-detected.
8
+ */
9
+ export function extractCreateTableMetadata(content) {
10
+ const tables = [];
11
+ const parseResult = parseSql(content);
12
+ for (const rawStmt of parseResult.stmts ?? []) {
13
+ const node = rawStmt.stmt;
14
+ if (!node || !('CreateStmt' in node))
15
+ continue;
16
+ const createStmt = node.CreateStmt;
17
+ const tableName = createStmt.relation?.relname;
18
+ if (!tableName)
19
+ continue;
20
+ const tableLevelPrimaryKeyColumns = new Set((createStmt.tableElts ?? []).flatMap((elt) => {
21
+ /* v8 ignore next 8 */
22
+ if (!elt || typeof elt !== 'object' || !('Constraint' in elt))
23
+ return [];
24
+ const constraint = elt.Constraint;
25
+ if (constraint.contype !== 'CONSTR_PRIMARY')
26
+ return [];
27
+ return (constraint.keys ?? []).flatMap((key) => key && typeof key === 'object' && 'String' in key && typeof key.String.sval === 'string'
28
+ ? [key.String.sval]
29
+ : []);
30
+ }));
31
+ tables.push({
32
+ tableName,
33
+ columns: (createStmt.tableElts ?? []).flatMap((elt) => {
34
+ if (!elt || typeof elt !== 'object' || !('ColumnDef' in elt))
35
+ return [];
36
+ const column = elt.ColumnDef;
37
+ if (!column || typeof column !== 'object' || !column.colname)
38
+ return [];
39
+ const rawConstraints = (column.constraints ?? []).flatMap((constraint) => {
40
+ /* v8 ignore next 4 */
41
+ if (!constraint || typeof constraint !== 'object' || !('Constraint' in constraint)) {
42
+ return [];
43
+ }
44
+ return constraint.Constraint ? [constraint.Constraint] : [];
45
+ });
46
+ let defaultConstraint;
47
+ let generatedConstraint;
48
+ for (const constraint of rawConstraints) {
49
+ if (constraint.contype === 'CONSTR_DEFAULT')
50
+ defaultConstraint ??= constraint;
51
+ if (constraint.contype === 'CONSTR_GENERATED')
52
+ generatedConstraint ??= constraint;
53
+ }
54
+ return [
55
+ {
56
+ name: column.colname,
57
+ location: column.location ?? null,
58
+ constraints: rawConstraints.map((constraint) => ({
59
+ /* v8 ignore next */
60
+ contype: constraint.contype ?? null,
61
+ })),
62
+ isPrimaryKey: rawConstraints.some((constraint) => constraint.contype === 'CONSTR_PRIMARY') ||
63
+ tableLevelPrimaryKeyColumns.has(column.colname),
64
+ defaultFunction: defaultConstraint
65
+ ? extractDefaultFunction(defaultConstraint.raw_expr)
66
+ : null,
67
+ generatedFunction: generatedConstraint
68
+ ? extractDefaultFunction(generatedConstraint.raw_expr)
69
+ : null,
70
+ generatedFunctionArgColumns: generatedConstraint
71
+ ? extractFuncCallArgColumnNames(generatedConstraint.raw_expr)
72
+ : [],
73
+ },
74
+ ];
75
+ }),
76
+ });
77
+ }
78
+ return tables;
79
+ }
@@ -0,0 +1,18 @@
1
+ import type { Node } from '@libpg-query/parser';
2
+ /**
3
+ * Resolves a CONSTR_DEFAULT constraint's raw_expr to a lowercase function/op name.
4
+ * Handles the shapes @libpg-query/parser produces for column defaults:
5
+ * - `FuncCall` (e.g. `uuidv7()`, `now()`): last segment of `funcname`, lowercased.
6
+ * - `SQLValueFunction` (e.g. bare `CURRENT_TIMESTAMP`): mapped from its `op` enum.
7
+ * - `TypeCast` (e.g. `now()::timestamptz`): recurses into the cast's inner arg.
8
+ * Returns null for any other/unrecognized raw_expr shape.
9
+ */
10
+ export declare function extractDefaultFunction(rawExpr: Node | undefined): string | null;
11
+ /**
12
+ * Resolves a FuncCall raw_expr's argument column names, lowercased (e.g. the `id` in
13
+ * `uuid_extract_timestamp(id)`). Used to verify a GENERATED column expression references
14
+ * a specific source column, not just that it calls the expected function.
15
+ * Returns an empty array for non-FuncCall shapes, a TypeCast-wrapped call recurses into
16
+ * its inner arg; non-column-ref args are skipped.
17
+ */
18
+ export declare function extractFuncCallArgColumnNames(rawExpr: Node | undefined): string[];
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Resolves a CONSTR_DEFAULT constraint's raw_expr to a lowercase function/op name.
3
+ * Handles the shapes @libpg-query/parser produces for column defaults:
4
+ * - `FuncCall` (e.g. `uuidv7()`, `now()`): last segment of `funcname`, lowercased.
5
+ * - `SQLValueFunction` (e.g. bare `CURRENT_TIMESTAMP`): mapped from its `op` enum.
6
+ * - `TypeCast` (e.g. `now()::timestamptz`): recurses into the cast's inner arg.
7
+ * Returns null for any other/unrecognized raw_expr shape.
8
+ */
9
+ export function extractDefaultFunction(rawExpr) {
10
+ if (!rawExpr)
11
+ return null;
12
+ if ('TypeCast' in rawExpr) {
13
+ return extractDefaultFunction(rawExpr.TypeCast.arg);
14
+ }
15
+ if ('FuncCall' in rawExpr) {
16
+ const funcname = rawExpr.FuncCall.funcname ?? [];
17
+ const last = funcname.at(-1);
18
+ if (last && 'String' in last && typeof last.String.sval === 'string') {
19
+ return last.String.sval.toLowerCase();
20
+ }
21
+ return null;
22
+ }
23
+ if ('SQLValueFunction' in rawExpr) {
24
+ return rawExpr.SQLValueFunction.op === 'SVFOP_CURRENT_TIMESTAMP' ? 'current_timestamp' : null;
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * Resolves a FuncCall raw_expr's argument column names, lowercased (e.g. the `id` in
30
+ * `uuid_extract_timestamp(id)`). Used to verify a GENERATED column expression references
31
+ * a specific source column, not just that it calls the expected function.
32
+ * Returns an empty array for non-FuncCall shapes, a TypeCast-wrapped call recurses into
33
+ * its inner arg; non-column-ref args are skipped.
34
+ */
35
+ export function extractFuncCallArgColumnNames(rawExpr) {
36
+ if (!rawExpr)
37
+ return [];
38
+ if ('TypeCast' in rawExpr) {
39
+ return extractFuncCallArgColumnNames(rawExpr.TypeCast.arg);
40
+ }
41
+ if (!('FuncCall' in rawExpr))
42
+ return [];
43
+ return (rawExpr.FuncCall.args ?? []).flatMap((arg) => {
44
+ if (!arg || typeof arg !== 'object' || !('ColumnRef' in arg))
45
+ return [];
46
+ const fields = arg.ColumnRef.fields ?? [];
47
+ const last = fields.at(-1);
48
+ /* v8 ignore next 4 */
49
+ return last && 'String' in last && typeof last.String.sval === 'string'
50
+ ? [last.String.sval.toLowerCase()]
51
+ : [];
52
+ });
53
+ }
@@ -0,0 +1,5 @@
1
+ export type SqlDropIndexMetadata = {
2
+ idxname: string;
3
+ location: number;
4
+ };
5
+ export declare function extractDropIndexMetadata(content: string): SqlDropIndexMetadata[];
@@ -0,0 +1,31 @@
1
+ import { parseSql } from './parser.mjs';
2
+ import { isRecord } from './unknown-record.mjs';
3
+ export function extractDropIndexMetadata(content) {
4
+ const indexes = [];
5
+ const parseResult = parseSql(content);
6
+ for (const rawStmt of parseResult.stmts ?? []) {
7
+ const node = rawStmt.stmt;
8
+ /* v8 ignore next 2 */
9
+ if (!node || !('DropStmt' in node) || node.DropStmt.removeType !== 'OBJECT_INDEX')
10
+ continue;
11
+ for (const object of node.DropStmt.objects ?? []) {
12
+ const objectValue = object;
13
+ const items = isRecord(objectValue) && isRecord(objectValue['List'])
14
+ ? objectValue['List']['items']
15
+ : undefined;
16
+ if (!Array.isArray(items))
17
+ continue;
18
+ const names = items.flatMap((item) => {
19
+ const itemValue = item;
20
+ const value = isRecord(itemValue) && isRecord(itemValue['String'])
21
+ ? itemValue['String']['sval']
22
+ : undefined;
23
+ return typeof value === 'string' ? [value] : [];
24
+ });
25
+ const idxname = names.at(-1);
26
+ if (idxname)
27
+ indexes.push({ idxname, location: rawStmt.stmt_location ?? 0 });
28
+ }
29
+ }
30
+ return indexes;
31
+ }
@@ -0,0 +1,3 @@
1
+ import type { SqlCreateIndexMetadata } from './index-metadata.mts';
2
+ /** UNIQUE/PRIMARY KEY create implicit unique indexes that never appear as CREATE INDEX. */
3
+ export declare function collectCreateStmtImplicitIndexes(statement: Record<string, unknown>, location: number, indexes: SqlCreateIndexMetadata[]): void;
@@ -0,0 +1,66 @@
1
+ import { isRecord } from './unknown-record.mjs';
2
+ function implicitIndexParam(name) {
3
+ return { name, opclass: null, ordering: null, nullsOrdering: null };
4
+ }
5
+ function pushImplicitIndex(indexes, relname, location, columnNames) {
6
+ indexes.push({
7
+ relname,
8
+ idxname: null,
9
+ unique: true,
10
+ indexParams: columnNames,
11
+ indexParamDetails: columnNames.map(implicitIndexParam),
12
+ includeParams: [],
13
+ whereClause: null,
14
+ whereClauseKey: null,
15
+ accessMethod: 'btree',
16
+ location,
17
+ });
18
+ }
19
+ function constraintKeyNames(constraint) {
20
+ const keys = constraint.keys;
21
+ if (!Array.isArray(keys))
22
+ return [];
23
+ return keys.flatMap((key) => {
24
+ const sval = isRecord(key) && isRecord(key.String) ? key.String.sval : undefined;
25
+ /* v8 ignore next */
26
+ return typeof sval === 'string' ? [sval] : [];
27
+ });
28
+ }
29
+ /** UNIQUE/PRIMARY KEY create implicit unique indexes that never appear as CREATE INDEX. */
30
+ export function collectCreateStmtImplicitIndexes(statement, location, indexes) {
31
+ const relation = statement.relation;
32
+ const relname = isRecord(relation) && typeof relation.relname === 'string' ? relation.relname : null;
33
+ const tableElts = statement.tableElts;
34
+ if (!relname || !Array.isArray(tableElts))
35
+ return;
36
+ for (const element of tableElts) {
37
+ if (!isRecord(element))
38
+ continue;
39
+ if (isRecord(element.ColumnDef)) {
40
+ const column = element.ColumnDef;
41
+ const colname = typeof column.colname === 'string' ? column.colname : null;
42
+ if (!colname)
43
+ continue;
44
+ for (const constraintNode of column.constraints ?? []) {
45
+ /* v8 ignore next */
46
+ if (!isRecord(constraintNode) || !isRecord(constraintNode.Constraint))
47
+ continue;
48
+ const contype = constraintNode.Constraint.contype;
49
+ if (contype !== 'CONSTR_UNIQUE' && contype !== 'CONSTR_PRIMARY')
50
+ continue;
51
+ pushImplicitIndex(indexes, relname, location, [colname]);
52
+ }
53
+ continue;
54
+ }
55
+ /* v8 ignore next */
56
+ if (!isRecord(element.Constraint))
57
+ continue;
58
+ const contype = element.Constraint.contype;
59
+ if (contype !== 'CONSTR_UNIQUE' && contype !== 'CONSTR_PRIMARY')
60
+ continue;
61
+ const columnNames = constraintKeyNames(element.Constraint);
62
+ if (columnNames.length === 0)
63
+ continue;
64
+ pushImplicitIndex(indexes, relname, location, columnNames);
65
+ }
66
+ }
@@ -0,0 +1,19 @@
1
+ export type SqlIndexParam = {
2
+ name: string | null;
3
+ opclass: string | null;
4
+ ordering: string | null;
5
+ nullsOrdering: string | null;
6
+ };
7
+ export type SqlCreateIndexMetadata = {
8
+ relname: string;
9
+ idxname: string | null;
10
+ unique: boolean;
11
+ indexParams: Array<string | null>;
12
+ indexParamDetails: SqlIndexParam[];
13
+ includeParams: string[];
14
+ whereClause: unknown;
15
+ whereClauseKey: string | null;
16
+ accessMethod: string;
17
+ location: number;
18
+ };
19
+ export declare function extractCreateIndexMetadata(content: string): SqlCreateIndexMetadata[];
@@ -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
+ }
@@ -1,12 +1,12 @@
1
- type LibPgQuery = typeof import('@libpg-query/parser');
2
- export declare class MissingSqlAstParserError extends Error {
3
- constructor();
4
- }
5
- export declare function extractAlterTableAddColumnLocations(content: string): number[];
6
- /**
7
- * Ensures the @libpg-query/parser WASM module is loaded.
8
- * Must be awaited once before any synchronous parseSync() calls.
9
- */
10
- export declare function initSqlAst(importer?: () => Promise<LibPgQuery>): Promise<void>;
11
- export declare function lineOfUtf8ByteOffset(content: string | Buffer, byteOffset: number): number;
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';
@@ -1,79 +1,8 @@
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
- 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,3 @@
1
+ export function isRecord(value) {
2
+ return typeof value === 'object' && value !== null;
3
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -40,6 +40,11 @@
40
40
  "import": "./dist/sql-ast/index.mjs",
41
41
  "default": "./dist/sql-ast/index.mjs"
42
42
  },
43
+ "./sql-scanner": {
44
+ "types": "./dist/sql-scanner/index.d.mts",
45
+ "import": "./dist/sql-scanner/index.mjs",
46
+ "default": "./dist/sql-scanner/index.mjs"
47
+ },
43
48
  "./package.json": "./package.json"
44
49
  },
45
50
  "publishConfig": {
@@ -51,7 +56,7 @@
51
56
  "typecheck": "tsc --noEmit --project tsconfig.json"
52
57
  },
53
58
  "optionalDependencies": {
54
- "@libpg-query/parser": "^17.6.10"
59
+ "@libpg-query/parser": "^18.0.0"
55
60
  },
56
61
  "engines": {
57
62
  "node": ">=24.0.0"