vouchington-tooling 0.0.9 → 0.0.11

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 (111) hide show
  1. package/README.md +21 -1
  2. package/dist/cli/commands/gha-artifacts-cleanup.d.mts +6 -0
  3. package/dist/cli/commands/gha-artifacts-cleanup.mjs +50 -0
  4. package/dist/cli/commands/http-origin.d.mts +2 -0
  5. package/dist/cli/commands/http-origin.mjs +14 -0
  6. package/dist/cli/index.mjs +11 -0
  7. package/dist/cli/parse-gha-artifacts-cleanup.d.mts +15 -0
  8. package/dist/cli/parse-gha-artifacts-cleanup.mjs +74 -0
  9. package/dist/cli/parse.d.mts +7 -2
  10. package/dist/cli/parse.mjs +36 -0
  11. package/dist/cli/usage.d.mts +1 -1
  12. package/dist/cli/usage.mjs +9 -0
  13. package/dist/gha-artifacts-cleanup/classify.d.mts +14 -0
  14. package/dist/gha-artifacts-cleanup/classify.mjs +32 -0
  15. package/dist/gha-artifacts-cleanup/commands.d.mts +23 -0
  16. package/dist/gha-artifacts-cleanup/commands.mjs +55 -0
  17. package/dist/gha-artifacts-cleanup/github.d.mts +16 -0
  18. package/dist/gha-artifacts-cleanup/github.mjs +100 -0
  19. package/dist/gha-artifacts-cleanup/index.d.mts +8 -0
  20. package/dist/gha-artifacts-cleanup/index.mjs +4 -0
  21. package/dist/gha-artifacts-cleanup/plan.d.mts +17 -0
  22. package/dist/gha-artifacts-cleanup/plan.mjs +40 -0
  23. package/dist/gha-selected-files/index.d.mts +6 -0
  24. package/dist/gha-selected-files/index.mjs +38 -0
  25. package/dist/http-origin/index.d.mts +1 -0
  26. package/dist/http-origin/index.mjs +23 -0
  27. package/dist/index.d.mts +11 -2
  28. package/dist/index.mjs +7 -1
  29. package/dist/openapi-document/build-openapi-document.d.mts +11 -0
  30. package/dist/openapi-document/build-openapi-document.mjs +122 -0
  31. package/dist/openapi-document/build-openapi-query.d.mts +3 -0
  32. package/dist/openapi-document/build-openapi-query.mjs +53 -0
  33. package/dist/openapi-document/build-openapi-request.d.mts +7 -0
  34. package/dist/openapi-document/build-openapi-request.mjs +29 -0
  35. package/dist/openapi-document/build-openapi-response.d.mts +9 -0
  36. package/dist/openapi-document/build-openapi-response.mjs +111 -0
  37. package/dist/openapi-document/component-registry.d.mts +9 -0
  38. package/dist/openapi-document/component-registry.mjs +49 -0
  39. package/dist/openapi-document/contract-schema-canonical.d.mts +4 -0
  40. package/dist/openapi-document/contract-schema-canonical.mjs +23 -0
  41. package/dist/openapi-document/contract-schema-intersection-merge.d.mts +13 -0
  42. package/dist/openapi-document/contract-schema-intersection-merge.mjs +96 -0
  43. package/dist/openapi-document/contract-schema-to-openapi.d.mts +7 -0
  44. package/dist/openapi-document/contract-schema-to-openapi.mjs +71 -0
  45. package/dist/openapi-document/contract-schema-types.d.mts +43 -0
  46. package/dist/openapi-document/contract-schema-types.mjs +1 -0
  47. package/dist/openapi-document/index.d.mts +19 -0
  48. package/dist/openapi-document/index.mjs +12 -0
  49. package/dist/openapi-document/openapi-error-response.d.mts +2 -0
  50. package/dist/openapi-document/openapi-error-response.mjs +13 -0
  51. package/dist/openapi-document/openapi-route-helpers.d.mts +9 -0
  52. package/dist/openapi-document/openapi-route-helpers.mjs +61 -0
  53. package/dist/openapi-document/openapi-types.d.mts +80 -0
  54. package/dist/openapi-document/openapi-types.mjs +1 -0
  55. package/dist/openapi-document/operation-types.d.mts +35 -0
  56. package/dist/openapi-document/operation-types.mjs +11 -0
  57. package/dist/openapi-document/query-types.d.mts +35 -0
  58. package/dist/openapi-document/query-types.mjs +1 -0
  59. package/dist/openapi-document/schema-node-builders.d.mts +3 -0
  60. package/dist/openapi-document/schema-node-builders.mjs +6 -0
  61. package/dist/openapi-document/write-openapi.d.mts +9 -0
  62. package/dist/openapi-document/write-openapi.mjs +19 -0
  63. package/dist/pg-schema-snapshot/build-snapshot.d.mts +9 -0
  64. package/dist/pg-schema-snapshot/build-snapshot.mjs +61 -0
  65. package/dist/pg-schema-snapshot/catalog-objects.d.mts +34 -0
  66. package/dist/pg-schema-snapshot/catalog-objects.mjs +64 -0
  67. package/dist/pg-schema-snapshot/catalog-queries.d.mts +20 -0
  68. package/dist/pg-schema-snapshot/catalog-queries.mjs +31 -0
  69. package/dist/pg-schema-snapshot/catalog-shared.d.mts +3 -0
  70. package/dist/pg-schema-snapshot/catalog-shared.mjs +19 -0
  71. package/dist/pg-schema-snapshot/catalog-table-constraints.d.mts +14 -0
  72. package/dist/pg-schema-snapshot/catalog-table-constraints.mjs +54 -0
  73. package/dist/pg-schema-snapshot/catalog-table-indexes.d.mts +30 -0
  74. package/dist/pg-schema-snapshot/catalog-table-indexes.mjs +81 -0
  75. package/dist/pg-schema-snapshot/catalog-tables.d.mts +22 -0
  76. package/dist/pg-schema-snapshot/catalog-tables.mjs +48 -0
  77. package/dist/pg-schema-snapshot/file-safety.d.mts +4 -0
  78. package/dist/pg-schema-snapshot/file-safety.mjs +71 -0
  79. package/dist/pg-schema-snapshot/generate.d.mts +18 -0
  80. package/dist/pg-schema-snapshot/generate.mjs +95 -0
  81. package/dist/pg-schema-snapshot/index-rename-detect.d.mts +40 -0
  82. package/dist/pg-schema-snapshot/index-rename-detect.mjs +71 -0
  83. package/dist/pg-schema-snapshot/index.d.mts +10 -0
  84. package/dist/pg-schema-snapshot/index.mjs +6 -0
  85. package/dist/pg-schema-snapshot/markdown-files.d.mts +1 -0
  86. package/dist/pg-schema-snapshot/markdown-files.mjs +26 -0
  87. package/dist/pg-schema-snapshot/render-markdown.d.mts +8 -0
  88. package/dist/pg-schema-snapshot/render-markdown.mjs +113 -0
  89. package/dist/pg-schema-snapshot/render-tables.d.mts +2 -0
  90. package/dist/pg-schema-snapshot/render-tables.mjs +55 -0
  91. package/dist/pg-schema-snapshot/snapshot-build-utils.d.mts +2 -0
  92. package/dist/pg-schema-snapshot/snapshot-build-utils.mjs +15 -0
  93. package/dist/pg-schema-snapshot/snapshot.test-helpers.d.mts +11 -0
  94. package/dist/pg-schema-snapshot/snapshot.test-helpers.mjs +101 -0
  95. package/dist/pg-schema-snapshot/table-snapshot-columns.d.mts +3 -0
  96. package/dist/pg-schema-snapshot/table-snapshot-columns.mjs +33 -0
  97. package/dist/pg-schema-snapshot/table-snapshot-constraints.d.mts +7 -0
  98. package/dist/pg-schema-snapshot/table-snapshot-constraints.mjs +55 -0
  99. package/dist/pg-schema-snapshot/table-snapshot.d.mts +10 -0
  100. package/dist/pg-schema-snapshot/table-snapshot.mjs +99 -0
  101. package/dist/pg-schema-snapshot/types.d.mts +103 -0
  102. package/dist/pg-schema-snapshot/types.mjs +1 -0
  103. package/dist/process-line-buffer/index.d.mts +7 -0
  104. package/dist/process-line-buffer/index.mjs +28 -0
  105. package/dist/shared-context/{fake-git.test-helpers.d.mts → fake-git.d.mts} +1 -0
  106. package/dist/shared-context/{fake-git.test-helpers.mjs → fake-git.mjs} +4 -1
  107. package/dist/shared-context/index.d.mts +2 -0
  108. package/dist/shared-context/index.mjs +1 -0
  109. package/package.json +37 -1
  110. package/scripts/gha/diagnose-port-collision.sh +232 -0
  111. package/scripts/gha/prepare-trivy-db.sh +32 -0
@@ -0,0 +1,34 @@
1
+ import type { CatalogQuery } from './types.mts';
2
+ export type CatalogEnumValueRow = {
3
+ enum_name: string;
4
+ value: string;
5
+ sort_order: number;
6
+ };
7
+ export declare function readEnums(query: CatalogQuery): Promise<CatalogEnumValueRow[]>;
8
+ export type CatalogViewRow = {
9
+ view_name: string;
10
+ materialized: boolean;
11
+ definition: string;
12
+ comment: string | null;
13
+ };
14
+ export declare function readViews(query: CatalogQuery): Promise<CatalogViewRow[]>;
15
+ export type CatalogExtensionRow = {
16
+ extension_name: string;
17
+ version: string;
18
+ };
19
+ export declare function readExtensions(query: CatalogQuery): Promise<CatalogExtensionRow[]>;
20
+ export type CatalogFunctionRow = {
21
+ function_name: string;
22
+ identity_arguments: string;
23
+ definition: string;
24
+ };
25
+ export declare function readFunctions(query: CatalogQuery): Promise<CatalogFunctionRow[]>;
26
+ export type CatalogPolicyRow = {
27
+ table_name: string;
28
+ policy_name: string;
29
+ command: string;
30
+ pg_roles: string[];
31
+ using_expression: string | null;
32
+ with_check_expression: string | null;
33
+ };
34
+ export declare function readPolicies(query: CatalogQuery): Promise<CatalogPolicyRow[]>;
@@ -0,0 +1,64 @@
1
+ import { EXCLUDE_EXTENSION_OWNED } from './catalog-shared.mjs';
2
+ export async function readEnums(query) {
3
+ const { rows } = await query(`/* readSchemaSnapshotEnums */
4
+ SELECT
5
+ target.typname AS enum_name,
6
+ pg_enum.enumlabel AS value,
7
+ pg_enum.enumsortorder AS sort_order
8
+ FROM pg_enum
9
+ JOIN pg_type target ON target.oid = pg_enum.enumtypid
10
+ JOIN pg_namespace ON pg_namespace.oid = target.typnamespace
11
+ WHERE pg_namespace.nspname = 'public'
12
+ AND ${EXCLUDE_EXTENSION_OWNED}
13
+ ORDER BY enum_name, sort_order`);
14
+ return rows;
15
+ }
16
+ export async function readViews(query) {
17
+ const { rows } = await query(`/* readSchemaSnapshotViews */
18
+ SELECT
19
+ target.relname AS view_name,
20
+ target.relkind = 'm' AS materialized,
21
+ pg_get_viewdef(target.oid) AS definition,
22
+ obj_description(target.oid, 'pg_class') AS comment
23
+ FROM pg_class target
24
+ JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
25
+ WHERE pg_namespace.nspname = 'public'
26
+ AND target.relkind = ANY($1)
27
+ AND ${EXCLUDE_EXTENSION_OWNED}
28
+ ORDER BY view_name`, [['v', 'm']]);
29
+ return rows;
30
+ }
31
+ export async function readExtensions(query) {
32
+ const { rows } = await query(`/* readSchemaSnapshotExtensions */
33
+ SELECT extname AS extension_name, extversion AS version
34
+ FROM pg_extension
35
+ ORDER BY extension_name`);
36
+ return rows;
37
+ }
38
+ export async function readFunctions(query) {
39
+ const { rows } = await query(`/* readSchemaSnapshotFunctions */
40
+ SELECT
41
+ target.proname AS function_name,
42
+ pg_get_function_identity_arguments(target.oid) AS identity_arguments,
43
+ pg_get_functiondef(target.oid) AS definition
44
+ FROM pg_proc target
45
+ JOIN pg_namespace ON pg_namespace.oid = target.pronamespace
46
+ WHERE pg_namespace.nspname = 'public'
47
+ AND ${EXCLUDE_EXTENSION_OWNED}
48
+ ORDER BY function_name, identity_arguments`);
49
+ return rows;
50
+ }
51
+ export async function readPolicies(query) {
52
+ const { rows } = await query(`/* readSchemaSnapshotPolicies */
53
+ SELECT
54
+ tablename AS table_name,
55
+ policyname AS policy_name,
56
+ cmd AS command,
57
+ roles AS pg_roles,
58
+ qual AS using_expression,
59
+ with_check AS with_check_expression
60
+ FROM pg_policies
61
+ WHERE schemaname = 'public'
62
+ ORDER BY table_name, policy_name`);
63
+ return rows;
64
+ }
@@ -0,0 +1,20 @@
1
+ import { type CatalogEnumValueRow, type CatalogExtensionRow, type CatalogFunctionRow, type CatalogPolicyRow, type CatalogViewRow, readEnums, readExtensions, readFunctions, readPolicies, readViews } from './catalog-objects.mts';
2
+ import { type CatalogConstraintRow, readConstraints } from './catalog-table-constraints.mts';
3
+ import { type CatalogIndexRow, type CatalogTriggerRow, readIndexes, readTriggers } from './catalog-table-indexes.mts';
4
+ import { type CatalogColumnRow, type CatalogTableRow, readColumns, readTables } from './catalog-tables.mts';
5
+ import type { CatalogQuery } from './types.mts';
6
+ export { readColumns, readConstraints, readEnums, readExtensions, readFunctions, readIndexes, readPolicies, readTables, readTriggers, readViews, };
7
+ export type { CatalogColumnRow, CatalogConstraintRow, CatalogEnumValueRow, CatalogExtensionRow, CatalogFunctionRow, CatalogIndexRow, CatalogPolicyRow, CatalogTableRow, CatalogTriggerRow, CatalogViewRow, };
8
+ export type SchemaCatalog = {
9
+ tables: CatalogTableRow[];
10
+ columns: CatalogColumnRow[];
11
+ constraints: CatalogConstraintRow[];
12
+ indexes: CatalogIndexRow[];
13
+ triggers: CatalogTriggerRow[];
14
+ enums: CatalogEnumValueRow[];
15
+ views: CatalogViewRow[];
16
+ extensions: CatalogExtensionRow[];
17
+ functions: CatalogFunctionRow[];
18
+ policies: CatalogPolicyRow[];
19
+ };
20
+ export declare function readSchemaCatalog(query: CatalogQuery): Promise<SchemaCatalog>;
@@ -0,0 +1,31 @@
1
+ import { readEnums, readExtensions, readFunctions, readPolicies, readViews, } from './catalog-objects.mjs';
2
+ import { readConstraints } from './catalog-table-constraints.mjs';
3
+ import { readIndexes, readTriggers, } from './catalog-table-indexes.mjs';
4
+ import { readColumns, readTables, } from './catalog-tables.mjs';
5
+ export { readColumns, readConstraints, readEnums, readExtensions, readFunctions, readIndexes, readPolicies, readTables, readTriggers, readViews, };
6
+ export async function readSchemaCatalog(query) {
7
+ const [tables, columns, constraints, indexes, triggers, enums, views, extensions, functions, policies,] = await Promise.all([
8
+ readTables(query),
9
+ readColumns(query),
10
+ readConstraints(query),
11
+ readIndexes(query),
12
+ readTriggers(query),
13
+ readEnums(query),
14
+ readViews(query),
15
+ readExtensions(query),
16
+ readFunctions(query),
17
+ readPolicies(query),
18
+ ]);
19
+ return {
20
+ tables,
21
+ columns,
22
+ constraints,
23
+ indexes,
24
+ triggers,
25
+ enums,
26
+ views,
27
+ extensions,
28
+ functions,
29
+ policies,
30
+ };
31
+ }
@@ -0,0 +1,3 @@
1
+ export declare const TABLE_RELKINDS: string[];
2
+ export declare const EXCLUDE_EXTENSION_OWNED = "NOT EXISTS (\n SELECT 1 FROM pg_depend\n WHERE pg_depend.objid = target.oid AND pg_depend.deptype = 'e'\n )";
3
+ export declare const EXCLUDE_PARTITION_CHILDREN = "NOT EXISTS (\n SELECT 1 FROM pg_inherits\n WHERE pg_inherits.inhrelid = target.oid\n )";
@@ -0,0 +1,19 @@
1
+ // Ordinary tables ('r') and partitioned-table parents ('p'). Partition children always inherit
2
+ // from a parent (pg_inherits), so every query below excludes rows owned by an inhrelid — that is
3
+ // what keeps date-derived partition children (e.g. `items__p_2026_07`) out of a snapshot that must
4
+ // be identical regardless of when it was generated. Constraints need the additional conparentid
5
+ // filter in catalog-table-constraints.mts because PostgreSQL can derive a row that remains owned
6
+ // by a logical parent while referencing a physical child.
7
+ export const TABLE_RELKINDS = ['r', 'p'];
8
+ // `NOT EXISTS (SELECT 1 FROM pg_depend WHERE objid = <oid> AND deptype = 'e')` excludes objects
9
+ // owned by an extension (e.g. the `pg_stat_statements`/`pg_stat_statements_info` views created by
10
+ // the pg_stat_statements extension in the public schema). Without it, extension upgrades would
11
+ // move the snapshot even though no application DDL changed.
12
+ export const EXCLUDE_EXTENSION_OWNED = `NOT EXISTS (
13
+ SELECT 1 FROM pg_depend
14
+ WHERE pg_depend.objid = target.oid AND pg_depend.deptype = 'e'
15
+ )`;
16
+ export const EXCLUDE_PARTITION_CHILDREN = `NOT EXISTS (
17
+ SELECT 1 FROM pg_inherits
18
+ WHERE pg_inherits.inhrelid = target.oid
19
+ )`;
@@ -0,0 +1,14 @@
1
+ import type { CatalogQuery } from './types.mts';
2
+ export type CatalogConstraintRow = {
3
+ table_name: string;
4
+ constraint_name: string;
5
+ contype: 'c' | 'f' | 'p' | 'u';
6
+ definition: string;
7
+ columns: string[];
8
+ referenced_table: string | null;
9
+ referenced_columns: string[];
10
+ on_update: 'no action' | 'restrict' | 'cascade' | 'set null' | 'set default' | null;
11
+ on_delete: 'no action' | 'restrict' | 'cascade' | 'set null' | 'set default' | null;
12
+ validated: boolean;
13
+ };
14
+ export declare function readConstraints(query: CatalogQuery): Promise<CatalogConstraintRow[]>;
@@ -0,0 +1,54 @@
1
+ import { EXCLUDE_EXTENSION_OWNED, EXCLUDE_PARTITION_CHILDREN, TABLE_RELKINDS, } from './catalog-shared.mjs';
2
+ export async function readConstraints(query) {
3
+ const { rows } = await query(`/* readSchemaSnapshotConstraints */
4
+ SELECT
5
+ target.relname AS table_name,
6
+ pg_constraint.conname AS constraint_name,
7
+ pg_constraint.contype AS contype,
8
+ pg_get_constraintdef(pg_constraint.oid) AS definition,
9
+ to_jsonb(ARRAY(
10
+ SELECT pg_attribute.attname
11
+ FROM unnest(pg_constraint.conkey) WITH ORDINALITY AS key_column(attnum, position)
12
+ JOIN pg_attribute
13
+ ON pg_attribute.attrelid = target.oid AND pg_attribute.attnum = key_column.attnum
14
+ ORDER BY key_column.position
15
+ )) AS columns,
16
+ referenced.relname AS referenced_table,
17
+ to_jsonb(ARRAY(
18
+ SELECT pg_attribute.attname
19
+ FROM unnest(pg_constraint.confkey) WITH ORDINALITY AS key_column(attnum, position)
20
+ JOIN pg_attribute
21
+ ON pg_attribute.attrelid = referenced.oid
22
+ AND pg_attribute.attnum = key_column.attnum
23
+ ORDER BY key_column.position
24
+ )) AS referenced_columns,
25
+ CASE pg_constraint.confupdtype
26
+ WHEN 'a' THEN 'no action'
27
+ WHEN 'r' THEN 'restrict'
28
+ WHEN 'c' THEN 'cascade'
29
+ WHEN 'n' THEN 'set null'
30
+ WHEN 'd' THEN 'set default'
31
+ ELSE NULL
32
+ END AS on_update,
33
+ CASE pg_constraint.confdeltype
34
+ WHEN 'a' THEN 'no action'
35
+ WHEN 'r' THEN 'restrict'
36
+ WHEN 'c' THEN 'cascade'
37
+ WHEN 'n' THEN 'set null'
38
+ WHEN 'd' THEN 'set default'
39
+ ELSE NULL
40
+ END AS on_delete,
41
+ pg_constraint.convalidated AS validated
42
+ FROM pg_constraint
43
+ JOIN pg_class target ON target.oid = pg_constraint.conrelid
44
+ JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
45
+ LEFT JOIN pg_class referenced ON referenced.oid = pg_constraint.confrelid
46
+ WHERE pg_namespace.nspname = 'public'
47
+ AND target.relkind = ANY($1)
48
+ AND pg_constraint.contype = ANY($2)
49
+ AND pg_constraint.conparentid = 0
50
+ AND ${EXCLUDE_PARTITION_CHILDREN}
51
+ AND ${EXCLUDE_EXTENSION_OWNED}
52
+ ORDER BY table_name, contype, constraint_name`, [TABLE_RELKINDS, ['p', 'u', 'c', 'f']]);
53
+ return rows;
54
+ }
@@ -0,0 +1,30 @@
1
+ import type { CatalogQuery } from './types.mts';
2
+ export type CatalogIndexRow = {
3
+ table_name: string;
4
+ index_name: string;
5
+ definition: string;
6
+ access_method: string;
7
+ unique: boolean;
8
+ primary: boolean;
9
+ constraint_backed: boolean;
10
+ valid: boolean;
11
+ ready: boolean;
12
+ keys: CatalogIndexKeyRow[];
13
+ included_columns: string[];
14
+ predicate: string | null;
15
+ };
16
+ type CatalogIndexKeyRow = {
17
+ column: string | null;
18
+ expression: string;
19
+ opclass: string;
20
+ descending: boolean;
21
+ nulls_first: boolean;
22
+ };
23
+ export declare function readIndexes(query: CatalogQuery): Promise<CatalogIndexRow[]>;
24
+ export type CatalogTriggerRow = {
25
+ table_name: string;
26
+ trigger_name: string;
27
+ definition: string;
28
+ };
29
+ export declare function readTriggers(query: CatalogQuery): Promise<CatalogTriggerRow[]>;
30
+ export {};
@@ -0,0 +1,81 @@
1
+ import { EXCLUDE_EXTENSION_OWNED, EXCLUDE_PARTITION_CHILDREN, TABLE_RELKINDS, } from './catalog-shared.mjs';
2
+ export async function readIndexes(query) {
3
+ const { rows } = await query(`/* readSchemaSnapshotIndexes */
4
+ SELECT
5
+ target.relname AS table_name,
6
+ index_relation.relname AS index_name,
7
+ pg_get_indexdef(pg_index.indexrelid) AS definition,
8
+ pg_am.amname AS access_method,
9
+ pg_index.indisunique AS unique,
10
+ pg_index.indisprimary AS primary,
11
+ EXISTS (
12
+ SELECT 1 FROM pg_constraint WHERE pg_constraint.conindid = pg_index.indexrelid
13
+ ) AS constraint_backed,
14
+ pg_index.indisvalid AS valid,
15
+ pg_index.indisready AS ready,
16
+ COALESCE(
17
+ jsonb_agg(
18
+ jsonb_build_object(
19
+ 'column', pg_attribute.attname,
20
+ 'expression', pg_get_indexdef(pg_index.indexrelid, key_column.position::integer, true),
21
+ 'opclass', pg_opclass.opcname,
22
+ 'descending', (pg_index.indoption[(key_column.position - 1)::integer] & 1) <> 0,
23
+ 'nulls_first', (pg_index.indoption[(key_column.position - 1)::integer] & 2) <> 0
24
+ )
25
+ ORDER BY key_column.position
26
+ ) FILTER (WHERE key_column.position <= pg_index.indnkeyatts),
27
+ '[]'::jsonb
28
+ ) AS keys,
29
+ COALESCE(
30
+ jsonb_agg(pg_attribute.attname ORDER BY key_column.position)
31
+ FILTER (WHERE key_column.position > pg_index.indnkeyatts),
32
+ '[]'::jsonb
33
+ ) AS included_columns,
34
+ pg_get_expr(pg_index.indpred, pg_index.indrelid) AS predicate
35
+ FROM pg_index
36
+ JOIN pg_class index_relation ON index_relation.oid = pg_index.indexrelid
37
+ JOIN pg_class target ON target.oid = pg_index.indrelid
38
+ JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
39
+ JOIN pg_am ON pg_am.oid = index_relation.relam
40
+ LEFT JOIN LATERAL unnest(pg_index.indkey) WITH ORDINALITY AS key_column(attnum, position)
41
+ ON true
42
+ LEFT JOIN pg_attribute
43
+ ON pg_attribute.attrelid = target.oid AND pg_attribute.attnum = key_column.attnum
44
+ LEFT JOIN pg_opclass
45
+ ON pg_opclass.oid = pg_index.indclass[(key_column.position - 1)::integer]
46
+ WHERE pg_namespace.nspname = 'public'
47
+ AND target.relkind = ANY($1)
48
+ AND ${EXCLUDE_PARTITION_CHILDREN}
49
+ AND ${EXCLUDE_EXTENSION_OWNED}
50
+ GROUP BY
51
+ target.relname,
52
+ index_relation.relname,
53
+ pg_index.indexrelid,
54
+ pg_am.amname,
55
+ pg_index.indisunique,
56
+ pg_index.indisprimary,
57
+ pg_index.indisvalid,
58
+ pg_index.indisready,
59
+ pg_index.indnkeyatts,
60
+ pg_index.indpred,
61
+ pg_index.indrelid
62
+ ORDER BY table_name, index_name`, [TABLE_RELKINDS]);
63
+ return rows;
64
+ }
65
+ export async function readTriggers(query) {
66
+ const { rows } = await query(`/* readSchemaSnapshotTriggers */
67
+ SELECT
68
+ target.relname AS table_name,
69
+ pg_trigger.tgname AS trigger_name,
70
+ pg_get_triggerdef(pg_trigger.oid) AS definition
71
+ FROM pg_trigger
72
+ JOIN pg_class target ON target.oid = pg_trigger.tgrelid
73
+ JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
74
+ WHERE pg_namespace.nspname = 'public'
75
+ AND target.relkind = ANY($1)
76
+ AND NOT pg_trigger.tgisinternal
77
+ AND ${EXCLUDE_PARTITION_CHILDREN}
78
+ AND ${EXCLUDE_EXTENSION_OWNED}
79
+ ORDER BY table_name, trigger_name`, [TABLE_RELKINDS]);
80
+ return rows;
81
+ }
@@ -0,0 +1,22 @@
1
+ import type { CatalogQuery } from './types.mts';
2
+ export type CatalogTableRow = {
3
+ table_name: string;
4
+ relkind: 'p' | 'r';
5
+ partition_strategy: '' | 'h' | 'l' | 'r';
6
+ partition_key: string | null;
7
+ comment: string | null;
8
+ };
9
+ export declare function readTables(query: CatalogQuery): Promise<CatalogTableRow[]>;
10
+ export type CatalogColumnRow = {
11
+ table_name: string;
12
+ column_name: string;
13
+ data_type: string;
14
+ nullable: boolean;
15
+ default_expression: string | null;
16
+ identity: '' | 'a' | 'd';
17
+ generated: '' | 's' | 'v';
18
+ collation: string | null;
19
+ comment: string | null;
20
+ ordinal_position: number;
21
+ };
22
+ export declare function readColumns(query: CatalogQuery): Promise<CatalogColumnRow[]>;
@@ -0,0 +1,48 @@
1
+ import { EXCLUDE_EXTENSION_OWNED, EXCLUDE_PARTITION_CHILDREN, TABLE_RELKINDS, } from './catalog-shared.mjs';
2
+ export async function readTables(query) {
3
+ const { rows } = await query(`/* readSchemaSnapshotTables */
4
+ SELECT
5
+ target.relname AS table_name,
6
+ target.relkind AS relkind,
7
+ COALESCE(pg_partitioned_table.partstrat, '') AS partition_strategy,
8
+ pg_get_partkeydef(pg_partitioned_table.partrelid) AS partition_key,
9
+ obj_description(target.oid, 'pg_class') AS comment
10
+ FROM pg_class target
11
+ JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
12
+ LEFT JOIN pg_partitioned_table ON pg_partitioned_table.partrelid = target.oid
13
+ WHERE pg_namespace.nspname = 'public'
14
+ AND target.relkind = ANY($1)
15
+ AND ${EXCLUDE_PARTITION_CHILDREN}
16
+ AND ${EXCLUDE_EXTENSION_OWNED}
17
+ ORDER BY table_name`, [TABLE_RELKINDS]);
18
+ return rows;
19
+ }
20
+ export async function readColumns(query) {
21
+ const { rows } = await query(`/* readSchemaSnapshotColumns */
22
+ SELECT
23
+ target.relname AS table_name,
24
+ pg_attribute.attname AS column_name,
25
+ format_type(pg_attribute.atttypid, pg_attribute.atttypemod) AS data_type,
26
+ NOT pg_attribute.attnotnull AS nullable,
27
+ pg_get_expr(pg_attrdef.adbin, pg_attrdef.adrelid) AS default_expression,
28
+ pg_attribute.attidentity AS identity,
29
+ pg_attribute.attgenerated AS generated,
30
+ pg_collation.collname AS collation,
31
+ col_description(target.oid, pg_attribute.attnum) AS comment,
32
+ pg_attribute.attnum AS ordinal_position
33
+ FROM pg_attribute
34
+ JOIN pg_class target ON target.oid = pg_attribute.attrelid
35
+ JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
36
+ LEFT JOIN pg_attrdef
37
+ ON pg_attrdef.adrelid = pg_attribute.attrelid AND pg_attrdef.adnum = pg_attribute.attnum
38
+ LEFT JOIN pg_collation
39
+ ON pg_collation.oid = pg_attribute.attcollation AND pg_collation.collname <> 'default'
40
+ WHERE pg_namespace.nspname = 'public'
41
+ AND target.relkind = ANY($1)
42
+ AND pg_attribute.attnum > 0
43
+ AND NOT pg_attribute.attisdropped
44
+ AND ${EXCLUDE_PARTITION_CHILDREN}
45
+ AND ${EXCLUDE_EXTENSION_OWNED}
46
+ ORDER BY table_name, ordinal_position`, [TABLE_RELKINDS]);
47
+ return rows;
48
+ }
@@ -0,0 +1,4 @@
1
+ export declare function lstatOrNull(path: string): Promise<import("node:fs").Stats | null>;
2
+ export declare function assertSafeExistingFilePath(root: string, path: string): Promise<void>;
3
+ export declare function ensureSafeParentDirectory(root: string, path: string): Promise<void>;
4
+ export declare function writeGeneratedFile(root: string, path: string, content: string): Promise<void>;
@@ -0,0 +1,71 @@
1
+ import { lstat, mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ function unsafeSchemaSnapshotPath(path) {
4
+ return new Error(`Unsafe generated PostgreSQL schema snapshot path: ${path}`);
5
+ }
6
+ export async function lstatOrNull(path) {
7
+ return lstat(path).catch((error) =>
8
+ /* v8 ignore next -- non-ENOENT stat failures are host-specific */
9
+ error.code === 'ENOENT' ? null : Promise.reject(error));
10
+ }
11
+ async function assertSafeDirectory(path, create) {
12
+ let stats = await lstatOrNull(path);
13
+ if (stats === null && create) {
14
+ await mkdir(path).catch((error) => error.code === 'EEXIST' ? undefined : Promise.reject(error));
15
+ stats = await lstatOrNull(path);
16
+ }
17
+ if (stats === null || stats.isSymbolicLink() || !stats.isDirectory()) {
18
+ throw unsafeSchemaSnapshotPath(path);
19
+ }
20
+ }
21
+ function relativeSnapshotPath(root, path) {
22
+ const relativePath = relative(resolve(root), resolve(path));
23
+ if (relativePath === '' ||
24
+ relativePath === '..' ||
25
+ relativePath.startsWith(`..${sep}`) ||
26
+ isAbsolute(relativePath)) {
27
+ throw unsafeSchemaSnapshotPath(path);
28
+ }
29
+ return relativePath;
30
+ }
31
+ async function assertSafePathComponents(directory, components, create) {
32
+ const [component, ...remaining] = components;
33
+ if (component === undefined)
34
+ return;
35
+ const path = join(directory, component);
36
+ const stats = await lstatOrNull(path);
37
+ if (stats === null && !create)
38
+ return;
39
+ await assertSafeDirectory(path, create);
40
+ await assertSafePathComponents(path, remaining, create);
41
+ }
42
+ async function assertSafeParentDirectories(root, path, create) {
43
+ const resolvedRoot = resolve(root);
44
+ const relativePath = relativeSnapshotPath(resolvedRoot, path);
45
+ await assertSafeDirectory(resolvedRoot, false);
46
+ await assertSafePathComponents(resolvedRoot, relativePath.split(sep).slice(0, -1), create);
47
+ }
48
+ async function assertSafeFilePath(root, path, create) {
49
+ await assertSafeParentDirectories(root, path, create);
50
+ const leaf = await lstatOrNull(path);
51
+ if (leaf?.isSymbolicLink())
52
+ throw unsafeSchemaSnapshotPath(path);
53
+ }
54
+ export async function assertSafeExistingFilePath(root, path) {
55
+ await assertSafeFilePath(root, path, false);
56
+ }
57
+ export async function ensureSafeParentDirectory(root, path) {
58
+ await assertSafeFilePath(root, path, true);
59
+ }
60
+ export async function writeGeneratedFile(root, path, content) {
61
+ await ensureSafeParentDirectory(root, path);
62
+ const temporaryDirectory = await mkdtemp(join(dirname(path), '.schema-snapshot-'));
63
+ const temporaryPath = join(temporaryDirectory, 'contents');
64
+ try {
65
+ await writeFile(temporaryPath, content);
66
+ await rename(temporaryPath, path);
67
+ }
68
+ finally {
69
+ await rm(temporaryDirectory, { force: true, recursive: true });
70
+ }
71
+ }
@@ -0,0 +1,18 @@
1
+ import type { CatalogQuery, SchemaGrowthMaps, SchemaSnapshot } from './types.mts';
2
+ export declare function stableStringify(value: unknown): string;
3
+ export declare function writeSchemaSnapshot({ snapshot, markdown, root, check, format, stringify, }: {
4
+ snapshot: SchemaSnapshot;
5
+ markdown: Map<string, string>;
6
+ root: string;
7
+ check?: boolean;
8
+ format?: (path: string, raw: string) => Promise<string>;
9
+ stringify?: (value: unknown) => string;
10
+ }): Promise<void>;
11
+ export declare function generateSchemaSnapshot({ query, growth, root, check, format, stringify, }: {
12
+ query: CatalogQuery;
13
+ growth: SchemaGrowthMaps;
14
+ root: string;
15
+ check?: boolean;
16
+ format?: (path: string, raw: string) => Promise<string>;
17
+ stringify?: (value: unknown) => string;
18
+ }): Promise<void>;
@@ -0,0 +1,95 @@
1
+ import { readFile, rm } from 'node:fs/promises';
2
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { buildSchemaSnapshot } from './build-snapshot.mjs';
4
+ import { readSchemaCatalog } from './catalog-queries.mjs';
5
+ import { assertSafeExistingFilePath, ensureSafeParentDirectory, lstatOrNull, writeGeneratedFile, } from './file-safety.mjs';
6
+ import { markdownFilesOnDisk } from './markdown-files.mjs';
7
+ import { renderSchemaMarkdown } from './render-markdown.mjs';
8
+ export function stableStringify(value) {
9
+ return `${JSON.stringify(sortKeys(value), null, 2)}\n`;
10
+ }
11
+ function sortKeys(value) {
12
+ if (Array.isArray(value))
13
+ return value.map(sortKeys);
14
+ if (value !== null && typeof value === 'object') {
15
+ return Object.fromEntries(Object.entries(value)
16
+ .toSorted(([left], [right]) => left.localeCompare(right))
17
+ .map(([key, nested]) => [key, sortKeys(nested)]));
18
+ }
19
+ return value;
20
+ }
21
+ async function identityFormat(_path, raw) {
22
+ return raw;
23
+ }
24
+ function assertSafeMarkdownPath(markdownRoot, path) {
25
+ const outputPath = resolve(markdownRoot, path);
26
+ const relativePath = relative(markdownRoot, outputPath);
27
+ if (isAbsolute(path) ||
28
+ !path.endsWith('.md') ||
29
+ relativePath === '' ||
30
+ relativePath === '..' ||
31
+ relativePath.startsWith(`..${sep}`) ||
32
+ isAbsolute(relativePath)) {
33
+ throw new Error(`Unsafe generated PostgreSQL schema Markdown path: ${path}`);
34
+ }
35
+ return outputPath;
36
+ }
37
+ async function schemaSnapshotFiles(snapshot, markdown, root, format, stringify) {
38
+ const jsonPath = join(root, 'schema.json');
39
+ const markdownRoot = join(root, 'markdown');
40
+ const [json, ...formattedMarkdown] = await Promise.all([
41
+ format(jsonPath, stringify(snapshot)),
42
+ ...[...markdown].map(async ([path, content]) => {
43
+ const outputPath = assertSafeMarkdownPath(markdownRoot, path);
44
+ return [outputPath, await format(outputPath, content)];
45
+ }),
46
+ ]);
47
+ return new Map([[jsonPath, json], ...formattedMarkdown]);
48
+ }
49
+ async function staleSchemaSnapshotFiles(files, root) {
50
+ const results = await Promise.all([...files].map(async ([path, content]) => {
51
+ await assertSafeExistingFilePath(root, path);
52
+ const actual = await readFile(path, 'utf8').catch((err) =>
53
+ /* v8 ignore next -- non-ENOENT read failures are host-specific */
54
+ err.code === 'ENOENT' ? null : Promise.reject(err));
55
+ return actual === content ? null : path;
56
+ }));
57
+ return results.filter((path) => path !== null);
58
+ }
59
+ async function extraMarkdownPaths(files, root) {
60
+ const expected = new Set(files.keys());
61
+ const orphaned = (await markdownFilesOnDisk(root)).filter((path) => !expected.has(path));
62
+ const legacyPath = join(root, 'schema.md');
63
+ const legacyExists = (await lstatOrNull(legacyPath)) !== null;
64
+ return [...orphaned, ...(legacyExists ? [legacyPath] : [])];
65
+ }
66
+ export async function writeSchemaSnapshot({ snapshot, markdown, root, check = false, format = identityFormat, stringify = stableStringify, }) {
67
+ const files = await schemaSnapshotFiles(snapshot, markdown, root, format, stringify);
68
+ if (!check) {
69
+ await Promise.all([...files.keys()].map((path) => ensureSafeParentDirectory(root, path)));
70
+ await Promise.all([...files].map(([path, content]) => writeGeneratedFile(root, path, content)));
71
+ await Promise.all((await extraMarkdownPaths(files, root)).map((path) => rm(path)));
72
+ return;
73
+ }
74
+ const stale = [
75
+ ...(await staleSchemaSnapshotFiles(files, root)),
76
+ ...(await extraMarkdownPaths(files, root)),
77
+ ].toSorted((left, right) => left.localeCompare(right));
78
+ if (stale.length > 0) {
79
+ throw new Error([
80
+ 'PostgreSQL schema snapshot is stale. Regenerate it and commit:',
81
+ ...stale.map((path) => `- ${path}`),
82
+ ].join('\n'));
83
+ }
84
+ }
85
+ export async function generateSchemaSnapshot({ query, growth, root, check = false, format = identityFormat, stringify = stableStringify, }) {
86
+ const snapshot = buildSchemaSnapshot(await readSchemaCatalog(query), growth);
87
+ await writeSchemaSnapshot({
88
+ snapshot,
89
+ markdown: renderSchemaMarkdown(snapshot),
90
+ root,
91
+ check,
92
+ format,
93
+ stringify,
94
+ });
95
+ }
@@ -0,0 +1,40 @@
1
+ export type SchemaIndexRenameSnapshot = {
2
+ tables: Record<string, {
3
+ indexes: Record<string, {
4
+ definition: string;
5
+ }>;
6
+ }>;
7
+ };
8
+ /**
9
+ * Removes the index name from a `pg_get_indexdef()` definition, leaving a byte-exact "shape key"
10
+ * for comparing two index definitions on identity (uniqueness, table, `ON ONLY`, access method,
11
+ * key columns, `INCLUDE`, opclasses, `WHERE`) while ignoring only the name.
12
+ *
13
+ * Locates the name by offset rather than by regex: it finds the first ` INDEX ` (present exactly
14
+ * once, since nothing before it in `CREATE [UNIQUE] INDEX ...` can contain that substring), then
15
+ * confirms the token immediately after is exactly `name` or `"name"`. Throws if neither matches —
16
+ * a caller that silently skipped an unrecognized definition would mask a real collision instead of
17
+ * reporting one.
18
+ */
19
+ export declare function indexShapeKey(name: string, definition: string): string;
20
+ export type RenamedIndex = {
21
+ table: string;
22
+ retiredName: string;
23
+ retiredDefinition: string;
24
+ renamedTo: string;
25
+ };
26
+ /**
27
+ * Detects indexes present at `base` whose name is gone at `head` but whose physical shape
28
+ * reappears under a different name — the cross-revision rename case that a single-revision
29
+ * shape scan structurally cannot see.
30
+ *
31
+ * Scope is renames only: a base index whose name simply disappears with no shape match at head
32
+ * (dropped outright, not renamed) is not reported.
33
+ *
34
+ * Index names are schema-unique in PostgreSQL, so a flat name set (rather than a per-table one)
35
+ * is sufficient to know whether a name survived into head.
36
+ */
37
+ export declare function detectRenamedIndexes({ base, head, }: {
38
+ base: SchemaIndexRenameSnapshot;
39
+ head: SchemaIndexRenameSnapshot;
40
+ }): RenamedIndex[];