vouchington-tooling 0.38.1 → 0.40.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/README.md CHANGED
@@ -512,6 +512,18 @@ the dedicated process group to exit after the direct child closes so descendants
512
512
  `isProcessGroupAlive` and `waitForProcessGroupExit` are also available when callers need the same
513
513
  process-group probe and bounded-drain semantics outside a browser session.
514
514
 
515
+ ### Generated arbiter input
516
+
517
+ The `pg-schema-snapshot` export also provides `projectGeneratedArbiterInput(conflict)` for
518
+ `PostgresSqlConflict` facts from `no-mistakes@0.83.0` or newer. It projects normalized key and
519
+ partial-predicate column identities, ordered assignment columns, direct non-EXCLUDED references,
520
+ and assignment indirection. Named constraints and unresolved expression keys have undefined
521
+ `keyColumns`; missing or incomplete expression coverage sets `complete` to false. Callers must
522
+ reject incomplete projections and retain their own snapshot-dependent replay verdict. The adapter
523
+ loads no optional parser peer at runtime and implements no replay-safety policy. Its public
524
+ `GeneratedArbiterConflict` contract is structural, so importing snapshot declarations does not
525
+ require the optional peer either.
526
+
515
527
  ### Contract schema type queries
516
528
 
517
529
  `getExportedTypeFacts` and `getCallRowTypeFacts` use an existing TypeScript `Program` from the
@@ -686,6 +698,16 @@ reexports, and literal dynamic/require loads. Missing or incomplete facts for a
686
698
  throw; external specifiers resolve to `null`. The default file inventory limit is 10,000
687
699
  files and can be reduced with `maxFiles`.
688
700
 
701
+ ## Static schema-allowlist pairs
702
+
703
+ `extractStaticStringPairs(source)` from `pg-schema-snapshot` collects the first two string literals
704
+ of every array expression in ESTree field traversal order, including arrays with extra elements.
705
+ Conditional alternatives precede consequents, preserving the existing parser walker. It
706
+ preserves duplicates and decoded string values. It does not evaluate imports, exported constants,
707
+ spreads, or templates, and malformed source throws. Schema-allowlist consumers retain their own
708
+ file selection, key filtering, deduplication, snapshot comparison, and diagnostics. Parsing is
709
+ source-only and does not build a TypeScript compiler program or execute the source.
710
+
689
711
  ## SQL view declarations
690
712
 
691
713
  `vouchington-tooling/sql-ast` exports `extractViewDeclarations(sql)` and the
@@ -698,14 +720,17 @@ lifecycle decisions.
698
720
 
699
721
  ## SQL index shapes
700
722
 
701
- `vouchington-tooling/sql-ast` exports `extractIndexShapes(sql)`. Await `initSqlAst()` before
702
- calling it. It returns `{ idxname, table, shapeKey }` for each named top-level `CREATE INDEX`.
703
- Consumers can group identical `shapeKey` values to find differently named index definitions
704
- with the same projected physical shape. The key normalizes effective sort and null ordering and
705
- removes parser locations from expressions and predicates. Invalid SQL throws. Anonymous indexes
706
- and indexes inside `DO` blocks are omitted. The projection does not model operator classes,
707
- collations, or `INCLUDE` columns, so callers using those features must extend it before relying
708
- on shape collisions.
723
+ `vouchington-tooling/sql-ast` exports asynchronous `extractIndexShapes(sql)`. Await its result;
724
+ it lazily loads the optional `no-mistakes` peer and requires version 0.81.0 or newer when
725
+ called with nonblank SQL. Other SQL exports do not require this peer. It does not need
726
+ `initSqlAst()`. It returns
727
+ `{ idxname, table, shapeKey }` for each named top-level `CREATE INDEX`. Consumers can group
728
+ identical keys to detect collisions. Keys use upstream structural identity, including effective
729
+ sort/null ordering, quoted relation identity, expressions, predicates, operator classes, and
730
+ `INCLUDE` columns. Key serialization is opaque and must not be persisted across versions.
731
+ Any parse diagnostic rejects the whole input. Anonymous indexes and indexes inside `DO` blocks
732
+ are omitted. Consumer call sites must change from synchronous calls to `await` when adopting
733
+ this release.
709
734
 
710
735
  ## Publication writer inventory facts
711
736
 
@@ -0,0 +1,49 @@
1
+ /** Minimal structural facts; public declarations do not require the optional SQL parser peer. */
2
+ export type GeneratedArbiterName = {
3
+ parts: readonly {
4
+ identity: string;
5
+ }[];
6
+ };
7
+ type GeneratedArbiterRoot = {
8
+ kind: 'columnReference';
9
+ name: GeneratedArbiterName;
10
+ } | {
11
+ kind: 'parenthesized';
12
+ expression: GeneratedArbiterRoot;
13
+ } | {
14
+ kind: 'functionCall' | 'cast' | 'nullTest' | 'distinctness' | 'parameter' | 'typedLiteral' | 'literal' | 'binary' | 'unary' | 'case' | 'subquery' | 'other';
15
+ };
16
+ export type GeneratedArbiterExpression = {
17
+ columns: readonly GeneratedArbiterName[];
18
+ root: GeneratedArbiterRoot;
19
+ childrenComplete?: boolean;
20
+ };
21
+ export type GeneratedArbiterConflict = {
22
+ target: {
23
+ kind: 'columns';
24
+ columns: readonly {
25
+ identity: string;
26
+ }[];
27
+ } | {
28
+ kind: 'expressions';
29
+ expressions: readonly GeneratedArbiterExpression[];
30
+ } | {
31
+ kind: 'constraint' | 'omitted';
32
+ };
33
+ predicate: GeneratedArbiterExpression | null;
34
+ action: {
35
+ kind: 'doNothing';
36
+ } | {
37
+ kind: 'doUpdate';
38
+ assignments: readonly {
39
+ columns: readonly GeneratedArbiterName[];
40
+ expression: GeneratedArbiterExpression;
41
+ complete: boolean;
42
+ target?: {
43
+ subscripts: readonly unknown[];
44
+ indirection?: readonly unknown[];
45
+ };
46
+ }[];
47
+ };
48
+ };
49
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { GeneratedArbiterConflict } from './generated-arbiter-facts.mts';
2
+ export type GeneratedArbiterAssignmentInput = {
3
+ columns: readonly string[];
4
+ /** A direct column reference, excluding the two-part EXCLUDED namespace. */
5
+ referencedColumn: string | undefined;
6
+ hasIndirection: boolean;
7
+ complete: boolean;
8
+ };
9
+ export type GeneratedArbiterInput = {
10
+ action: 'doNothing' | 'doUpdate';
11
+ /** Undefined for named constraints, omitted keys, or unresolved expression keys. */
12
+ keyColumns: ReadonlySet<string> | undefined;
13
+ predicateColumns: ReadonlySet<string>;
14
+ assignments: readonly GeneratedArbiterAssignmentInput[];
15
+ /** False means callers must reject rather than infer replay safety from partial facts. */
16
+ complete: boolean;
17
+ };
18
+ /** Projects released SQL facts for a snapshot-dependent generated-column replay verdict.
19
+ * This reports syntax and identities only; callers own protection and safety policy.
20
+ */
21
+ export declare function projectGeneratedArbiterInput(conflict: GeneratedArbiterConflict): GeneratedArbiterInput;
@@ -0,0 +1,73 @@
1
+ /** Projects released SQL facts for a snapshot-dependent generated-column replay verdict.
2
+ * This reports syntax and identities only; callers own protection and safety policy.
3
+ */
4
+ export function projectGeneratedArbiterInput(conflict) {
5
+ let complete = true;
6
+ const predicateColumns = new Set();
7
+ if (conflict.predicate) {
8
+ complete = expressionComplete(conflict.predicate);
9
+ collectColumns(conflict.predicate, predicateColumns);
10
+ }
11
+ let keyColumns;
12
+ if (conflict.target.kind === 'columns' && conflict.target.columns.length > 0) {
13
+ keyColumns = new Set(conflict.target.columns.map((column) => column.identity));
14
+ }
15
+ else if (conflict.target.kind === 'expressions') {
16
+ const columns = new Set();
17
+ let resolved = conflict.target.expressions.length > 0;
18
+ for (const expression of conflict.target.expressions) {
19
+ const references = new Set();
20
+ collectColumns(expression, references);
21
+ const represented = expressionComplete(expression);
22
+ complete &&= represented;
23
+ if (!represented || references.size !== 1)
24
+ resolved = false;
25
+ for (const column of references)
26
+ columns.add(column);
27
+ }
28
+ if (resolved)
29
+ keyColumns = columns;
30
+ }
31
+ const assignments = conflict.action.kind === 'doUpdate'
32
+ ? conflict.action.assignments.map((assignment) => {
33
+ const columns = assignment.columns.map(lastIdentity);
34
+ const represented = assignment.complete &&
35
+ columns.every((column) => column !== undefined) &&
36
+ expressionComplete(assignment.expression);
37
+ complete &&= represented;
38
+ let root = assignment.expression.root;
39
+ while (root.kind === 'parenthesized')
40
+ root = root.expression;
41
+ return {
42
+ columns: columns.filter((column) => column !== undefined),
43
+ referencedColumn: root.kind === 'columnReference' && !isExcluded(root.name)
44
+ ? lastIdentity(root.name)
45
+ : undefined,
46
+ hasIndirection: Boolean(assignment.target &&
47
+ (assignment.target.subscripts.length > 0 ||
48
+ (assignment.target.indirection?.length ?? 0) > 0)),
49
+ complete: represented,
50
+ };
51
+ })
52
+ : [];
53
+ return { action: conflict.action.kind, keyColumns, predicateColumns, assignments, complete };
54
+ }
55
+ function lastIdentity(name) {
56
+ return name.parts.at(-1)?.identity;
57
+ }
58
+ function isExcluded(name) {
59
+ return name.parts.length === 2 && name.parts[0]?.identity === 'excluded';
60
+ }
61
+ function collectColumns(expression, columns) {
62
+ for (const name of expression.columns) {
63
+ const column = lastIdentity(name);
64
+ if (!isExcluded(name) && column !== undefined)
65
+ columns.add(column);
66
+ }
67
+ }
68
+ function expressionComplete(expression) {
69
+ // Older optional peers do not carry completeness; do not silently admit their partial facts.
70
+ return ('childrenComplete' in expression &&
71
+ expression.childrenComplete === true &&
72
+ expression.columns.every((name) => lastIdentity(name) !== undefined));
73
+ }
@@ -1,12 +1,16 @@
1
+ export type { GeneratedArbiterConflict } from './generated-arbiter-facts.mts';
2
+ export { projectGeneratedArbiterInput } from './generated-arbiter-input.mts';
3
+ export type { GeneratedArbiterInput, GeneratedArbiterAssignmentInput, } from './generated-arbiter-input.mts';
1
4
  export { buildSchemaSnapshot, buildTableSnapshots } from './build-snapshot.mts';
2
5
  export { readColumns, readConstraints, readEnums, readExtensions, readFunctions, readIndexes, readPolicies, readSchemaCatalog, readTables, readTriggers, readViews, } from './catalog-queries.mts';
3
6
  export type { CatalogColumnRow, CatalogConstraintRow, CatalogEnumValueRow, CatalogExtensionRow, CatalogFunctionRow, CatalogIndexRow, CatalogPolicyRow, CatalogTableRow, CatalogTriggerRow, CatalogViewRow, SchemaCatalog, } from './catalog-queries.mts';
4
7
  export { generateSchemaSnapshot, stableStringify, writeSchemaSnapshot } from './generate.mts';
5
8
  export { detectRenamedIndexes, indexShapeKey } from './index-rename-detect.mts';
6
9
  export type { RenamedIndex, SchemaIndexRenameSnapshot } from './index-rename-detect.mts';
7
- export { createPostgresReplayContext } from './replay-context.mts';
10
+ export { createPostgresReplayContext, createPostgresReplayContextFromSchema, } from './replay-context.mts';
8
11
  export type { GeneratedColumnReferences, PostgresReplayContext } from './replay-context.mts';
9
12
  export { renderSchemaMarkdown } from './render-markdown.mts';
10
13
  export type { SchemaMarkdownFiles } from './render-markdown.mts';
11
14
  export { renderTableDocument } from './render-tables.mts';
12
15
  export type { CatalogQuery, PartitionPolicy, SchemaColumnSnapshot, SchemaEnumSnapshot, SchemaExtensionSnapshot, SchemaForeignKeySnapshot, SchemaFunctionSnapshot, SchemaGrowthMaps, SchemaIndexKeySnapshot, SchemaIndexSnapshot, SchemaKeyConstraintSnapshot, SchemaPhysicalPartitionSnapshot, SchemaPolicySnapshot, SchemaSnapshot, SchemaTableSnapshot, SchemaViewSnapshot, } from './types.mts';
16
+ export { extractStaticStringPairs } from './static-string-pairs.mts';
@@ -1,7 +1,9 @@
1
+ export { projectGeneratedArbiterInput } from "./generated-arbiter-input.mjs";
1
2
  export { buildSchemaSnapshot, buildTableSnapshots } from "./build-snapshot.mjs";
2
3
  export { readColumns, readConstraints, readEnums, readExtensions, readFunctions, readIndexes, readPolicies, readSchemaCatalog, readTables, readTriggers, readViews, } from "./catalog-queries.mjs";
3
4
  export { generateSchemaSnapshot, stableStringify, writeSchemaSnapshot } from "./generate.mjs";
4
5
  export { detectRenamedIndexes, indexShapeKey } from "./index-rename-detect.mjs";
5
- export { createPostgresReplayContext } from "./replay-context.mjs";
6
+ export { createPostgresReplayContext, createPostgresReplayContextFromSchema, } from "./replay-context.mjs";
6
7
  export { renderSchemaMarkdown } from "./render-markdown.mjs";
7
8
  export { renderTableDocument } from "./render-tables.mjs";
9
+ export { extractStaticStringPairs } from "./static-string-pairs.mjs";
@@ -11,6 +11,8 @@ export type PostgresReplayContext = {
11
11
  /** Memoized per table; unknown tables return undefined and empty dependency sets return an empty map. */
12
12
  generatedDependenciesForTable(table: string): ReadonlyMap<string, ReadonlySet<string>> | undefined;
13
13
  };
14
+ /** Collects generated-expression facts with the optional SQL peer, once per snapshot. */
15
+ export declare function createPostgresReplayContextFromSchema(schema: SchemaSnapshot): Promise<PostgresReplayContext>;
14
16
  /**
15
17
  * Projects injected schema snapshot data and parser-derived generated-expression references for
16
18
  * replay checks. The caller owns snapshot loading and SQL parsing. Every STORED generated column
@@ -1,3 +1,38 @@
1
+ import { loadPostgresParser } from "../sql-ast/no-mistakes-peer.mjs";
2
+ /** Collects generated-expression facts with the optional SQL peer, once per snapshot. */
3
+ export async function createPostgresReplayContextFromSchema(schema) {
4
+ const definitions = Object.entries(schema.tables).flatMap(([table, snapshotTable]) => Object.entries(snapshotTable.columns).flatMap(([column, definition]) => definition.generated === 'stored' && definition.generatedExpression !== null
5
+ ? [{ table, column, expression: definition.generatedExpression }]
6
+ : []));
7
+ if (definitions.length === 0) {
8
+ return createPostgresReplayContext({ schema, generatedColumnReferences: [] });
9
+ }
10
+ const { parsePostgresSql } = await loadPostgresParser('createPostgresReplayContextFromSchema');
11
+ const parsed = await parsePostgresSql(definitions.map(({ table, column, expression }) => ({
12
+ sql: `SELECT (${expression})`,
13
+ fileName: `schema-snapshot:${table}.${column}`,
14
+ })));
15
+ const generatedColumnReferences = definitions.map(({ table, column }, index) => {
16
+ const facts = parsed[index];
17
+ const statement = facts?.statements[0];
18
+ if (!facts ||
19
+ facts.diagnostics.length > 0 ||
20
+ facts.statements.length !== 1 ||
21
+ statement?.kind !== 'select' ||
22
+ !statement.query.complete ||
23
+ statement.query.unsupported.length > 0) {
24
+ throw new Error(`Unable to collect generated-column references for ${table}.${column}`);
25
+ }
26
+ return {
27
+ table,
28
+ column,
29
+ sourceColumns: [
30
+ ...new Set(statement.query.columns.map((reference) => reference.name.parts.at(-1).identity)),
31
+ ],
32
+ };
33
+ });
34
+ return createPostgresReplayContext({ schema, generatedColumnReferences });
35
+ }
1
36
  /**
2
37
  * Projects injected schema snapshot data and parser-derived generated-expression references for
3
38
  * replay checks. The caller owns snapshot loading and SQL parsing. Every STORED generated column
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Collects the first two string literals of every source array in ESTree field traversal order.
3
+ * Order follows the existing parser walker, including conditional alternatives before consequents.
4
+ * Arrays may contain further elements. This does not evaluate constants, imports, or spreads;
5
+ * repeated pairs remain repeated so callers can apply their own selection and deduplication.
6
+ * Invalid source throws through the source-only parser.
7
+ */
8
+ export declare function extractStaticStringPairs(source: string): readonly (readonly [string, string])[];
@@ -0,0 +1,26 @@
1
+ import { isNode, parseSource, walk } from "../post-publication-inventory/readers/source-ast.mjs";
2
+ /**
3
+ * Collects the first two string literals of every source array in ESTree field traversal order.
4
+ * Order follows the existing parser walker, including conditional alternatives before consequents.
5
+ * Arrays may contain further elements. This does not evaluate constants, imports, or spreads;
6
+ * repeated pairs remain repeated so callers can apply their own selection and deduplication.
7
+ * Invalid source throws through the source-only parser.
8
+ */
9
+ export function extractStaticStringPairs(source) {
10
+ const pairs = [];
11
+ const { ast } = parseSource(source);
12
+ walk(ast, (node) => {
13
+ if (node.type !== 'ArrayExpression' || !Array.isArray(node.elements))
14
+ return;
15
+ const [first, second] = node.elements;
16
+ if (isNode(first) &&
17
+ isNode(second) &&
18
+ first.type === 'Literal' &&
19
+ second.type === 'Literal' &&
20
+ typeof first.value === 'string' &&
21
+ typeof second.value === 'string') {
22
+ pairs.push([first.value, second.value]);
23
+ }
24
+ });
25
+ return pairs;
26
+ }
@@ -1,34 +1,11 @@
1
1
  /**
2
- * Extracts a canonical "shape key" for every top-level CREATE INDEX statement in the given SQL,
3
- * for detecting index definitions that build the same physical index under different names.
4
- *
5
- * The key is built from normalized `IndexStmt` AST fields (table, uniqueness, access method, each
6
- * indexed column/expression with its *effective* sort order and null ordering, and the predicate
7
- * with position-dependent `location` fields stripped) rather than from raw source text. This
8
- * canonicalizes cosmetic syntax variance that leaves the physical index unchanged:
9
- * - implicit vs. explicit `ASC` (`SORTBY_DEFAULT` is normalized to `SORTBY_ASC`, PostgreSQL's
10
- * actual default for btree ordering);
11
- * - implicit vs. explicit `NULLS FIRST`/`NULLS LAST` (PostgreSQL's real default depends on sort
12
- * direction — `NULLS LAST` for ascending, `NULLS FIRST` for descending — so
13
- * `SORTBY_NULLS_DEFAULT` is resolved against the effective ordering rather than compared as-is);
14
- * - redundant parentheses in a predicate or expression (parentheses are pure grouping and leave no
15
- * trace in the parsed AST once `location` offsets — which shift under added parens even though
16
- * the tree shape is unchanged — are stripped before comparison).
17
- *
18
- * Operator classes, collations, and INCLUDE columns are intentionally NOT modeled; callers whose
19
- * generators use them must extend the projection before using this collision policy for those SQL.
20
- *
21
- * @libpg-query/parser has no deparse/SQL-regeneration function, so this hashes a normalized JSON
22
- * projection of the relevant AST subtrees instead of reconstructing SQL text.
23
- *
24
- * DO $$ ... END $$; blocks are opaque to the top-level parser — they appear as a single DoStmt,
25
- * not individual IndexStmt nodes — so nested CREATE INDEX statements are not covered.
26
- *
27
- * Unparseable SQL throws rather than returning an empty shape list.
28
- * Requires initSqlAst() to have resolved before the first call.
2
+ * Returns collision keys for named top-level CREATE INDEX statements using released SQL facts.
3
+ * Effective ordering, expressions, predicates, operator classes, and INCLUDE columns participate
4
+ * in the upstream structural identity. DO blocks remain opaque to this top-level projection.
5
+ * Any parse diagnostic rejects the complete input instead of returning partial collision facts.
29
6
  */
30
- export declare function extractIndexShapes(sql: string): {
7
+ export declare function extractIndexShapes(sql: string): Promise<{
31
8
  idxname: string;
32
9
  table: string;
33
10
  shapeKey: string;
34
- }[];
11
+ }[]>;
@@ -1,85 +1,28 @@
1
- import { parseSql } from "./parser.mjs";
1
+ import { loadPostgresParser } from "./no-mistakes-peer.mjs";
2
2
  /**
3
- * Extracts a canonical "shape key" for every top-level CREATE INDEX statement in the given SQL,
4
- * for detecting index definitions that build the same physical index under different names.
5
- *
6
- * The key is built from normalized `IndexStmt` AST fields (table, uniqueness, access method, each
7
- * indexed column/expression with its *effective* sort order and null ordering, and the predicate
8
- * with position-dependent `location` fields stripped) rather than from raw source text. This
9
- * canonicalizes cosmetic syntax variance that leaves the physical index unchanged:
10
- * - implicit vs. explicit `ASC` (`SORTBY_DEFAULT` is normalized to `SORTBY_ASC`, PostgreSQL's
11
- * actual default for btree ordering);
12
- * - implicit vs. explicit `NULLS FIRST`/`NULLS LAST` (PostgreSQL's real default depends on sort
13
- * direction — `NULLS LAST` for ascending, `NULLS FIRST` for descending — so
14
- * `SORTBY_NULLS_DEFAULT` is resolved against the effective ordering rather than compared as-is);
15
- * - redundant parentheses in a predicate or expression (parentheses are pure grouping and leave no
16
- * trace in the parsed AST once `location` offsets — which shift under added parens even though
17
- * the tree shape is unchanged — are stripped before comparison).
18
- *
19
- * Operator classes, collations, and INCLUDE columns are intentionally NOT modeled; callers whose
20
- * generators use them must extend the projection before using this collision policy for those SQL.
21
- *
22
- * @libpg-query/parser has no deparse/SQL-regeneration function, so this hashes a normalized JSON
23
- * projection of the relevant AST subtrees instead of reconstructing SQL text.
24
- *
25
- * DO $$ ... END $$; blocks are opaque to the top-level parser — they appear as a single DoStmt,
26
- * not individual IndexStmt nodes — so nested CREATE INDEX statements are not covered.
27
- *
28
- * Unparseable SQL throws rather than returning an empty shape list.
29
- * Requires initSqlAst() to have resolved before the first call.
3
+ * Returns collision keys for named top-level CREATE INDEX statements using released SQL facts.
4
+ * Effective ordering, expressions, predicates, operator classes, and INCLUDE columns participate
5
+ * in the upstream structural identity. DO blocks remain opaque to this top-level projection.
6
+ * Any parse diagnostic rejects the complete input instead of returning partial collision facts.
30
7
  */
31
- export function extractIndexShapes(sql) {
8
+ export async function extractIndexShapes(sql) {
32
9
  if (!sql.trim())
33
10
  return [];
34
- const result = parseSql(sql);
35
- const shapes = [];
36
- for (const stmt of result.stmts ?? []) {
37
- const node = stmt.stmt;
38
- if (!node || !('IndexStmt' in node))
39
- continue;
40
- const indexStmt = node.IndexStmt;
41
- const idxname = indexStmt.idxname;
42
- if (!idxname)
43
- continue;
44
- const table = indexStmt.relation?.relname ?? '';
45
- const canonical = {
46
- table,
47
- unique: Boolean(indexStmt.unique),
48
- accessMethod: indexStmt.accessMethod ?? 'btree',
49
- indexParams: (indexStmt.indexParams ?? []).map(normalizeIndexParam),
50
- whereClause: indexStmt.whereClause ? stripLocations(indexStmt.whereClause) : null,
51
- };
52
- shapes.push({ idxname, table, shapeKey: JSON.stringify(canonical) });
11
+ const { parsePostgresSql } = await loadPostgresParser('extractIndexShapes');
12
+ const facts = await parsePostgresSql({ sql });
13
+ if (facts.diagnostics.length > 0) {
14
+ throw new Error(`SQL syntax error: ${facts.diagnostics.map(({ message }) => message).join('; ')}`);
53
15
  }
54
- return shapes;
55
- }
56
- function normalizeIndexParam(param) {
57
- if (!('IndexElem' in param))
58
- return stripLocations(param);
59
- const elem = param.IndexElem;
60
- const ordering = elem.ordering === undefined || elem.ordering === 'SORTBY_DEFAULT' ? 'SORTBY_ASC' : elem.ordering;
61
- const nullsOrdering = elem.nulls_ordering === undefined || elem.nulls_ordering === 'SORTBY_NULLS_DEFAULT'
62
- ? ordering === 'SORTBY_DESC'
63
- ? 'SORTBY_NULLS_FIRST'
64
- : 'SORTBY_NULLS_LAST'
65
- : elem.nulls_ordering;
66
- return {
67
- key: elem.name ?? stripLocations(elem.expr),
68
- ordering,
69
- nullsOrdering,
70
- };
71
- }
72
- function stripLocations(value) {
73
- if (Array.isArray(value))
74
- return value.map(stripLocations);
75
- if (value && typeof value === 'object') {
76
- const out = {};
77
- for (const [key, val] of Object.entries(value)) {
78
- if (key === 'location')
79
- continue;
80
- out[key] = stripLocations(val);
81
- }
82
- return out;
83
- }
84
- return value;
16
+ return facts.statements.flatMap((statement) => {
17
+ if (statement.kind !== 'createIndex' || !statement.index.name)
18
+ return [];
19
+ const { index } = statement;
20
+ return [
21
+ {
22
+ idxname: index.name.parts.at(-1).value,
23
+ table: index.table.parts.at(-1).value,
24
+ shapeKey: index.structuralIdentity,
25
+ },
26
+ ];
27
+ });
85
28
  }
@@ -0,0 +1,86 @@
1
+ export declare function loadPostgresParser(caller: string): Promise<{
2
+ default: typeof import("no-mistakes");
3
+ analyzeTypeScriptModules(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TypeScriptModulesOptions>): Promise<import("no-mistakes").TypeScriptModulesReport>;
4
+ parsePostgresSql(source: import("no-mistakes").PostgresSqlSource): Promise<import("no-mistakes").PostgresSqlFacts>;
5
+ parsePostgresSql(sources: import("no-mistakes").PostgresSqlSource[]): Promise<import("no-mistakes").PostgresSqlFacts[]>;
6
+ dependencies(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TraversePathsOptions>): Promise<import("no-mistakes").ImportClosureResult>;
7
+ dependencies(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TraverseGraphOptions>): Promise<import("no-mistakes").DependencyResult>;
8
+ dependents(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TraverseOptions>): Promise<import("no-mistakes").DependencyResult>;
9
+ related(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TraverseOptions>): Promise<import("no-mistakes").DependencyResult>;
10
+ analyzeProject(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").AnalyzeProjectOptions>): Promise<import("no-mistakes").AnalyzeProjectResult>;
11
+ writePlanningImpactArtifacts(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").WritePlanningImpactArtifactsOptions>): Promise<import("no-mistakes").PlanningImpactArtifacts>;
12
+ symbols(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").SymbolsSignatureImpactOptions>): Promise<import("no-mistakes").SignatureImpactResult>;
13
+ symbols(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").SymbolsListOptions>): Promise<import("no-mistakes").SymbolsResult>;
14
+ symbols(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").SymbolsOptions>): Promise<import("no-mistakes").SymbolsResult | import("no-mistakes").SignatureImpactResult>;
15
+ importUsages(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ImportUsagesOptions>): Promise<import("no-mistakes").ImportUsagesResult>;
16
+ importers(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ImportersOptions>): Promise<import("no-mistakes").ImportersResult>;
17
+ exportsOf(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ExportsOfOptions>): Promise<import("no-mistakes").ExportsOfResult>;
18
+ deadExports(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").DeadExportsOptions>): Promise<import("no-mistakes").DeadExportsResult>;
19
+ callSites(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").CallSitesOptions>): Promise<import("no-mistakes").CallSitesResult>;
20
+ resolveCheck(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ResolveCheckOptions>): Promise<import("no-mistakes").ResolveCheckResult>;
21
+ resolveCheck(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ResolveCheckFilesOptions>): Promise<import("no-mistakes").ResolveCheckBatchResult>;
22
+ fetches(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").FetchesOptions>): Promise<import("no-mistakes").FetchReport>;
23
+ flow(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").FlowOptions>): Promise<import("no-mistakes").FlowReport>;
24
+ check(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").CheckOptions>): Promise<import("no-mistakes").CheckReport>;
25
+ resolveConfig(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").ResolvedConfig>;
26
+ validateMermaidMarkdown(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").MermaidValidationOptions>): Promise<import("no-mistakes").MermaidValidationResult>;
27
+ testsPlan(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsPlanOptions>): Promise<import("no-mistakes").TestPlan>;
28
+ testsImpact(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsImpactOptions>): Promise<import("no-mistakes").TestPlan>;
29
+ testsTargets(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsTargetsOptions>): Promise<import("no-mistakes").TestsTargetsReport>;
30
+ testsWhy(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsWhyOptions>): Promise<Record<string, import("no-mistakes").WhyStep[]>>;
31
+ testsComment(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsPlanDocumentOptions>): Promise<string>;
32
+ testsGraph(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsPlanDocumentOptions>): Promise<import("no-mistakes").TestGraph>;
33
+ testsGraphMermaid(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").TestsPlanDocumentOptions>): Promise<string>;
34
+ playwrightCheck(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PlaywrightOptions>): Promise<unknown>;
35
+ playwrightEdges(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PlaywrightOptions>): Promise<unknown>;
36
+ playwrightRelated(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PlaywrightRelatedOptions>): Promise<unknown>;
37
+ playwrightTests(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PlaywrightOptions>): Promise<unknown>;
38
+ queues(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").QueueReport>;
39
+ queueEdges(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").QueueEdge[]>;
40
+ queueRelated(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").QueueEdge[]>;
41
+ queueCheck(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").QueueCheckFinding[]>;
42
+ serverRoutes(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").ServerRoutesReport>;
43
+ serverRouteList(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<unknown[]>;
44
+ serverRouteEdges(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").GraphEdge[]>;
45
+ serverRouteRelated(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").GraphEdge[]>;
46
+ serverContracts(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").ServerContractsReport>;
47
+ reactAnalyze(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").ReactComponentFacts[]>;
48
+ reactCheck(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions>): Promise<import("no-mistakes").ReactViolation[]>;
49
+ reactUsages(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ProjectOptions & {
50
+ target: string;
51
+ }>): Promise<import("no-mistakes").ReactUsagesReport>;
52
+ generatePostgresCatalog(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PostgresCatalogOptions & {
53
+ coverage: "ordering";
54
+ }>): Promise<import("no-mistakes").PostgresOrderingCatalog>;
55
+ generatePostgresCatalog(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PostgresCatalogOptions & {
56
+ coverage?: "complete";
57
+ }>): Promise<import("no-mistakes").PostgresCompleteCatalog>;
58
+ generatePostgresCatalog(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").PostgresCatalogOptions>): Promise<import("no-mistakes").PostgresCatalog>;
59
+ lockfileDiff(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").LockfileDiffOptions>): Promise<import("no-mistakes").LockfileDiffEntry[]>;
60
+ ciImpact(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").CiImpactOptions>): Promise<import("no-mistakes").CiImpactReport>;
61
+ ciEnv(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").CiEnvOptions>): Promise<import("no-mistakes").CiEnvReport>;
62
+ ciTopology(options?: import("no-mistakes").WithInvocationOptions<import("no-mistakes").CiTopologyOptions>): Promise<import("no-mistakes").WorkflowTopology>;
63
+ ciTopologyImpact(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").CiTopologyImpactOptions>): Promise<import("no-mistakes").CiTopologyImpactReport>;
64
+ createWorkflowTopologyIndex(topology: import("no-mistakes").WorkflowTopology): import("no-mistakes").WorkflowTopologyIndex;
65
+ impactedChecks(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").ImpactedChecksOptions>): Promise<import("no-mistakes").ImpactedChecksReport>;
66
+ dataPw(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").DataPwOptions>): Promise<import("no-mistakes").DataPwReport>;
67
+ effects(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").EffectsOptions>): Promise<import("no-mistakes").EffectsReport>;
68
+ rscCallers(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").RscCallersOptions>): Promise<import("no-mistakes").RscCallersReport>;
69
+ registryExtension(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").RegistryExtensionOptions>): Promise<import("no-mistakes").RegistryExtensionReport>;
70
+ infraResourceRefs(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").InfraOptions & {
71
+ address: string;
72
+ }>): Promise<import("no-mistakes").ResourceRefRow[]>;
73
+ infraOutputs(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").InfraOptions & {
74
+ moduleDir: string;
75
+ }>): Promise<import("no-mistakes").ModuleOutputsResult>;
76
+ infraTestFor(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").InfraOptions & {
77
+ tfFile: string;
78
+ }>): Promise<import("no-mistakes").TestForRow[]>;
79
+ swiftImporters(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").SwiftOptions & {
80
+ file: string;
81
+ }>): Promise<import("no-mistakes").SwiftImporterRow[]>;
82
+ swiftTestTargets(options: import("no-mistakes").WithInvocationOptions<import("no-mistakes").SwiftOptions & {
83
+ file: string;
84
+ }>): Promise<import("no-mistakes").SwiftTestTargetRow[]>;
85
+ version(): Promise<string>;
86
+ }>;
@@ -0,0 +1,13 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ export async function loadPostgresParser(caller) {
3
+ const entry = import.meta.resolve('no-mistakes');
4
+ const metadata = JSON.parse(await readFile(new URL('package.json', entry), 'utf8'));
5
+ const version = /^(\d+)\.(\d+)\.(\d+)(-[\w.-]+)?(?:\+[\w.-]+)?$/.exec(metadata.version);
6
+ const major = Number(version?.[1]);
7
+ const minor = Number(version?.[2]);
8
+ const patch = Number(version?.[3]);
9
+ if (!version || (major === 0 && (minor < 81 || (minor === 81 && patch === 0 && version[4])))) {
10
+ throw new Error(`${caller} requires no-mistakes >=0.81.0`);
11
+ }
12
+ return import('no-mistakes');
13
+ }
@@ -1,5 +1,11 @@
1
1
  # PostgreSQL replay context
2
2
 
3
+ `await createPostgresReplayContextFromSchema(schema)` collects STORED generated-expression
4
+ references using the optional `no-mistakes >=0.81.0` SQL peer, then returns the memoized replay context.
5
+ It rejects malformed expressions, additional statements, and unsupported queries instead of
6
+ treating missing facts as an empty dependency set. Snapshots without STORED expressions do not
7
+ load the peer. An incompatible peer is rejected before loading its parser. Snapshot loading and INSERT policy remain the caller's responsibility.
8
+
3
9
  `createPostgresReplayContext` projects a caller-owned schema snapshot and generated-column
4
10
  reference facts into the two lookups used by replay checks. It does not load snapshots, parse SQL,
5
11
  or decide INSERT policy.
@@ -7,7 +13,7 @@ or decide INSERT policy.
7
13
  The caller supplies one `{ table, column, sourceColumns }` fact for each snapshot column whose
8
14
  `generated` value is `stored` and whose `generatedExpression` is present. Build `sourceColumns` from
9
15
  the parser's typed expression column references, retaining each reference's final identifier
10
- component. Supply an empty array for a valid expression with no column references. Missing facts
16
+ identity (unquoted names fold to lowercase; quoted names preserve case). Supply an empty array for a valid expression with no column references. Missing facts
11
17
  throw when that table's dependencies are requested, so parser failures cannot silently become an
12
18
  empty dependency set.
13
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.38.1",
3
+ "version": "0.40.0",
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": {
@@ -380,14 +380,14 @@
380
380
  "@modelcontextprotocol/sdk": "1.31.0",
381
381
  "@types/picomatch": "^4.0.3",
382
382
  "@types/spdx-expression-parse": "4.0.0",
383
- "agent-blackboard": "^0.6.0",
384
- "no-mistakes": "0.78.0",
383
+ "agent-blackboard": "^0.6.1",
384
+ "no-mistakes": "0.83.0",
385
385
  "smol-toml": "1.9.0",
386
386
  "typescript5": "npm:typescript@5.9.3"
387
387
  },
388
388
  "peerDependencies": {
389
389
  "@modelcontextprotocol/sdk": "^1.31.0",
390
- "agent-blackboard": "^0.6.0",
390
+ "agent-blackboard": "^0.6.1",
391
391
  "no-mistakes": "*",
392
392
  "typescript": ">=5",
393
393
  "vitest": ">=5"