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,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
|
+
}
|