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,250 @@
1
+ import { detectPgVersion, majorVersion } from './version-detector.js';
2
+ import { translateSnapshot } from './translator.js';
3
+ import * as queries from './queries/index.js';
4
+
5
+ const SYSTEM_SCHEMAS = ['pg_catalog', 'information_schema', 'pg_toast'];
6
+
7
+ export class SchemaIntrospector {
8
+ /** @param {import('pg').Pool} pool */
9
+ constructor(pool) {
10
+ this.pool = pool;
11
+ this.logger = console;
12
+ }
13
+
14
+ /**
15
+ * @param {Object} [options]
16
+ * @param {string[]} [options.schemas] - Specific schemas to introspect (default: all user schemas)
17
+ * @returns {Promise<import('../types/schema.js').SchemaSnapshot>}
18
+ */
19
+ async introspect(options = {}) {
20
+ const version = await detectPgVersion(this.pool);
21
+ const pgMajor = majorVersion(version);
22
+ const versionStr = await this.detectVersionString();
23
+
24
+ // Resolve target schemas
25
+ const targetSchemas = options.schemas || await this.resolveUserSchemas();
26
+
27
+ // Database-level objects (queried once, not per-schema)
28
+ const [
29
+ roles,
30
+ tablespaces,
31
+ accessMethods,
32
+ databases,
33
+ casts,
34
+ proceduralLanguages,
35
+ defaultPrivileges,
36
+ foreignDataWrappers,
37
+ foreignServers,
38
+ userMappings,
39
+ eventTriggers,
40
+ publications,
41
+ subscriptions,
42
+ ] = await Promise.all([
43
+ queries.queryRoles(this.pool).catch(() => []),
44
+ queries.queryInterfaceTablespaces(this.pool).catch(() => []),
45
+ queries.queryInterfaceAccessMethods(this.pool).catch(() => []),
46
+ queries.queryInterfaceDatabases(this.pool).catch(() => []),
47
+ queries.queryCasts(this.pool).catch(() => []),
48
+ queries.queryProceduralLanguages(this.pool).catch(() => []),
49
+ queries.queryDefaultPrivileges(this.pool).catch(() => []),
50
+ queries.queryForeignDataWrappers(this.pool).catch(() => []),
51
+ queries.queryForeignServers(this.pool).catch(() => []),
52
+ queries.queryUserMappings(this.pool).catch(() => []),
53
+ queries.queryEventTriggers(this.pool, version).catch(() => []),
54
+ queries.queryPublications(this.pool, version).catch(() => []),
55
+ queries.querySubscriptions(this.pool, version).catch(() => []),
56
+ ]);
57
+
58
+ // Per-schema queries (parallelized)
59
+ const [
60
+ schemasResult,
61
+ tables,
62
+ columns,
63
+ constraints,
64
+ indexes,
65
+ indexColumns,
66
+ functions,
67
+ triggers,
68
+ typesResult,
69
+ viewsResult,
70
+ sequences,
71
+ partitions,
72
+ policies,
73
+ extensions,
74
+ inheritance,
75
+ comments,
76
+ grants,
77
+ statistics,
78
+ collations,
79
+ conversions,
80
+ operators,
81
+ operatorClasses,
82
+ operatorFamilies,
83
+ textSearchConfigs,
84
+ textSearchDictionaries,
85
+ textSearchParsers,
86
+ textSearchTemplates,
87
+ foreignTables,
88
+ multiranges,
89
+ rules,
90
+ aggregates,
91
+ procedures,
92
+ toastOptions,
93
+ ] = await Promise.all([
94
+ queries.querySchemas(this.pool).then(r => r.filter(s => targetSchemas.includes(s.name))).catch(() => []),
95
+ queries.queryTables(this.pool, version).then(r => r.filter(t => targetSchemas.includes(t.schema))).catch(() => []),
96
+ queries.queryColumns(this.pool, version).then(r => r.filter(c => targetSchemas.includes(c.schema))).catch(() => []),
97
+ queries.queryConstraints(this.pool).then(r => r.filter(c => targetSchemas.includes(c.schema))).catch(() => []),
98
+ queries.queryIndexes(this.pool).then(r => r.filter(i => targetSchemas.includes(i.schema))).catch(() => []),
99
+ queries.queryIndexColumns(this.pool).then(r => r.filter(i => targetSchemas.includes(i.schema))).catch(() => []),
100
+ queries.queryFunctions(this.pool).then(r => r.filter(f => targetSchemas.includes(f.schema))).catch(() => []),
101
+ queries.queryTriggers(this.pool).then(r => r.filter(t => targetSchemas.includes(t.schema))).catch(() => []),
102
+ queries.queryTypes(this.pool).then(r => ({
103
+ enums: (r.enums || []).filter(e => targetSchemas.includes(e.schema)),
104
+ composites: (r.composites || []).filter(c => targetSchemas.includes(c.schema)),
105
+ domains: (r.domains || []).filter(d => targetSchemas.includes(d.schema)),
106
+ ranges: (r.ranges || []).filter(r => targetSchemas.includes(r.schema)),
107
+ })).catch(() => ({ enums: [], composites: [], domains: [], ranges: [] })),
108
+ queries.queryViews(this.pool).then(r => r).catch(() => ({ views: [], materializedViews: [] })),
109
+ queries.querySequences(this.pool).then(r => r.filter(s => targetSchemas.includes(s.schema))).catch(() => []),
110
+ queries.queryPartitions(this.pool).then(r => r.filter(p => targetSchemas.includes(p.child_schema))).catch(() => []),
111
+ queries.queryPolicies(this.pool).then(r => r.filter(p => targetSchemas.includes(p.schema))).catch(() => []),
112
+ queries.queryExtensions(this.pool).catch(() => []),
113
+ queries.queryInheritance(this.pool).then(r => r.filter(i => targetSchemas.includes(i.child_schema))).catch(() => []),
114
+ queries.queryComments(this.pool).catch(() => ({})),
115
+ queries.queryGrants(this.pool).then(r => r.filter(g => targetSchemas.includes(g.schema))).catch(() => []),
116
+ queries.queryStatistics(this.pool, version).then(r => r.filter(s => targetSchemas.includes(s.schema))).catch(() => []),
117
+ queries.queryCollations(this.pool).then(r => r.filter(c => targetSchemas.includes(c.schema))).catch(() => []),
118
+ queries.queryConversions(this.pool).then(r => r.filter(c => targetSchemas.includes(c.schema))).catch(() => []),
119
+ queries.queryOperators(this.pool).then(r => r.filter(o => targetSchemas.includes(o.schema))).catch(() => []),
120
+ queries.queryOperatorClasses(this.pool).then(r => r.filter(o => targetSchemas.includes(o.schema))).catch(() => []),
121
+ queries.queryOperatorFamilies(this.pool).then(r => r.filter(o => targetSchemas.includes(o.schema))).catch(() => []),
122
+ queries.queryTextSearchConfigs(this.pool).then(r => ({
123
+ configs: (r.configs || []).filter(t => targetSchemas.includes(t.schema)),
124
+ tokenMappings: (r.tokenMappings || []).filter(t => targetSchemas.includes(t.schema)),
125
+ })).catch(() => ({ configs: [], tokenMappings: [] })),
126
+ queries.queryTextSearchDictionaries(this.pool).then(r => r.filter(t => targetSchemas.includes(t.schema))).catch(() => []),
127
+ queries.queryTextSearchParsers(this.pool).then(r => r.filter(t => targetSchemas.includes(t.schema))).catch(() => []),
128
+ queries.queryTextSearchTemplates(this.pool).then(r => r.filter(t => targetSchemas.includes(t.schema))).catch(() => []),
129
+ queries.queryForeignTables(this.pool).then(r => ({
130
+ tables: (r.tables || []).filter(f => targetSchemas.includes(f.schema)),
131
+ columnOptions: (r.columnOptions || []).filter(c => targetSchemas.includes(c.schema)),
132
+ })).catch(() => ({ tables: [], columnOptions: [] })),
133
+ queries.queryMultiranges(this.pool, version).then(r => r.filter(m => targetSchemas.includes(m.schema))).catch(() => []),
134
+ queries.queryRules(this.pool).then(r => r.filter(r => targetSchemas.includes(r.schema))).catch(() => []),
135
+ queries.queryFunctions(this.pool).then(r => r.filter(f => targetSchemas.includes(f.schema) && f.kind === 'AGGREGATE')).catch(() => []),
136
+ queries.queryFunctions(this.pool).then(r => r.filter(f => targetSchemas.includes(f.schema) && f.kind === 'PROCEDURE')).catch(() => []),
137
+ queries.queryToastOptions(this.pool).then(r => r.filter(t => targetSchemas.includes(t.schema))).catch(() => []),
138
+ ]);
139
+
140
+ // PG18+ features
141
+ let pg18Features = { notEnforced: [], virtualColumns: [] };
142
+ if (pgMajor >= 18) {
143
+ pg18Features = await queries.queryPg18Features(this.pool).catch(() => ({ notEnforced: [], virtualColumns: [] }));
144
+ }
145
+
146
+ // Current database info
147
+ let databaseInfo = null;
148
+ try {
149
+ const dbResult = await this.pool.query(`
150
+ SELECT
151
+ current_database() AS name,
152
+ pg_catalog.pg_get_userbyid(datdba) AS owner,
153
+ pg_catalog.pg_encoding_to_char(encoding) AS encoding,
154
+ datcollate AS collate,
155
+ datctype AS ctype
156
+ FROM pg_catalog.pg_database
157
+ WHERE datname = current_database()
158
+ `);
159
+ databaseInfo = dbResult.rows[0] || null;
160
+ } catch {
161
+ databaseInfo = null;
162
+ }
163
+
164
+ // Translate raw results to SchemaSnapshot
165
+ const snapshot = translateSnapshot({
166
+ version: { numeric: version, major: pgMajor, string: versionStr },
167
+ database: databaseInfo,
168
+ schemas: schemasResult,
169
+ tables,
170
+ columns,
171
+ constraints,
172
+ indexes,
173
+ indexColumns,
174
+ functions,
175
+ triggers,
176
+ types: {
177
+ ...typesResult,
178
+ multiranges,
179
+ },
180
+ views: viewsResult.views || [],
181
+ materializedViews: viewsResult.materializedViews || [],
182
+ sequences,
183
+ partitions,
184
+ policies,
185
+ extensions,
186
+ inheritance,
187
+ comments,
188
+ grants,
189
+ pg18Features,
190
+ // New object types
191
+ publications,
192
+ subscriptions,
193
+ statistics,
194
+ collations,
195
+ conversions,
196
+ operators,
197
+ operatorClasses,
198
+ operatorFamilies,
199
+ textSearchConfigs,
200
+ textSearchDictionaries,
201
+ textSearchParsers,
202
+ textSearchTemplates,
203
+ foreignDataWrappers,
204
+ foreignServers,
205
+ userMappings,
206
+ foreignTables,
207
+ casts,
208
+ eventTriggers,
209
+ rules,
210
+ roles,
211
+ tablespaces,
212
+ accessMethods,
213
+ proceduralLanguages,
214
+ defaultPrivileges,
215
+ databases,
216
+ aggregates,
217
+ procedures,
218
+ toastOptions,
219
+ });
220
+
221
+ return snapshot;
222
+ }
223
+
224
+ /**
225
+ * Resolve user schemas (excluding system schemas).
226
+ * @returns {Promise<string[]>}
227
+ */
228
+ async resolveUserSchemas() {
229
+ const result = await this.pool.query(`
230
+ SELECT n.nspname AS name
231
+ FROM pg_catalog.pg_namespace n
232
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
233
+ AND n.nspname NOT LIKE 'pg_temp_%'
234
+ AND n.nspname NOT LIKE 'pg_toast_temp_%'
235
+ ORDER BY n.nspname
236
+ `);
237
+ return result.rows.map(r => r.name);
238
+ }
239
+
240
+ /** @returns {Promise<number>} */
241
+ async detectVersion() {
242
+ return detectPgVersion(this.pool);
243
+ }
244
+
245
+ /** @returns {Promise<string>} */
246
+ async detectVersionString() {
247
+ const result = await this.pool.query("SELECT current_setting('server_version') AS version");
248
+ return result.rows[0].version;
249
+ }
250
+ }
@@ -0,0 +1,29 @@
1
+ const ACCESS_METHODS_QUERY = `
2
+ SELECT
3
+ a.oid,
4
+ a.amname AS name,
5
+ a.amtype AS type,
6
+ a.amhandler::regproc::text AS handler
7
+ FROM pg_catalog.pg_am a
8
+ WHERE a.amname NOT IN ('heap', 'btree', 'hash', 'gist', 'gin', 'spgist', 'brin')
9
+ OR a.amtype = 't'
10
+ ORDER BY a.amname
11
+ `;
12
+
13
+ /**
14
+ * @param {import('pg').Pool} pool
15
+ * @returns {Promise<Array>}
16
+ */
17
+ export async function queryInterfaceAccessMethods(pool) {
18
+ try {
19
+ const result = await pool.query(ACCESS_METHODS_QUERY);
20
+
21
+ return result.rows.map(row => ({
22
+ name: row.name,
23
+ type: row.type === 't' ? 'TABLE' : 'INDEX',
24
+ handler: row.handler || undefined,
25
+ }));
26
+ } catch (error) {
27
+ return [];
28
+ }
29
+ }
@@ -0,0 +1,38 @@
1
+ const CASTS_QUERY = `
2
+ SELECT
3
+ c.oid,
4
+ format_type(c.castsource, NULL) AS source_type,
5
+ format_type(c.casttarget, NULL) AS target_type,
6
+ CASE WHEN c.castfunc = 0 THEN NULL ELSE c.castfunc::regproc::text END AS function,
7
+ CASE c.castcontext
8
+ WHEN 'e' THEN 'EXPLICIT'
9
+ WHEN 'a' THEN 'ASSIGNMENT'
10
+ WHEN 'i' THEN 'IMPLICIT'
11
+ END AS context,
12
+ CASE c.castmethod
13
+ WHEN 'f' THEN 'FUNCTION'
14
+ WHEN 'i' THEN 'INOUT'
15
+ WHEN 'b' THEN 'BINARY'
16
+ END AS method,
17
+ pg_catalog.obj_description(c.oid, 'pg_cast') AS comment
18
+ FROM pg_catalog.pg_cast c
19
+ WHERE c.castsource NOT IN (
20
+ SELECT oid FROM pg_type WHERE typnamespace = 'pg_catalog'::regnamespace
21
+ ) AND c.casttarget NOT IN (
22
+ SELECT oid FROM pg_type WHERE typnamespace = 'pg_catalog'::regnamespace
23
+ )
24
+ ORDER BY source_type, target_type
25
+ `;
26
+
27
+ /**
28
+ * @param {import('pg').Pool} pool
29
+ * @returns {Promise<Array>}
30
+ */
31
+ export async function queryCasts(pool) {
32
+ try {
33
+ const result = await pool.query(CASTS_QUERY);
34
+ return result.rows;
35
+ } catch (error) {
36
+ return [];
37
+ }
38
+ }
@@ -0,0 +1,51 @@
1
+ const COLLATIONS_QUERY = `
2
+ SELECT
3
+ c.oid,
4
+ n.nspname AS schema,
5
+ c.collname AS name,
6
+ pg_catalog.pg_get_userbyid(c.collowner) AS owner,
7
+ CASE c.collprovider
8
+ WHEN 'd' THEN 'icu'
9
+ WHEN 'c' THEN 'libc'
10
+ WHEN 'i' THEN 'icu'
11
+ END AS provider,
12
+ c.collcollate AS lc_collate,
13
+ c.collctype AS lc_ctype,
14
+ c.collcollate AS locale,
15
+ c.collencoding AS encoding,
16
+ COALESCE(c.collisdeterministic, true) AS is_deterministic,
17
+ c.collversion AS version,
18
+ pg_catalog.obj_description(c.oid, 'pg_collation') AS comment
19
+ FROM pg_catalog.pg_collation c
20
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.collnamespace
21
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
22
+ AND c.collname NOT LIKE 'pg_%'
23
+ ORDER BY n.nspname, c.collname
24
+ `;
25
+
26
+ /**
27
+ * @param {import('pg').Pool} pool
28
+ * @param {number} version
29
+ * @returns {Promise<Array>}
30
+ */
31
+ export async function queryCollations(pool, version) {
32
+ try {
33
+ const result = await pool.query(COLLATIONS_QUERY);
34
+
35
+ return result.rows.map(row => ({
36
+ schema: row.schema,
37
+ name: row.name,
38
+ owner: row.owner,
39
+ provider: row.provider,
40
+ locale: row.locale,
41
+ lcCollate: row.lc_collate || undefined,
42
+ lcCtype: row.lc_ctype || undefined,
43
+ encoding: row.encoding,
44
+ isDeterministic: row.is_deterministic,
45
+ version: row.version || undefined,
46
+ comment: row.comment || undefined,
47
+ }));
48
+ } catch (error) {
49
+ return [];
50
+ }
51
+ }
@@ -0,0 +1,66 @@
1
+ const TABLE_COMMENTS_QUERY = `
2
+ SELECT n.nspname || '.' || c.relname AS object_key,
3
+ pg_catalog.obj_description(c.oid, 'pg_class') AS comment
4
+ FROM pg_catalog.pg_class c
5
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
6
+ WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f')
7
+ AND pg_catalog.obj_description(c.oid, 'pg_class') IS NOT NULL
8
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
9
+ AND n.nspname NOT LIKE 'pg_temp_%'
10
+ `;
11
+
12
+ const COLUMN_COMMENTS_QUERY = `
13
+ SELECT n.nspname || '.' || c.relname || '.' || a.attname AS object_key,
14
+ pg_catalog.col_description(a.attrelid, a.attnum) AS comment
15
+ FROM pg_catalog.pg_attribute a
16
+ JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
17
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
18
+ WHERE a.attnum > 0
19
+ AND NOT a.attisdropped
20
+ AND c.relkind IN ('r', 'p', 'v', 'm')
21
+ AND pg_catalog.col_description(a.attrelid, a.attnum) IS NOT NULL
22
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
23
+ AND n.nspname NOT LIKE 'pg_temp_%'
24
+ `;
25
+
26
+ const TYPE_COMMENTS_QUERY = `
27
+ SELECT n.nspname || '.' || t.typname AS object_key,
28
+ pg_catalog.obj_description(t.oid, 'pg_type') AS comment
29
+ FROM pg_catalog.pg_type t
30
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
31
+ WHERE t.typtype IN ('e', 'c', 'd', 'r')
32
+ AND pg_catalog.obj_description(t.oid, 'pg_type') IS NOT NULL
33
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
34
+ AND n.nspname NOT LIKE 'pg_temp_%'
35
+ `;
36
+
37
+ const FUNCTION_COMMENTS_QUERY = `
38
+ SELECT n.nspname || '.' || p.proname || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ')' AS object_key,
39
+ pg_catalog.obj_description(p.oid, 'pg_proc') AS comment
40
+ FROM pg_catalog.pg_proc p
41
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
42
+ WHERE pg_catalog.obj_description(p.oid, 'pg_proc') IS NOT NULL
43
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
44
+ AND n.nspname NOT LIKE 'pg_temp_%'
45
+ `;
46
+
47
+ /**
48
+ * @param {import('pg').Pool} pool
49
+ * @returns {Promise<Record<string,string>>}
50
+ */
51
+ export async function queryComments(pool) {
52
+ const [tables, columns, types, functions] = await Promise.all([
53
+ pool.query(TABLE_COMMENTS_QUERY).then(r => r.rows),
54
+ pool.query(COLUMN_COMMENTS_QUERY).then(r => r.rows),
55
+ pool.query(TYPE_COMMENTS_QUERY).then(r => r.rows),
56
+ pool.query(FUNCTION_COMMENTS_QUERY).then(r => r.rows),
57
+ ]);
58
+
59
+ const comments = {};
60
+ for (const row of [...tables, ...columns, ...types, ...functions]) {
61
+ if (row.comment) {
62
+ comments[row.object_key] = row.comment;
63
+ }
64
+ }
65
+ return comments;
66
+ }
@@ -0,0 +1,65 @@
1
+ const CONSTRAINTS_QUERY = `
2
+ SELECT c.oid,
3
+ n.nspname AS schema,
4
+ c.conname AS name,
5
+ c.contype AS type,
6
+ c.conrelid::regclass::text AS table_ref,
7
+ CASE WHEN c.confrelid != 0 THEN c.confrelid::regclass::text ELSE NULL END AS foreign_table,
8
+ pg_catalog.pg_get_constraintdef(c.oid, true) AS definition,
9
+ c.convalidated AS is_validated,
10
+ c.condeferrable AS deferrable,
11
+ c.condeferred AS initially_deferred,
12
+ c.conkey AS column_indices,
13
+ c.confkey AS foreign_column_indices,
14
+ c.confupdtype AS on_update,
15
+ c.confdeltype AS on_delete,
16
+ c.confmatchtype AS match_type,
17
+ c.coninhcount > 0 AS is_inherited,
18
+ c.conislocal AS is_local,
19
+ c.connoinherit AS no_inherit,
20
+ pg_catalog.col_description(c.oid, 0) AS comment,
21
+ CASE WHEN c.contype = 'x' THEN pg_catalog.pg_get_expr(c.conbin, c.conrelid) ELSE NULL END AS exclusion_expression
22
+ FROM pg_catalog.pg_constraint c
23
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace
24
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
25
+ AND n.nspname NOT LIKE 'pg_temp_%'
26
+ ORDER BY n.nspname, c.conrelid::regclass::text, c.conname
27
+ `;
28
+
29
+ const FK_ACTION_MAP = {
30
+ 'a': 'NO ACTION',
31
+ 'r': 'RESTRICT',
32
+ 'c': 'CASCADE',
33
+ 'n': 'SET NULL',
34
+ 'd': 'SET DEFAULT',
35
+ };
36
+
37
+ const FK_MATCH_MAP = {
38
+ 'f': 'FULL',
39
+ 'p': 'PARTIAL',
40
+ 's': 'SIMPLE',
41
+ };
42
+
43
+ const CONTYPE_MAP = {
44
+ 'p': 'PRIMARY_KEY',
45
+ 'u': 'UNIQUE',
46
+ 'f': 'FOREIGN_KEY',
47
+ 'c': 'CHECK',
48
+ 'x': 'EXCLUSION',
49
+ 'n': 'NOT_NULL',
50
+ };
51
+
52
+ /**
53
+ * @param {import('pg').Pool} pool
54
+ * @returns {Promise<Array>}
55
+ */
56
+ export async function queryConstraints(pool) {
57
+ const result = await pool.query(CONSTRAINTS_QUERY);
58
+ return result.rows.map(row => ({
59
+ ...row,
60
+ type: CONTYPE_MAP[row.type] || row.type,
61
+ on_update: FK_ACTION_MAP[row.on_update] || row.on_update,
62
+ on_delete: FK_ACTION_MAP[row.on_delete] || row.on_delete,
63
+ match_type: FK_MATCH_MAP[row.match_type] || row.match_type,
64
+ }));
65
+ }
@@ -0,0 +1,39 @@
1
+ const CONVERSIONS_QUERY = `
2
+ SELECT
3
+ c.oid,
4
+ n.nspname AS schema,
5
+ c.conname AS name,
6
+ pg_catalog.pg_get_userbyid(c.conowner) AS owner,
7
+ c.conproc::regproc::text AS proc,
8
+ c.condefault AS is_default,
9
+ pg_catalog.pg_encoding_to_char(c.conforencoding) AS source_encoding,
10
+ pg_catalog.pg_encoding_to_char(c.contoencoding) AS target_encoding,
11
+ pg_catalog.obj_description(c.oid, 'pg_conversion') AS comment
12
+ FROM pg_catalog.pg_conversion c
13
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.connamespace
14
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
15
+ ORDER BY n.nspname, c.conname
16
+ `;
17
+
18
+ /**
19
+ * @param {import('pg').Pool} pool
20
+ * @returns {Promise<Array>}
21
+ */
22
+ export async function queryConversions(pool) {
23
+ try {
24
+ const result = await pool.query(CONVERSIONS_QUERY);
25
+
26
+ return result.rows.map(row => ({
27
+ schema: row.schema,
28
+ name: row.name,
29
+ owner: row.owner,
30
+ proc: row.proc,
31
+ is_default: row.is_default,
32
+ source_encoding: row.source_encoding,
33
+ target_encoding: row.target_encoding,
34
+ comment: row.comment || undefined,
35
+ }));
36
+ } catch (error) {
37
+ return [];
38
+ }
39
+ }
@@ -0,0 +1,43 @@
1
+ const DATABASES_QUERY = `
2
+ SELECT
3
+ d.oid,
4
+ d.datname AS name,
5
+ pg_catalog.pg_get_userbyid(d.datdba) AS owner,
6
+ pg_catalog.pg_encoding_to_char(d.encoding) AS encoding,
7
+ d.datcollate AS collate,
8
+ d.datctype AS ctype,
9
+ d.datistemplate AS is_template,
10
+ d.datallowconn AS allow_conn,
11
+ d.datconnlimit AS conn_limit,
12
+ t.spcname AS tablespace,
13
+ pg_catalog.shobj_description(d.oid, 'pg_database') AS comment
14
+ FROM pg_catalog.pg_database d
15
+ LEFT JOIN pg_catalog.pg_tablespace t ON t.oid = d.dattablespace
16
+ WHERE d.datname NOT LIKE 'template%'
17
+ ORDER BY d.datname
18
+ `;
19
+
20
+ /**
21
+ * @param {import('pg').Pool} pool
22
+ * @returns {Promise<Array>}
23
+ */
24
+ export async function queryInterfaceDatabases(pool) {
25
+ try {
26
+ const result = await pool.query(DATABASES_QUERY);
27
+
28
+ return result.rows.map(row => ({
29
+ name: row.name,
30
+ owner: row.owner,
31
+ encoding: row.encoding,
32
+ collate: row.collate,
33
+ ctype: row.ctype,
34
+ is_template: row.is_template,
35
+ allow_conn: row.allow_conn,
36
+ conn_limit: row.conn_limit,
37
+ tablespace: row.tablespace,
38
+ comment: row.comment || undefined,
39
+ }));
40
+ } catch (error) {
41
+ return [];
42
+ }
43
+ }
@@ -0,0 +1,37 @@
1
+ const DEFAULT_PRIVILEGES_QUERY = `
2
+ SELECT
3
+ n.nspname AS schema,
4
+ pg_catalog.pg_get_userbyid(d.defaclrole) AS role,
5
+ CASE d.defaclobjtype
6
+ WHEN 'r' THEN 'TABLE'
7
+ WHEN 'S' THEN 'SEQUENCE'
8
+ WHEN 'f' THEN 'FUNCTION'
9
+ WHEN 'T' THEN 'TYPE'
10
+ WHEN 's' THEN 'SCHEMA'
11
+ WHEN 'n' THEN 'SCHEMA'
12
+ END AS object_type,
13
+ d.defaclacl AS acl
14
+ FROM pg_catalog.pg_default_acl d
15
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = d.defaclnamespace
16
+ WHERE n.nspname IS NULL OR n.nspname NOT IN ('pg_catalog', 'information_schema')
17
+ ORDER BY n.nspname, d.defaclobjtype, pg_catalog.pg_get_userbyid(d.defaclrole)
18
+ `;
19
+
20
+ /**
21
+ * @param {import('pg').Pool} pool
22
+ * @returns {Promise<Array>}
23
+ */
24
+ export async function queryDefaultPrivileges(pool) {
25
+ try {
26
+ const result = await pool.query(DEFAULT_PRIVILEGES_QUERY);
27
+
28
+ return result.rows.map(row => ({
29
+ schema: row.schema || null,
30
+ role: row.role,
31
+ object_type: row.object_type,
32
+ acl: row.acl || [],
33
+ }));
34
+ } catch (error) {
35
+ return [];
36
+ }
37
+ }
@@ -0,0 +1,43 @@
1
+ const EVENT_TRIGGERS_QUERY = `
2
+ SELECT
3
+ e.oid,
4
+ e.evtname AS name,
5
+ pg_catalog.pg_get_userbyid(e.evtowner) AS owner,
6
+ e.evtevent AS event,
7
+ e.evtfoid::regproc::text AS function,
8
+ CASE e.evtenabled
9
+ WHEN 'O' THEN 'ENABLED'
10
+ WHEN 'D' THEN 'DISABLED'
11
+ WHEN 'R' THEN 'REPLICA'
12
+ WHEN 'A' THEN 'ALWAYS'
13
+ END AS enabled,
14
+ e.evttags AS tags,
15
+ pg_catalog.obj_description(e.oid, 'pg_event_trigger') AS comment
16
+ FROM pg_catalog.pg_event_trigger e
17
+ ORDER BY e.evtname
18
+ `;
19
+
20
+ /**
21
+ * @param {import('pg').Pool} pool
22
+ * @param {number} version
23
+ * @returns {Promise<Array>}
24
+ */
25
+ export async function queryEventTriggers(pool, version) {
26
+ if (version < 90300) return [];
27
+
28
+ try {
29
+ const result = await pool.query(EVENT_TRIGGERS_QUERY);
30
+
31
+ return result.rows.map(row => ({
32
+ name: row.name,
33
+ owner: row.owner,
34
+ event: row.event,
35
+ function: row.function,
36
+ enabled: row.enabled,
37
+ tags: row.tags || [],
38
+ comment: row.comment || undefined,
39
+ }));
40
+ } catch (error) {
41
+ return [];
42
+ }
43
+ }
@@ -0,0 +1,21 @@
1
+ const EXTENSIONS_QUERY = `
2
+ SELECT e.extname AS name,
3
+ n.nspname AS schema,
4
+ e.extversion AS version,
5
+ pg_catalog.pg_get_userbyid(e.extowner) AS owner,
6
+ e.extrelocatable AS is_relocatable,
7
+ pg_catalog.obj_description(e.oid, 'pg_extension') AS comment,
8
+ true AS is_available
9
+ FROM pg_catalog.pg_extension e
10
+ JOIN pg_catalog.pg_namespace n ON n.oid = e.extnamespace
11
+ ORDER BY e.extname
12
+ `;
13
+
14
+ /**
15
+ * @param {import('pg').Pool} pool
16
+ * @returns {Promise<Array>}
17
+ */
18
+ export async function queryExtensions(pool) {
19
+ const result = await pool.query(EXTENSIONS_QUERY);
20
+ return result.rows;
21
+ }