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.
Files changed (104) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +304 -0
  3. package/package.json +52 -0
  4. package/src/behavioral/behavioral-applier.js +374 -0
  5. package/src/behavioral/behavioral-extractor.js +266 -0
  6. package/src/behavioral/behavioral-puller.js +35 -0
  7. package/src/behavioral/index.js +4 -0
  8. package/src/behavioral/phase-sorter.js +24 -0
  9. package/src/constants.js +97 -0
  10. package/src/ddl-generator/alter-generator.js +1512 -0
  11. package/src/ddl-generator/comment-generator.js +95 -0
  12. package/src/ddl-generator/create-generator.js +1426 -0
  13. package/src/ddl-generator/drop-generator.js +194 -0
  14. package/src/ddl-generator/generator.js +78 -0
  15. package/src/ddl-generator/grant-generator.js +31 -0
  16. package/src/ddl-generator/index.js +3 -0
  17. package/src/ddl-generator/pg-version.js +15 -0
  18. package/src/ddl-generator/rename-generator.js +88 -0
  19. package/src/ddl-generator/safe-patterns.js +241 -0
  20. package/src/differ/change-classifier.js +192 -0
  21. package/src/differ/dependency-resolver.js +523 -0
  22. package/src/differ/index.js +12 -0
  23. package/src/differ/object-matcher.js +510 -0
  24. package/src/differ/property-differ.js +1134 -0
  25. package/src/differ/risk-tagger.js +561 -0
  26. package/src/differ/schema-differ.js +309 -0
  27. package/src/differ/utils/levenshtein.js +210 -0
  28. package/src/differ/utils/path-builder.js +198 -0
  29. package/src/differ/utils/type-compatibility.js +472 -0
  30. package/src/errors.js +147 -0
  31. package/src/executor/drift-detector.js +221 -0
  32. package/src/executor/index.js +7 -0
  33. package/src/executor/lock-manager.js +308 -0
  34. package/src/executor/migration-executor.js +1249 -0
  35. package/src/executor/progress-tracker.js +81 -0
  36. package/src/executor/recovery-manager.js +47 -0
  37. package/src/executor/snapshot-manager.js +22 -0
  38. package/src/executor/sql-splitter.js +120 -0
  39. package/src/executor/transaction-manager.js +288 -0
  40. package/src/index.js +483 -0
  41. package/src/introspection/index.js +4 -0
  42. package/src/introspection/introspector.js +250 -0
  43. package/src/introspection/queries/access-methods.js +29 -0
  44. package/src/introspection/queries/casts.js +38 -0
  45. package/src/introspection/queries/collations.js +51 -0
  46. package/src/introspection/queries/comments.js +66 -0
  47. package/src/introspection/queries/constraints.js +65 -0
  48. package/src/introspection/queries/conversions.js +39 -0
  49. package/src/introspection/queries/databases.js +43 -0
  50. package/src/introspection/queries/default-privileges.js +37 -0
  51. package/src/introspection/queries/event-triggers.js +43 -0
  52. package/src/introspection/queries/extensions.js +21 -0
  53. package/src/introspection/queries/foreign-data.js +136 -0
  54. package/src/introspection/queries/functions.js +75 -0
  55. package/src/introspection/queries/grants.js +38 -0
  56. package/src/introspection/queries/index.js +33 -0
  57. package/src/introspection/queries/indexes.js +99 -0
  58. package/src/introspection/queries/inheritance.js +24 -0
  59. package/src/introspection/queries/multiranges.js +37 -0
  60. package/src/introspection/queries/operators.js +180 -0
  61. package/src/introspection/queries/partitions.js +42 -0
  62. package/src/introspection/queries/pg18-19.js +43 -0
  63. package/src/introspection/queries/policies.js +35 -0
  64. package/src/introspection/queries/procedural-languages.js +36 -0
  65. package/src/introspection/queries/publications.js +114 -0
  66. package/src/introspection/queries/roles.js +66 -0
  67. package/src/introspection/queries/rules.js +49 -0
  68. package/src/introspection/queries/sequences.js +37 -0
  69. package/src/introspection/queries/statistics.js +74 -0
  70. package/src/introspection/queries/subscriptions.js +99 -0
  71. package/src/introspection/queries/tables.js +151 -0
  72. package/src/introspection/queries/tablespaces.js +36 -0
  73. package/src/introspection/queries/text-search.js +146 -0
  74. package/src/introspection/queries/triggers.js +61 -0
  75. package/src/introspection/queries/types.js +106 -0
  76. package/src/introspection/queries/views.js +146 -0
  77. package/src/introspection/translator.js +1283 -0
  78. package/src/introspection/version-detector.js +25 -0
  79. package/src/planner/backfill-planner.js +36 -0
  80. package/src/planner/dry-run.js +21 -0
  81. package/src/planner/index.js +6 -0
  82. package/src/planner/migration-planner.js +356 -0
  83. package/src/planner/smart-migrator.js +82 -0
  84. package/src/planner/step-sequencer.js +61 -0
  85. package/src/planner/type-registry.js +47 -0
  86. package/src/risk/compatibility-checker.js +29 -0
  87. package/src/risk/data-loss-checker.js +32 -0
  88. package/src/risk/destructive-checker.js +38 -0
  89. package/src/risk/index.js +6 -0
  90. package/src/risk/lock-analyzer.js +39 -0
  91. package/src/risk/recommendations.js +44 -0
  92. package/src/risk/risk-engine.js +25 -0
  93. package/src/storage/index.js +5 -0
  94. package/src/storage/memory-storage.js +87 -0
  95. package/src/storage/migration-table.js +423 -0
  96. package/src/storage/rollback-generator.js +344 -0
  97. package/src/types/changes.js +136 -0
  98. package/src/types/connection.js +17 -0
  99. package/src/types/execution.js +107 -0
  100. package/src/types/index.js +1 -0
  101. package/src/types/migration.js +67 -0
  102. package/src/types/risk.js +32 -0
  103. package/src/types/schema.d.ts +1004 -0
  104. 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
+ }