pg-migration-engine 0.1.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/LICENSE +35 -0
- package/README.md +304 -0
- package/package.json +52 -0
- package/src/behavioral/behavioral-applier.js +374 -0
- package/src/behavioral/behavioral-extractor.js +266 -0
- package/src/behavioral/behavioral-puller.js +35 -0
- package/src/behavioral/index.js +4 -0
- package/src/behavioral/phase-sorter.js +24 -0
- package/src/constants.js +97 -0
- package/src/ddl-generator/alter-generator.js +1512 -0
- package/src/ddl-generator/comment-generator.js +95 -0
- package/src/ddl-generator/create-generator.js +1426 -0
- package/src/ddl-generator/drop-generator.js +194 -0
- package/src/ddl-generator/generator.js +78 -0
- package/src/ddl-generator/grant-generator.js +31 -0
- package/src/ddl-generator/index.js +3 -0
- package/src/ddl-generator/pg-version.js +15 -0
- package/src/ddl-generator/rename-generator.js +88 -0
- package/src/ddl-generator/safe-patterns.js +241 -0
- package/src/differ/change-classifier.js +192 -0
- package/src/differ/dependency-resolver.js +523 -0
- package/src/differ/index.js +12 -0
- package/src/differ/object-matcher.js +510 -0
- package/src/differ/property-differ.js +1134 -0
- package/src/differ/risk-tagger.js +561 -0
- package/src/differ/schema-differ.js +309 -0
- package/src/differ/utils/levenshtein.js +210 -0
- package/src/differ/utils/path-builder.js +198 -0
- package/src/differ/utils/type-compatibility.js +472 -0
- package/src/errors.js +147 -0
- package/src/executor/drift-detector.js +221 -0
- package/src/executor/index.js +7 -0
- package/src/executor/lock-manager.js +308 -0
- package/src/executor/migration-executor.js +1249 -0
- package/src/executor/progress-tracker.js +81 -0
- package/src/executor/recovery-manager.js +47 -0
- package/src/executor/snapshot-manager.js +22 -0
- package/src/executor/sql-splitter.js +120 -0
- package/src/executor/transaction-manager.js +288 -0
- package/src/index.js +483 -0
- package/src/introspection/index.js +4 -0
- package/src/introspection/introspector.js +250 -0
- package/src/introspection/queries/access-methods.js +29 -0
- package/src/introspection/queries/casts.js +38 -0
- package/src/introspection/queries/collations.js +51 -0
- package/src/introspection/queries/comments.js +66 -0
- package/src/introspection/queries/constraints.js +65 -0
- package/src/introspection/queries/conversions.js +39 -0
- package/src/introspection/queries/databases.js +43 -0
- package/src/introspection/queries/default-privileges.js +37 -0
- package/src/introspection/queries/event-triggers.js +43 -0
- package/src/introspection/queries/extensions.js +21 -0
- package/src/introspection/queries/foreign-data.js +136 -0
- package/src/introspection/queries/functions.js +75 -0
- package/src/introspection/queries/grants.js +38 -0
- package/src/introspection/queries/index.js +33 -0
- package/src/introspection/queries/indexes.js +99 -0
- package/src/introspection/queries/inheritance.js +24 -0
- package/src/introspection/queries/multiranges.js +37 -0
- package/src/introspection/queries/operators.js +180 -0
- package/src/introspection/queries/partitions.js +42 -0
- package/src/introspection/queries/pg18-19.js +43 -0
- package/src/introspection/queries/policies.js +35 -0
- package/src/introspection/queries/procedural-languages.js +36 -0
- package/src/introspection/queries/publications.js +114 -0
- package/src/introspection/queries/roles.js +66 -0
- package/src/introspection/queries/rules.js +49 -0
- package/src/introspection/queries/sequences.js +37 -0
- package/src/introspection/queries/statistics.js +74 -0
- package/src/introspection/queries/subscriptions.js +99 -0
- package/src/introspection/queries/tables.js +151 -0
- package/src/introspection/queries/tablespaces.js +36 -0
- package/src/introspection/queries/text-search.js +146 -0
- package/src/introspection/queries/triggers.js +61 -0
- package/src/introspection/queries/types.js +106 -0
- package/src/introspection/queries/views.js +146 -0
- package/src/introspection/translator.js +1283 -0
- package/src/introspection/version-detector.js +25 -0
- package/src/planner/backfill-planner.js +36 -0
- package/src/planner/dry-run.js +21 -0
- package/src/planner/index.js +6 -0
- package/src/planner/migration-planner.js +356 -0
- package/src/planner/smart-migrator.js +82 -0
- package/src/planner/step-sequencer.js +61 -0
- package/src/planner/type-registry.js +47 -0
- package/src/risk/compatibility-checker.js +29 -0
- package/src/risk/data-loss-checker.js +32 -0
- package/src/risk/destructive-checker.js +38 -0
- package/src/risk/index.js +6 -0
- package/src/risk/lock-analyzer.js +39 -0
- package/src/risk/recommendations.js +44 -0
- package/src/risk/risk-engine.js +25 -0
- package/src/storage/index.js +5 -0
- package/src/storage/memory-storage.js +87 -0
- package/src/storage/migration-table.js +423 -0
- package/src/storage/rollback-generator.js +344 -0
- package/src/types/changes.js +136 -0
- package/src/types/connection.js +17 -0
- package/src/types/execution.js +107 -0
- package/src/types/index.js +1 -0
- package/src/types/migration.js +67 -0
- package/src/types/risk.js +32 -0
- package/src/types/schema.d.ts +1004 -0
- package/src/types/schema.js +561 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PG18+ specific introspection queries.
|
|
3
|
+
* Only called when detected PG version >= 18.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PG18_NOT_ENFORCED_QUERY = `
|
|
7
|
+
SELECT n.nspname AS schema,
|
|
8
|
+
c.conname AS name,
|
|
9
|
+
c.conrelid::regclass::text AS table_ref,
|
|
10
|
+
NOT c.conenforced AS not_enforced
|
|
11
|
+
FROM pg_catalog.pg_constraint c
|
|
12
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace
|
|
13
|
+
WHERE c.conenforced = false
|
|
14
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
15
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const PG18_VIRTUAL_COLUMNS_QUERY = `
|
|
19
|
+
SELECT a.attrelid::regclass::text AS table_ref,
|
|
20
|
+
n.nspname AS schema,
|
|
21
|
+
c.relname AS table_name,
|
|
22
|
+
a.attname AS name,
|
|
23
|
+
a.attgenerated AS generated_type
|
|
24
|
+
FROM pg_catalog.pg_attribute a
|
|
25
|
+
JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
|
|
26
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
27
|
+
WHERE a.attgenerated = 'v'
|
|
28
|
+
AND NOT a.attisdropped
|
|
29
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
30
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {import('pg').Pool} pool
|
|
35
|
+
* @returns {Promise<{notEnforced:Array,virtualColumns:Array}>}
|
|
36
|
+
*/
|
|
37
|
+
export async function queryPg18Features(pool) {
|
|
38
|
+
const [notEnforced, virtualColumns] = await Promise.all([
|
|
39
|
+
pool.query(PG18_NOT_ENFORCED_QUERY).then(r => r.rows),
|
|
40
|
+
pool.query(PG18_VIRTUAL_COLUMNS_QUERY).then(r => r.rows),
|
|
41
|
+
]);
|
|
42
|
+
return { notEnforced, virtualColumns };
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const POLICIES_QUERY = `
|
|
2
|
+
SELECT pol.polname AS name,
|
|
3
|
+
n.nspname AS schema,
|
|
4
|
+
c.relname AS table_name,
|
|
5
|
+
CASE pol.polcmd
|
|
6
|
+
WHEN '*' THEN 'ALL'
|
|
7
|
+
WHEN 'r' THEN 'SELECT'
|
|
8
|
+
WHEN 'a' THEN 'INSERT'
|
|
9
|
+
WHEN 'w' THEN 'UPDATE'
|
|
10
|
+
WHEN 'd' THEN 'DELETE'
|
|
11
|
+
END AS command,
|
|
12
|
+
pol.polpermissive AS is_permissive,
|
|
13
|
+
CASE
|
|
14
|
+
WHEN pol.polroles = '{0}' THEN ARRAY['PUBLIC']
|
|
15
|
+
ELSE ARRAY(SELECT rolname FROM pg_catalog.pg_roles WHERE oid = ANY(pol.polroles))
|
|
16
|
+
END AS roles,
|
|
17
|
+
pg_catalog.pg_get_expr(pol.polqual, pol.polrelid) AS using_expression,
|
|
18
|
+
pg_catalog.pg_get_expr(pol.polwithcheck, pol.polrelid) AS with_check_expression,
|
|
19
|
+
pg_catalog.obj_description(pol.oid, 'pg_policy') AS comment
|
|
20
|
+
FROM pg_catalog.pg_policy pol
|
|
21
|
+
JOIN pg_catalog.pg_class c ON c.oid = pol.polrelid
|
|
22
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
23
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
24
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
25
|
+
ORDER BY n.nspname, c.relname, pol.polname
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('pg').Pool} pool
|
|
30
|
+
* @returns {Promise<Array>}
|
|
31
|
+
*/
|
|
32
|
+
export async function queryPolicies(pool) {
|
|
33
|
+
const result = await pool.query(POLICIES_QUERY);
|
|
34
|
+
return result.rows;
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const LANGUAGES_QUERY = `
|
|
2
|
+
SELECT
|
|
3
|
+
l.oid,
|
|
4
|
+
l.lanname AS name,
|
|
5
|
+
pg_catalog.pg_get_userbyid(l.lanowner) AS owner,
|
|
6
|
+
l.lanispl AS is_pl,
|
|
7
|
+
l.lanpltrusted AS is_trusted,
|
|
8
|
+
CASE WHEN l.lanplcallfoid = 0 THEN NULL ELSE l.lanplcallfoid::regproc::text END AS handler,
|
|
9
|
+
CASE WHEN l.laninline = 0 THEN NULL ELSE l.laninline::regproc::text END AS inline,
|
|
10
|
+
CASE WHEN l.lanvalidator = 0 THEN NULL ELSE l.lanvalidator::regproc::text END AS validator
|
|
11
|
+
FROM pg_catalog.pg_language l
|
|
12
|
+
WHERE l.lanispl OR l.lanname NOT IN ('internal', 'sql', 'c')
|
|
13
|
+
ORDER BY l.lanname
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('pg').Pool} pool
|
|
18
|
+
* @returns {Promise<Array>}
|
|
19
|
+
*/
|
|
20
|
+
export async function queryProceduralLanguages(pool) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await pool.query(LANGUAGES_QUERY);
|
|
23
|
+
|
|
24
|
+
return result.rows.map(row => ({
|
|
25
|
+
name: row.name,
|
|
26
|
+
owner: row.owner,
|
|
27
|
+
is_pl: row.is_pl,
|
|
28
|
+
is_trusted: row.is_trusted,
|
|
29
|
+
handler: row.handler,
|
|
30
|
+
inline: row.inline,
|
|
31
|
+
validator: row.validator,
|
|
32
|
+
}));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const PUBLICATIONS_QUERY = `
|
|
2
|
+
SELECT
|
|
3
|
+
p.oid,
|
|
4
|
+
p.pubname AS name,
|
|
5
|
+
pg_catalog.pg_get_userbyid(p.pubowner) AS owner,
|
|
6
|
+
p.puballtables AS all_tables,
|
|
7
|
+
p.pubinsert AS insert,
|
|
8
|
+
p.pubupdate AS update,
|
|
9
|
+
p.pubdelete AS delete,
|
|
10
|
+
p.pubtruncate AS truncate,
|
|
11
|
+
p.pubviaroot AS via_root,
|
|
12
|
+
pg_catalog.obj_description(p.oid, 'pg_publication') AS comment
|
|
13
|
+
FROM pg_catalog.pg_publication p
|
|
14
|
+
ORDER BY p.pubname
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const PUBLICATION_TABLES_QUERY = `
|
|
18
|
+
SELECT
|
|
19
|
+
p.pubname AS publication,
|
|
20
|
+
n.nspname AS table_schema,
|
|
21
|
+
c.relname AS table_name,
|
|
22
|
+
pg_catalog.pg_get_expr(pr.prqual, c.oid) AS row_filter,
|
|
23
|
+
pr.prattrs AS column_attnums
|
|
24
|
+
FROM pg_catalog.pg_publication_rel pr
|
|
25
|
+
JOIN pg_catalog.pg_publication p ON p.oid = pr.prpubid
|
|
26
|
+
JOIN pg_catalog.pg_class c ON c.oid = pr.prrelid
|
|
27
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
28
|
+
WHERE c.relkind IN ('r', 'p')
|
|
29
|
+
ORDER BY p.pubname, n.nspname, c.relname
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const PUBLICATION_SCHEMAS_QUERY = `
|
|
33
|
+
SELECT
|
|
34
|
+
p.pubname AS publication,
|
|
35
|
+
n.nspname AS schema_name
|
|
36
|
+
FROM pg_catalog.pg_publication_namespace pn
|
|
37
|
+
JOIN pg_catalog.pg_publication p ON p.oid = pn.pnpubid
|
|
38
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = pn.pnnspid
|
|
39
|
+
ORDER BY p.pubname, n.nspname
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {import('pg').Pool} pool
|
|
44
|
+
* @param {number} version
|
|
45
|
+
* @returns {Promise<Array>}
|
|
46
|
+
*/
|
|
47
|
+
export async function queryPublications(pool, version) {
|
|
48
|
+
if (version < 100000) return [];
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const pubsResult = await pool.query(PUBLICATIONS_QUERY);
|
|
52
|
+
const tablesResult = await pool.query(PUBLICATION_TABLES_QUERY);
|
|
53
|
+
|
|
54
|
+
let schemasResult = { rows: [] };
|
|
55
|
+
if (version >= 150000) {
|
|
56
|
+
try {
|
|
57
|
+
schemasResult = await pool.query(PUBLICATION_SCHEMAS_QUERY);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// pg_publication_namespace doesn't exist before PG15
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const publicationMap = new Map();
|
|
64
|
+
|
|
65
|
+
for (const row of pubsResult.rows) {
|
|
66
|
+
publicationMap.set(row.name, {
|
|
67
|
+
name: row.name,
|
|
68
|
+
owner: row.owner,
|
|
69
|
+
all_tables: row.all_tables,
|
|
70
|
+
insert: row.insert,
|
|
71
|
+
update: row.update,
|
|
72
|
+
delete: row.delete,
|
|
73
|
+
truncate: row.truncate,
|
|
74
|
+
via_root: row.via_root,
|
|
75
|
+
comment: row.comment,
|
|
76
|
+
tables: [],
|
|
77
|
+
schemas: [],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const row of tablesResult.rows) {
|
|
82
|
+
const pub = publicationMap.get(row.publication);
|
|
83
|
+
if (pub) {
|
|
84
|
+
const tableEntry = {
|
|
85
|
+
table: `${row.table_schema}.${row.table_name}`,
|
|
86
|
+
};
|
|
87
|
+
if (row.row_filter) {
|
|
88
|
+
tableEntry.rowFilter = row.row_filter;
|
|
89
|
+
}
|
|
90
|
+
if (row.column_attnums) {
|
|
91
|
+
// Parse column attnums to get column names from the table
|
|
92
|
+
try {
|
|
93
|
+
const attnums = row.column_attnums;
|
|
94
|
+
if (attnums && attnums.length > 0) {
|
|
95
|
+
tableEntry.columns = attnums;
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
}
|
|
99
|
+
pub.tables.push(tableEntry);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const row of schemasResult.rows) {
|
|
104
|
+
const pub = publicationMap.get(row.publication);
|
|
105
|
+
if (pub) {
|
|
106
|
+
pub.schemas.push(row.schema_name);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Array.from(publicationMap.values());
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const ROLES_QUERY = `
|
|
2
|
+
SELECT
|
|
3
|
+
r.oid,
|
|
4
|
+
r.rolname AS name,
|
|
5
|
+
r.rolsuper AS is_superuser,
|
|
6
|
+
r.rolinherit AS inherit,
|
|
7
|
+
r.rolcreaterole AS can_create_role,
|
|
8
|
+
r.rolcreatedb AS can_create_db,
|
|
9
|
+
r.rolcanlogin AS can_login,
|
|
10
|
+
r.rolreplication AS is_replication,
|
|
11
|
+
r.rolconnlimit AS conn_limit,
|
|
12
|
+
r.rolvaliduntil AS valid_until,
|
|
13
|
+
pg_catalog.shobj_description(r.oid, 'pg_authid') AS comment
|
|
14
|
+
FROM pg_catalog.pg_roles r
|
|
15
|
+
WHERE r.rolname NOT LIKE 'pg_%'
|
|
16
|
+
ORDER BY r.rolname
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const ROLE_MEMBERSHIPS_QUERY = `
|
|
20
|
+
SELECT
|
|
21
|
+
m.roleid::regrole::text AS role,
|
|
22
|
+
m.member::regrole::text AS member,
|
|
23
|
+
m.admin_option AS admin_option
|
|
24
|
+
FROM pg_catalog.pg_auth_members m
|
|
25
|
+
ORDER BY m.roleid, m.member
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('pg').Pool} pool
|
|
30
|
+
* @returns {Promise<Array>}
|
|
31
|
+
*/
|
|
32
|
+
export async function queryRoles(pool) {
|
|
33
|
+
try {
|
|
34
|
+
const [rolesResult, membershipsResult] = await Promise.all([
|
|
35
|
+
pool.query(ROLES_QUERY),
|
|
36
|
+
pool.query(ROLE_MEMBERSHIPS_QUERY),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const membershipMap = new Map();
|
|
40
|
+
for (const m of membershipsResult.rows) {
|
|
41
|
+
if (!membershipMap.has(m.role)) {
|
|
42
|
+
membershipMap.set(m.role, []);
|
|
43
|
+
}
|
|
44
|
+
membershipMap.get(m.role).push({
|
|
45
|
+
member: m.member,
|
|
46
|
+
admin_option: m.admin_option,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return rolesResult.rows.map(row => ({
|
|
51
|
+
name: row.name,
|
|
52
|
+
is_superuser: row.is_superuser,
|
|
53
|
+
inherit: row.inherit,
|
|
54
|
+
can_create_role: row.can_create_role,
|
|
55
|
+
can_create_db: row.can_create_db,
|
|
56
|
+
can_login: row.can_login,
|
|
57
|
+
is_replication: row.is_replication,
|
|
58
|
+
conn_limit: row.conn_limit,
|
|
59
|
+
valid_until: row.valid_until,
|
|
60
|
+
comment: row.comment || undefined,
|
|
61
|
+
memberships: membershipMap.get(row.name) || [],
|
|
62
|
+
}));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const RULES_QUERY = `
|
|
2
|
+
SELECT
|
|
3
|
+
r.oid,
|
|
4
|
+
n.nspname AS schema,
|
|
5
|
+
c.relname AS table,
|
|
6
|
+
r.rulename AS name,
|
|
7
|
+
pg_catalog.pg_get_ruledef(r.oid) AS definition,
|
|
8
|
+
CASE r.ev_type
|
|
9
|
+
WHEN '1' THEN 'SELECT'
|
|
10
|
+
WHEN '2' THEN 'UPDATE'
|
|
11
|
+
WHEN '3' THEN 'INSERT'
|
|
12
|
+
WHEN '4' THEN 'DELETE'
|
|
13
|
+
END AS event,
|
|
14
|
+
r.is_instead AS is_instead,
|
|
15
|
+
r.ev_enabled AS is_enabled,
|
|
16
|
+
pg_catalog.pg_get_expr(r.ev_qual, r.ev_class) AS qual,
|
|
17
|
+
pg_catalog.obj_description(r.oid, 'pg_rewrite') AS comment
|
|
18
|
+
FROM pg_catalog.pg_rewrite r
|
|
19
|
+
JOIN pg_catalog.pg_class c ON c.oid = r.ev_class
|
|
20
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
21
|
+
WHERE r.rulename != '_RETURN'
|
|
22
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
23
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
24
|
+
ORDER BY n.nspname, c.relname, r.rulename
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {import('pg').Pool} pool
|
|
29
|
+
* @returns {Promise<Array>}
|
|
30
|
+
*/
|
|
31
|
+
export async function queryRules(pool) {
|
|
32
|
+
try {
|
|
33
|
+
const result = await pool.query(RULES_QUERY);
|
|
34
|
+
|
|
35
|
+
return result.rows.map(row => ({
|
|
36
|
+
schema: row.schema,
|
|
37
|
+
table: row.table,
|
|
38
|
+
name: row.name,
|
|
39
|
+
definition: row.definition,
|
|
40
|
+
event: row.event,
|
|
41
|
+
is_instead: row.is_instead,
|
|
42
|
+
is_enabled: row.is_enabled === 'O' || row.is_enabled === 'A',
|
|
43
|
+
qual: row.qual,
|
|
44
|
+
comment: row.comment,
|
|
45
|
+
}));
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SEQUENCES_QUERY = `
|
|
2
|
+
SELECT s.relname AS name,
|
|
3
|
+
n.nspname AS schema,
|
|
4
|
+
s.relowner::regrole::text AS owner,
|
|
5
|
+
seq.seqtypid::regtype::text AS data_type,
|
|
6
|
+
seq.seqstart AS start_value,
|
|
7
|
+
seq.seqincrement AS increment,
|
|
8
|
+
seq.seqmin AS min_value,
|
|
9
|
+
seq.seqmax AS max_value,
|
|
10
|
+
seq.seqcache AS cache,
|
|
11
|
+
seq.seqcycle AS cycle,
|
|
12
|
+
CASE
|
|
13
|
+
WHEN d.refobjid IS NOT NULL THEN
|
|
14
|
+
d.refobjid::regclass::text || '.' || a.attname
|
|
15
|
+
ELSE NULL
|
|
16
|
+
END AS owned_by,
|
|
17
|
+
CASE WHEN s.reltablespace != 0 THEN (SELECT spcname FROM pg_catalog.pg_tablespace WHERE oid = s.reltablespace) ELSE NULL END AS tablespace,
|
|
18
|
+
pg_catalog.obj_description(s.oid, 'pg_class') AS comment,
|
|
19
|
+
pg_catalog.pg_sequence_last_value(seq.seqrelid) AS current_value
|
|
20
|
+
FROM pg_catalog.pg_sequence seq
|
|
21
|
+
JOIN pg_catalog.pg_class s ON s.oid = seq.seqrelid
|
|
22
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = s.relnamespace
|
|
23
|
+
LEFT JOIN pg_catalog.pg_depend d ON d.objid = s.oid AND d.deptype = 'a' AND d.classid = 'pg_class'::regclass
|
|
24
|
+
LEFT JOIN pg_catalog.pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
|
|
25
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
26
|
+
AND n.nspname NOT LIKE 'pg_temp_%'
|
|
27
|
+
ORDER BY n.nspname, s.relname
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {import('pg').Pool} pool
|
|
32
|
+
* @returns {Promise<Array>}
|
|
33
|
+
*/
|
|
34
|
+
export async function querySequences(pool) {
|
|
35
|
+
const result = await pool.query(SEQUENCES_QUERY);
|
|
36
|
+
return result.rows;
|
|
37
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const STATISTICS_QUERY = `
|
|
2
|
+
SELECT
|
|
3
|
+
s.oid,
|
|
4
|
+
n.nspname AS schema,
|
|
5
|
+
s.stxname AS name,
|
|
6
|
+
pg_catalog.pg_get_userbyid(s.stxowner) AS owner,
|
|
7
|
+
s.stxkind AS kinds,
|
|
8
|
+
s.stxkeys AS columns_raw,
|
|
9
|
+
c.relname AS table_name,
|
|
10
|
+
n2.nspname AS table_schema,
|
|
11
|
+
pg_catalog.pg_get_statisticsobjdef(s.oid) AS definition,
|
|
12
|
+
s.stxstattarget AS statistics_target,
|
|
13
|
+
pg_catalog.obj_description(s.oid, 'pg_statistic_ext') AS comment,
|
|
14
|
+
pg_catalog.pg_relation_size(s.oid) AS size
|
|
15
|
+
FROM pg_catalog.pg_statistic_ext s
|
|
16
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = s.stxnamespace
|
|
17
|
+
JOIN pg_catalog.pg_class c ON c.oid = s.stxrelid
|
|
18
|
+
JOIN pg_catalog.pg_namespace n2 ON n2.oid = c.relnamespace
|
|
19
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
20
|
+
ORDER BY n.nspname, s.stxname
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const KIND_MAP = {
|
|
24
|
+
'd': 'ndistinct',
|
|
25
|
+
'f': 'dependencies',
|
|
26
|
+
'm': 'mcv',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {import('pg').Pool} pool
|
|
31
|
+
* @param {number} version
|
|
32
|
+
* @returns {Promise<Array>}
|
|
33
|
+
*/
|
|
34
|
+
export async function queryStatistics(pool, version) {
|
|
35
|
+
if (version < 100000) return [];
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await pool.query(STATISTICS_QUERY);
|
|
39
|
+
|
|
40
|
+
return result.rows.map(row => {
|
|
41
|
+
let kinds = [];
|
|
42
|
+
if (row.kinds && typeof row.kinds === 'string') {
|
|
43
|
+
kinds = row.kinds.split('').map(k => KIND_MAP[k] || k);
|
|
44
|
+
} else if (Array.isArray(row.kinds)) {
|
|
45
|
+
kinds = row.kinds.map(k => KIND_MAP[k] || k);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let columns = [];
|
|
49
|
+
if (row.columns_raw) {
|
|
50
|
+
if (typeof row.columns_raw === 'string') {
|
|
51
|
+
columns = row.columns_raw.split(' ').filter(Boolean);
|
|
52
|
+
} else if (Array.isArray(row.columns_raw)) {
|
|
53
|
+
columns = row.columns_raw.map(String);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
schema: row.schema,
|
|
59
|
+
name: row.name,
|
|
60
|
+
owner: row.owner,
|
|
61
|
+
kinds,
|
|
62
|
+
columns,
|
|
63
|
+
table_name: row.table_name,
|
|
64
|
+
table_schema: row.table_schema,
|
|
65
|
+
definition: row.definition,
|
|
66
|
+
statisticsTarget: row.statistics_target ?? undefined,
|
|
67
|
+
comment: row.comment || undefined,
|
|
68
|
+
size: row.size ?? undefined,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const SUBSCRIPTIONS_QUERY = `
|
|
2
|
+
SELECT
|
|
3
|
+
s.oid,
|
|
4
|
+
s.subname AS name,
|
|
5
|
+
pg_catalog.pg_get_userbyid(s.subowner) AS owner,
|
|
6
|
+
s.subconninfo AS conninfo,
|
|
7
|
+
s.subslotname AS slot_name,
|
|
8
|
+
s.subsynccommit AS sync_commit,
|
|
9
|
+
s.subenabled AS enabled,
|
|
10
|
+
s.subpublications AS publications,
|
|
11
|
+
s.subbinary AS binary_transfer,
|
|
12
|
+
s.substream AS streaming,
|
|
13
|
+
s.subtwophasestate AS two_phase_state,
|
|
14
|
+
CASE WHEN s.subdisableonerr THEN true ELSE false END AS disable_on_error,
|
|
15
|
+
s.suborigin AS origin,
|
|
16
|
+
pg_catalog.obj_description(s.oid, 'pg_subscription') AS comment
|
|
17
|
+
FROM pg_catalog.pg_subscription s
|
|
18
|
+
ORDER BY s.subname
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const SUBSCRIPTION_TABLES_QUERY = `
|
|
22
|
+
SELECT
|
|
23
|
+
s.subname AS subscription,
|
|
24
|
+
n.nspname AS table_schema,
|
|
25
|
+
c.relname AS table_name,
|
|
26
|
+
sr.srsubstate AS state
|
|
27
|
+
FROM pg_catalog.pg_subscription_rel sr
|
|
28
|
+
JOIN pg_catalog.pg_subscription s ON s.oid = sr.srsubid
|
|
29
|
+
JOIN pg_catalog.pg_class c ON c.oid = sr.srrelid
|
|
30
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
31
|
+
ORDER BY s.subname, n.nspname, c.relname
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('pg').Pool} pool
|
|
36
|
+
* @param {number} version
|
|
37
|
+
* @returns {Promise<Array>}
|
|
38
|
+
*/
|
|
39
|
+
export async function querySubscriptions(pool, version) {
|
|
40
|
+
if (version < 100000) return [];
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const subsResult = await pool.query(SUBSCRIPTIONS_QUERY);
|
|
44
|
+
let tablesResult = { rows: [] };
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
tablesResult = await pool.query(SUBSCRIPTION_TABLES_QUERY);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Table may not exist if no subscription tables
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const subMap = new Map();
|
|
53
|
+
|
|
54
|
+
for (const row of subsResult.rows) {
|
|
55
|
+
let publications = [];
|
|
56
|
+
if (row.publications && typeof row.publications === 'string') {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(row.publications);
|
|
59
|
+
publications = Array.isArray(parsed) ? parsed : [parsed];
|
|
60
|
+
} catch {
|
|
61
|
+
publications = row.publications.split(',').map(s => s.trim()).filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
} else if (Array.isArray(row.publications)) {
|
|
64
|
+
publications = row.publications;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
subMap.set(row.name, {
|
|
68
|
+
name: row.name,
|
|
69
|
+
owner: row.owner,
|
|
70
|
+
conninfo: row.conninfo,
|
|
71
|
+
slot_name: row.slot_name,
|
|
72
|
+
sync_commit: row.sync_commit,
|
|
73
|
+
enabled: row.enabled,
|
|
74
|
+
publications,
|
|
75
|
+
binary_transfer: row.binary_transfer || false,
|
|
76
|
+
streaming: row.streaming || false,
|
|
77
|
+
two_phase: row.two_phase_state && row.two_phase_state !== 'd',
|
|
78
|
+
disable_on_error: row.disable_on_error || false,
|
|
79
|
+
origin: row.origin || 'any',
|
|
80
|
+
comment: row.comment,
|
|
81
|
+
tables: [],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const row of tablesResult.rows) {
|
|
86
|
+
const sub = subMap.get(row.subscription);
|
|
87
|
+
if (sub) {
|
|
88
|
+
sub.tables.push({
|
|
89
|
+
table: `${row.table_schema}.${row.table_name}`,
|
|
90
|
+
state: row.state,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Array.from(subMap.values());
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|