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.
- package/README.md +21 -1
- package/dist/cli/commands/gha-artifacts-cleanup.d.mts +6 -0
- package/dist/cli/commands/gha-artifacts-cleanup.mjs +50 -0
- package/dist/cli/commands/http-origin.d.mts +2 -0
- package/dist/cli/commands/http-origin.mjs +14 -0
- package/dist/cli/index.mjs +11 -0
- package/dist/cli/parse-gha-artifacts-cleanup.d.mts +15 -0
- package/dist/cli/parse-gha-artifacts-cleanup.mjs +74 -0
- package/dist/cli/parse.d.mts +7 -2
- package/dist/cli/parse.mjs +36 -0
- package/dist/cli/usage.d.mts +1 -1
- package/dist/cli/usage.mjs +9 -0
- package/dist/gha-artifacts-cleanup/classify.d.mts +14 -0
- package/dist/gha-artifacts-cleanup/classify.mjs +32 -0
- package/dist/gha-artifacts-cleanup/commands.d.mts +23 -0
- package/dist/gha-artifacts-cleanup/commands.mjs +55 -0
- package/dist/gha-artifacts-cleanup/github.d.mts +16 -0
- package/dist/gha-artifacts-cleanup/github.mjs +100 -0
- package/dist/gha-artifacts-cleanup/index.d.mts +8 -0
- package/dist/gha-artifacts-cleanup/index.mjs +4 -0
- package/dist/gha-artifacts-cleanup/plan.d.mts +17 -0
- package/dist/gha-artifacts-cleanup/plan.mjs +40 -0
- package/dist/gha-selected-files/index.d.mts +6 -0
- package/dist/gha-selected-files/index.mjs +38 -0
- package/dist/http-origin/index.d.mts +1 -0
- package/dist/http-origin/index.mjs +23 -0
- package/dist/index.d.mts +11 -2
- package/dist/index.mjs +7 -1
- package/dist/openapi-document/build-openapi-document.d.mts +11 -0
- package/dist/openapi-document/build-openapi-document.mjs +122 -0
- package/dist/openapi-document/build-openapi-query.d.mts +3 -0
- package/dist/openapi-document/build-openapi-query.mjs +53 -0
- package/dist/openapi-document/build-openapi-request.d.mts +7 -0
- package/dist/openapi-document/build-openapi-request.mjs +29 -0
- package/dist/openapi-document/build-openapi-response.d.mts +9 -0
- package/dist/openapi-document/build-openapi-response.mjs +111 -0
- package/dist/openapi-document/component-registry.d.mts +9 -0
- package/dist/openapi-document/component-registry.mjs +49 -0
- package/dist/openapi-document/contract-schema-canonical.d.mts +4 -0
- package/dist/openapi-document/contract-schema-canonical.mjs +23 -0
- package/dist/openapi-document/contract-schema-intersection-merge.d.mts +13 -0
- package/dist/openapi-document/contract-schema-intersection-merge.mjs +96 -0
- package/dist/openapi-document/contract-schema-to-openapi.d.mts +7 -0
- package/dist/openapi-document/contract-schema-to-openapi.mjs +71 -0
- package/dist/openapi-document/contract-schema-types.d.mts +43 -0
- package/dist/openapi-document/contract-schema-types.mjs +1 -0
- package/dist/openapi-document/index.d.mts +19 -0
- package/dist/openapi-document/index.mjs +12 -0
- package/dist/openapi-document/openapi-error-response.d.mts +2 -0
- package/dist/openapi-document/openapi-error-response.mjs +13 -0
- package/dist/openapi-document/openapi-route-helpers.d.mts +9 -0
- package/dist/openapi-document/openapi-route-helpers.mjs +61 -0
- package/dist/openapi-document/openapi-types.d.mts +80 -0
- package/dist/openapi-document/openapi-types.mjs +1 -0
- package/dist/openapi-document/operation-types.d.mts +35 -0
- package/dist/openapi-document/operation-types.mjs +11 -0
- package/dist/openapi-document/query-types.d.mts +35 -0
- package/dist/openapi-document/query-types.mjs +1 -0
- package/dist/openapi-document/schema-node-builders.d.mts +3 -0
- package/dist/openapi-document/schema-node-builders.mjs +6 -0
- package/dist/openapi-document/write-openapi.d.mts +9 -0
- package/dist/openapi-document/write-openapi.mjs +19 -0
- package/dist/pg-schema-snapshot/build-snapshot.d.mts +9 -0
- package/dist/pg-schema-snapshot/build-snapshot.mjs +61 -0
- package/dist/pg-schema-snapshot/catalog-objects.d.mts +34 -0
- package/dist/pg-schema-snapshot/catalog-objects.mjs +64 -0
- package/dist/pg-schema-snapshot/catalog-queries.d.mts +20 -0
- package/dist/pg-schema-snapshot/catalog-queries.mjs +31 -0
- package/dist/pg-schema-snapshot/catalog-shared.d.mts +3 -0
- package/dist/pg-schema-snapshot/catalog-shared.mjs +19 -0
- package/dist/pg-schema-snapshot/catalog-table-constraints.d.mts +14 -0
- package/dist/pg-schema-snapshot/catalog-table-constraints.mjs +54 -0
- package/dist/pg-schema-snapshot/catalog-table-indexes.d.mts +30 -0
- package/dist/pg-schema-snapshot/catalog-table-indexes.mjs +81 -0
- package/dist/pg-schema-snapshot/catalog-tables.d.mts +22 -0
- package/dist/pg-schema-snapshot/catalog-tables.mjs +48 -0
- package/dist/pg-schema-snapshot/file-safety.d.mts +4 -0
- package/dist/pg-schema-snapshot/file-safety.mjs +71 -0
- package/dist/pg-schema-snapshot/generate.d.mts +18 -0
- package/dist/pg-schema-snapshot/generate.mjs +95 -0
- package/dist/pg-schema-snapshot/index-rename-detect.d.mts +40 -0
- package/dist/pg-schema-snapshot/index-rename-detect.mjs +71 -0
- package/dist/pg-schema-snapshot/index.d.mts +10 -0
- package/dist/pg-schema-snapshot/index.mjs +6 -0
- package/dist/pg-schema-snapshot/markdown-files.d.mts +1 -0
- package/dist/pg-schema-snapshot/markdown-files.mjs +26 -0
- package/dist/pg-schema-snapshot/render-markdown.d.mts +8 -0
- package/dist/pg-schema-snapshot/render-markdown.mjs +113 -0
- package/dist/pg-schema-snapshot/render-tables.d.mts +2 -0
- package/dist/pg-schema-snapshot/render-tables.mjs +55 -0
- package/dist/pg-schema-snapshot/snapshot-build-utils.d.mts +2 -0
- package/dist/pg-schema-snapshot/snapshot-build-utils.mjs +15 -0
- package/dist/pg-schema-snapshot/snapshot.test-helpers.d.mts +11 -0
- package/dist/pg-schema-snapshot/snapshot.test-helpers.mjs +101 -0
- package/dist/pg-schema-snapshot/table-snapshot-columns.d.mts +3 -0
- package/dist/pg-schema-snapshot/table-snapshot-columns.mjs +33 -0
- package/dist/pg-schema-snapshot/table-snapshot-constraints.d.mts +7 -0
- package/dist/pg-schema-snapshot/table-snapshot-constraints.mjs +55 -0
- package/dist/pg-schema-snapshot/table-snapshot.d.mts +10 -0
- package/dist/pg-schema-snapshot/table-snapshot.mjs +99 -0
- package/dist/pg-schema-snapshot/types.d.mts +103 -0
- package/dist/pg-schema-snapshot/types.mjs +1 -0
- package/dist/process-line-buffer/index.d.mts +7 -0
- package/dist/process-line-buffer/index.mjs +28 -0
- package/dist/shared-context/{fake-git.test-helpers.d.mts → fake-git.d.mts} +1 -0
- package/dist/shared-context/{fake-git.test-helpers.mjs → fake-git.mjs} +4 -1
- package/dist/shared-context/index.d.mts +2 -0
- package/dist/shared-context/index.mjs +1 -0
- package/package.json +37 -1
- package/scripts/gha/diagnose-port-collision.sh +232 -0
- 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[];
|