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,151 @@
1
+ const SCHEMAS_QUERY = `
2
+ SELECT n.nspname AS name,
3
+ n.nspowner::regrole::text AS owner,
4
+ n.nspacl AS privileges,
5
+ pg_catalog.obj_description(n.oid, 'pg_namespace') AS comment
6
+ FROM pg_catalog.pg_namespace n
7
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
8
+ AND n.nspname NOT LIKE 'pg_temp_%'
9
+ AND n.nspname NOT LIKE 'pg_toast_temp_%'
10
+ ORDER BY n.nspname
11
+ `;
12
+
13
+ const TABLES_QUERY = `
14
+ SELECT c.oid,
15
+ n.nspname AS schema,
16
+ c.relname AS name,
17
+ c.relkind AS kind,
18
+ c.relowner::regrole::text AS owner,
19
+ c.relpersistence AS persistence,
20
+ c.relrowsecurity AS rls_enabled,
21
+ c.relforcerowsecurity AS rls_forced,
22
+ c.reloptions AS storage_options,
23
+ CASE WHEN c.reltablespace != 0 THEN c.reltablespace::regrole::text ELSE NULL END AS tablespace,
24
+ CASE WHEN c.relkind = 'p' THEN pt.partstrat ELSE NULL END AS partition_strategy,
25
+ pg_catalog.pg_get_expr(c.relpartbound, c.oid) AS partition_bound,
26
+ c.relreplident AS replica_identity,
27
+ (SELECT idx.relname FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class idx ON idx.oid = i.indexrelid WHERE i.indrelid = c.oid AND i.indisreplident = true LIMIT 1) AS replica_identity_index,
28
+ pg_catalog.pg_get_partkeydef(c.oid) AS partition_key_def,
29
+ /* HAS_OIDS_PLACEHOLDER */,
30
+ EXISTS (SELECT 1 FROM pg_catalog.pg_class tc JOIN pg_catalog.pg_namespace tn ON tn.oid = tc.relnamespace JOIN pg_catalog.pg_depend d ON d.objid = tc.oid WHERE tc.relkind = 't' AND tn.nspname = 'pg_toast' AND d.refobjid = c.oid AND d.classid = 'pg_class'::regclass) AS has_toast_table,
31
+ EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = 'pg_class'::regclass AND d.objid = c.oid AND d.refclassid = 'pg_class'::regclass AND d.deptype = 'e') AS user_catalog_table,
32
+ am.amname AS access_method
33
+ FROM pg_catalog.pg_class c
34
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
35
+ LEFT JOIN pg_catalog.pg_partitioned_table pt ON pt.partrelid = c.oid
36
+ LEFT JOIN pg_catalog.pg_am am ON am.oid = c.relam
37
+ WHERE c.relkind IN ('r', 'p', 'f')
38
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
39
+ AND n.nspname NOT LIKE 'pg_temp_%'
40
+ ORDER BY n.nspname, c.relname
41
+ `;
42
+
43
+ const TOAST_OPTIONS_QUERY = `
44
+ SELECT
45
+ n.nspname AS schema,
46
+ c.relname AS table_name,
47
+ tc.relname AS toast_table_name,
48
+ tc.reloptions AS toast_storage_options
49
+ FROM pg_catalog.pg_class c
50
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
51
+ JOIN pg_catalog.pg_depend d ON d.refobjid = c.oid AND d.classid = 'pg_class'::regclass
52
+ JOIN pg_catalog.pg_class tc ON tc.oid = d.objid AND tc.relkind = 't'
53
+ JOIN pg_catalog.pg_namespace tn ON tn.oid = tc.relnamespace AND tn.nspname = 'pg_toast'
54
+ WHERE c.relkind IN ('r', 'p')
55
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
56
+ AND n.nspname NOT LIKE 'pg_temp_%'
57
+ ORDER BY n.nspname, c.relname
58
+ `;
59
+
60
+ const COLUMNS_QUERY = `
61
+ SELECT a.attrelid::regclass::text AS table_ref,
62
+ n.nspname AS schema,
63
+ c.relname AS table_name,
64
+ a.attname AS name,
65
+ a.attnum AS ordinal_position,
66
+ format_type(a.atttypid, a.atttypmod) AS data_type,
67
+ NOT a.attnotnull AS is_nullable,
68
+ a.attgenerated AS generated,
69
+ CASE WHEN a.attgenerated IN ('s', 'v') AND d.adbin IS NOT NULL
70
+ THEN pg_catalog.pg_get_expr(d.adbin, d.adrelid)
71
+ ELSE NULL END AS generated_expression,
72
+ CASE WHEN (a.attgenerated IS NULL OR a.attgenerated = '') AND d.adbin IS NOT NULL
73
+ THEN pg_catalog.pg_get_expr(d.adbin, d.adrelid)
74
+ ELSE NULL END AS default_value,
75
+ a.attidentity AS identity,
76
+ CASE WHEN a.attcollation != 0 AND a.attcollation != 100
77
+ THEN a.attcollation::regcollation::text ELSE NULL END AS collation,
78
+ a.attstorage::text AS storage,
79
+ pg_catalog.col_description(a.attrelid, a.attnum) AS comment,
80
+ a.attstattarget AS statistics_target,
81
+ /* COMPRESSION_PLACEHOLDER */
82
+ s.seqstart AS identity_start,
83
+ s.seqincrement AS identity_increment,
84
+ s.seqmin AS identity_min,
85
+ s.seqmax AS identity_max,
86
+ s.seqcycle AS identity_cycle,
87
+ s.seqcache AS identity_cache,
88
+ a.attinhcount AS inherited_count,
89
+ a.attislocal AS is_local,
90
+ a.attacl AS privileges,
91
+ EXISTS (SELECT 1 FROM pg_catalog.pg_constraint con WHERE con.conrelid = a.attrelid AND con.contype = 'p' AND a.attnum = ANY(con.conkey)) AS is_primary_key,
92
+ EXISTS (SELECT 1 FROM pg_catalog.pg_index idx WHERE idx.indrelid = a.attrelid AND idx.indisunique AND a.attnum = ANY(idx.indkey) AND NOT idx.indisprimary) AS is_unique,
93
+ a.attlen AS length,
94
+ a.attndims AS array_dimensions
95
+ FROM pg_catalog.pg_attribute a
96
+ JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
97
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
98
+ LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
99
+ LEFT JOIN pg_catalog.pg_sequence s ON s.seqrelid = (
100
+ SELECT seq.oid FROM pg_catalog.pg_class seq
101
+ JOIN pg_catalog.pg_namespace seqn ON seqn.oid = seq.relnamespace
102
+ JOIN pg_catalog.pg_depend dep ON dep.objid = seq.oid AND dep.classid = 'pg_class'::regclass
103
+ WHERE seq.relkind = 'S' AND dep.refobjid = a.attrelid AND dep.refobjsubid = a.attnum
104
+ AND seqn.nspname NOT LIKE 'pg_temp_%'
105
+ LIMIT 1
106
+ )
107
+ WHERE a.attnum > 0
108
+ AND NOT a.attisdropped
109
+ AND c.relkind IN ('r', 'p', 'f')
110
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
111
+ AND n.nspname NOT LIKE 'pg_temp_%'
112
+ ORDER BY n.nspname, c.relname, a.attnum
113
+ `;
114
+
115
+ /**
116
+ * @param {import('pg').Pool} pool
117
+ * @returns {Promise<Array<{name:string,owner:string}>>}
118
+ */
119
+ export async function querySchemas(pool) {
120
+ const result = await pool.query(SCHEMAS_QUERY);
121
+ return result.rows;
122
+ }
123
+
124
+ /**
125
+ * @param {import('pg').Pool} pool
126
+ * @param {number} version
127
+ * @returns {Promise<Array>}
128
+ */
129
+ export async function queryTables(pool, version = 150000) {
130
+ const hasOidsCol = (version < 120000) ? 'c.relhasoids' : 'false';
131
+ const query = TABLES_QUERY.replace('/* HAS_OIDS_PLACEHOLDER */', `${hasOidsCol} AS has_oids`);
132
+ const result = await pool.query(query);
133
+ return result.rows;
134
+ }
135
+
136
+ /**
137
+ * @param {import('pg').Pool} pool
138
+ * @param {number} version
139
+ * @returns {Promise<Array>}
140
+ */
141
+ export async function queryColumns(pool, version = 150000) {
142
+ const hasCompression = (version >= 140000) ? "a.attcompression::text AS compression," : "NULL AS compression,";
143
+ const query = COLUMNS_QUERY.replace('/* COMPRESSION_PLACEHOLDER */', hasCompression);
144
+ const result = await pool.query(query);
145
+ return result.rows;
146
+ }
147
+
148
+ export async function queryToastOptions(pool) {
149
+ const result = await pool.query(TOAST_OPTIONS_QUERY);
150
+ return result.rows;
151
+ }
@@ -0,0 +1,36 @@
1
+ const TABLESPACES_QUERY = `
2
+ SELECT
3
+ t.oid,
4
+ t.spcname AS name,
5
+ pg_catalog.pg_get_userbyid(t.spcowner) AS owner,
6
+ t.spclocation AS location,
7
+ t.spcdefault AS is_default,
8
+ t.spcacl AS acl,
9
+ t.spcoptions AS options
10
+ FROM pg_catalog.pg_tablespace t
11
+ WHERE t.spcname NOT LIKE 'pg_%'
12
+ ORDER BY t.spcname
13
+ `;
14
+
15
+ const DEFAULT_TABLESPACES = ['pg_default', 'pg_global'];
16
+
17
+ /**
18
+ * @param {import('pg').Pool} pool
19
+ * @returns {Promise<Array>}
20
+ */
21
+ export async function queryInterfaceTablespaces(pool) {
22
+ try {
23
+ const result = await pool.query(TABLESPACES_QUERY);
24
+
25
+ return result.rows.map(row => ({
26
+ name: row.name,
27
+ owner: row.owner,
28
+ location: row.location || undefined,
29
+ is_default: row.is_default || DEFAULT_TABLESPACES.includes(row.name),
30
+ acl: row.acl || undefined,
31
+ options: row.options || undefined,
32
+ }));
33
+ } catch (error) {
34
+ return [];
35
+ }
36
+ }
@@ -0,0 +1,146 @@
1
+ const TEXT_SEARCH_CONFIGS_QUERY = `
2
+ SELECT
3
+ t.oid,
4
+ n.nspname AS schema,
5
+ t.cfgname AS name,
6
+ pg_catalog.pg_get_userbyid(t.cfgowner) AS owner,
7
+ format_type(t.cfgparser, NULL) AS parser,
8
+ n2.nspname AS parser_schema,
9
+ pg_catalog.obj_description(t.oid, 'pg_ts_config') AS comment
10
+ FROM pg_catalog.pg_ts_config t
11
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.cfgnamespace
12
+ LEFT JOIN pg_catalog.pg_ts_parser p ON p.oid = t.cfgparser
13
+ LEFT JOIN pg_catalog.pg_namespace n2 ON n2.oid = p.prsnamespace
14
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
15
+ ORDER BY n.nspname, t.cfgname
16
+ `;
17
+
18
+ const TEXT_SEARCH_CONFIG_MAP_QUERY = `
19
+ SELECT
20
+ n.nspname AS schema,
21
+ t.cfgname AS config_name,
22
+ m.maptokentype AS token_type,
23
+ m.mapseqno AS seq,
24
+ d.dictname AS dictionary,
25
+ nd.nspname AS dict_schema,
26
+ m.mapdict AS dict_oid
27
+ FROM pg_catalog.pg_ts_config_map m
28
+ JOIN pg_catalog.pg_ts_config t ON t.oid = m.mapcfg
29
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.cfgnamespace
30
+ JOIN pg_catalog.pg_ts_dict d ON d.oid = m.mapdict
31
+ JOIN pg_catalog.pg_namespace nd ON nd.oid = d.dictnamespace
32
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
33
+ ORDER BY n.nspname, t.cfgname, m.mapseqno
34
+ `;
35
+
36
+ const TEXT_SEARCH_DICTIONARIES_QUERY = `
37
+ SELECT
38
+ d.oid,
39
+ n.nspname AS schema,
40
+ d.dictname AS name,
41
+ pg_catalog.pg_get_userbyid(d.dictowner) AS owner,
42
+ format_type(d.dicttemplate, NULL) AS template,
43
+ n2.nspname AS template_schema,
44
+ d.dictinitoption AS options,
45
+ pg_catalog.obj_description(d.oid, 'pg_ts_dict') AS comment
46
+ FROM pg_catalog.pg_ts_dict d
47
+ JOIN pg_catalog.pg_namespace n ON n.oid = d.dictnamespace
48
+ LEFT JOIN pg_catalog.pg_ts_template t ON t.oid = d.dicttemplate
49
+ LEFT JOIN pg_catalog.pg_namespace n2 ON n2.oid = t.tmplnamespace
50
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
51
+ ORDER BY n.nspname, d.dictname
52
+ `;
53
+
54
+ const TEXT_SEARCH_PARSERS_QUERY = `
55
+ SELECT
56
+ p.oid,
57
+ n.nspname AS schema,
58
+ p.prsname AS name,
59
+ pg_catalog.pg_get_userbyid(p.prsowner) AS owner,
60
+ p.prsstart::regproc::text AS start,
61
+ p.prstoken::regproc::text AS get_token,
62
+ p.prsend::regproc::text AS end,
63
+ p.prsheadline::regproc::text AS headline,
64
+ p.prslextype::regproc::text AS lextypes,
65
+ pg_catalog.obj_description(p.oid, 'pg_ts_parser') AS comment
66
+ FROM pg_catalog.pg_ts_parser p
67
+ JOIN pg_catalog.pg_namespace n ON n.oid = p.prsnamespace
68
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
69
+ ORDER BY n.nspname, p.prsname
70
+ `;
71
+
72
+ const TEXT_SEARCH_TEMPLATES_QUERY = `
73
+ SELECT
74
+ t.oid,
75
+ n.nspname AS schema,
76
+ t.tmplname AS name,
77
+ pg_catalog.pg_get_userbyid(t.tmplowner) AS owner,
78
+ t.tmplinit::regproc::text AS init,
79
+ t.tmpllexize::regproc::text AS lexize,
80
+ pg_catalog.obj_description(t.oid, 'pg_ts_template') AS comment
81
+ FROM pg_catalog.pg_ts_template t
82
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.tmplnamespace
83
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
84
+ ORDER BY n.nspname, t.tmplname
85
+ `;
86
+
87
+ /**
88
+ * @param {import('pg').Pool} pool
89
+ * @returns {Promise<{configs: Array, tokenMappings: Array}>}
90
+ */
91
+ export async function queryTextSearchConfigs(pool) {
92
+ try {
93
+ const configsResult = await pool.query(TEXT_SEARCH_CONFIGS_QUERY);
94
+ let mappingsResult = { rows: [] };
95
+ try {
96
+ mappingsResult = await pool.query(TEXT_SEARCH_CONFIG_MAP_QUERY);
97
+ } catch (e) {
98
+ // May fail on older versions
99
+ }
100
+ return {
101
+ configs: configsResult.rows,
102
+ tokenMappings: mappingsResult.rows,
103
+ };
104
+ } catch (error) {
105
+ return { configs: [], tokenMappings: [] };
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @param {import('pg').Pool} pool
111
+ * @returns {Promise<Array>}
112
+ */
113
+ export async function queryTextSearchDictionaries(pool) {
114
+ try {
115
+ const result = await pool.query(TEXT_SEARCH_DICTIONARIES_QUERY);
116
+ return result.rows;
117
+ } catch (error) {
118
+ return [];
119
+ }
120
+ }
121
+
122
+ /**
123
+ * @param {import('pg').Pool} pool
124
+ * @returns {Promise<Array>}
125
+ */
126
+ export async function queryTextSearchParsers(pool) {
127
+ try {
128
+ const result = await pool.query(TEXT_SEARCH_PARSERS_QUERY);
129
+ return result.rows;
130
+ } catch (error) {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ /**
136
+ * @param {import('pg').Pool} pool
137
+ * @returns {Promise<Array>}
138
+ */
139
+ export async function queryTextSearchTemplates(pool) {
140
+ try {
141
+ const result = await pool.query(TEXT_SEARCH_TEMPLATES_QUERY);
142
+ return result.rows;
143
+ } catch (error) {
144
+ return [];
145
+ }
146
+ }
@@ -0,0 +1,61 @@
1
+ const TRIGGERS_QUERY = `
2
+ SELECT t.tgname AS name,
3
+ n.nspname AS schema,
4
+ c.relname AS table_name,
5
+ tn.nspname AS table_schema,
6
+ CASE
7
+ WHEN (t.tgtype::int & 2) != 0 THEN 'BEFORE'
8
+ WHEN (t.tgtype::int & 64) != 0 THEN 'INSTEAD OF'
9
+ ELSE 'AFTER'
10
+ END AS timing,
11
+ array_remove(ARRAY[
12
+ CASE WHEN (t.tgtype::int & 4) != 0 THEN 'INSERT' ELSE NULL END,
13
+ CASE WHEN (t.tgtype::int & 8) != 0 THEN 'DELETE' ELSE NULL END,
14
+ CASE WHEN (t.tgtype::int & 16) != 0 THEN 'UPDATE' ELSE NULL END,
15
+ CASE WHEN (t.tgtype::int & 32) != 0 THEN 'TRUNCATE' ELSE NULL END
16
+ ], NULL) AS events,
17
+ CASE WHEN (t.tgtype::int & 1) != 0 THEN 'ROW' ELSE 'STATEMENT' END AS level,
18
+ (t.tgtype::int & 1) != 0 AS is_for_each_row,
19
+ pg_catalog.pg_get_triggerdef(t.oid) AS definition,
20
+ CASE t.tgenabled
21
+ WHEN 'O' THEN 'ENABLED'
22
+ WHEN 'D' THEN 'DISABLED'
23
+ WHEN 'R' THEN 'REPLICA'
24
+ WHEN 'A' THEN 'ALWAYS'
25
+ END AS enabled,
26
+ p.proname AS function_name,
27
+ pn.nspname AS function_schema,
28
+ NULL AS when_condition,
29
+ t.tgconstraint IS NOT NULL AS is_constraint,
30
+ t.tgdeferrable AS is_deferrable,
31
+ t.tginitdeferred AS is_deferred,
32
+ (SELECT array_agg(a.attname)
33
+ FROM unnest(t.tgattr) AS uattnum
34
+ JOIN pg_attribute a ON a.attrelid = t.tgrelid AND a.attnum = uattnum
35
+ ) AS update_of_columns,
36
+ t.tgoldtable AS old_table_name,
37
+ t.tgnewtable AS new_table_name,
38
+ pg_catalog.obj_description(t.oid, 'pg_trigger') AS comment
39
+ FROM pg_catalog.pg_trigger t
40
+ JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
41
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
42
+ JOIN pg_catalog.pg_namespace tn ON tn.oid = c.relnamespace
43
+ JOIN pg_catalog.pg_proc p ON p.oid = t.tgfoid
44
+ JOIN pg_catalog.pg_namespace pn ON pn.oid = p.pronamespace
45
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
46
+ AND n.nspname NOT LIKE 'pg_temp_%'
47
+ ORDER BY tn.nspname, c.relname, t.tgname
48
+ `;
49
+
50
+ /**
51
+ * @param {import('pg').Pool} pool
52
+ * @returns {Promise<Array>}
53
+ */
54
+ export async function queryTriggers(pool) {
55
+ const result = await pool.query(TRIGGERS_QUERY);
56
+ return result.rows.map(row => ({
57
+ ...row,
58
+ events: (row.events || []).filter(Boolean),
59
+ function_call: `${row.function_schema}.${row.function_name}`,
60
+ }));
61
+ }
@@ -0,0 +1,106 @@
1
+ const ENUMS_QUERY = `
2
+ SELECT t.typname AS name,
3
+ n.nspname AS schema,
4
+ t.typowner::regrole::text AS owner,
5
+ array_agg(e.enumlabel ORDER BY e.enumsortorder)::text[] AS enum_values,
6
+ pg_catalog.obj_description(t.oid, 'pg_type') AS comment,
7
+ t.typacl AS privileges,
8
+ t.typarray::regtype AS array_type
9
+ FROM pg_catalog.pg_type t
10
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
11
+ JOIN pg_catalog.pg_enum e ON e.enumtypid = t.oid
12
+ WHERE t.typtype = 'e'
13
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
14
+ AND n.nspname NOT LIKE 'pg_temp_%'
15
+ GROUP BY t.oid, t.typname, n.nspname, t.typowner, t.typacl, t.typarray
16
+ ORDER BY n.nspname, t.typname
17
+ `;
18
+
19
+ const COMPOSITES_QUERY = `
20
+ SELECT t.typname AS name,
21
+ n.nspname AS schema,
22
+ t.typowner::regrole::text AS owner,
23
+ array_agg(
24
+ json_build_object(
25
+ 'name', a.attname,
26
+ 'type', format_type(a.atttypid, a.atttypmod),
27
+ 'collation', COALESCE(coll.collname, NULL)
28
+ )
29
+ ORDER BY a.attnum
30
+ ) AS attributes,
31
+ pg_catalog.obj_description(t.oid, 'pg_type') AS comment
32
+ FROM pg_catalog.pg_type t
33
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
34
+ JOIN pg_catalog.pg_class c ON c.oid = t.typrelid
35
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
36
+ LEFT JOIN pg_catalog.pg_collation coll ON coll.oid = a.attcollation
37
+ WHERE t.typtype = 'c'
38
+ AND c.relkind = 'c'
39
+ AND a.attnum > 0
40
+ AND NOT a.attisdropped
41
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
42
+ AND n.nspname NOT LIKE 'pg_temp_%'
43
+ GROUP BY t.oid, t.typname, n.nspname, t.typowner
44
+ ORDER BY n.nspname, t.typname
45
+ `;
46
+
47
+ const DOMAINS_QUERY = `
48
+ SELECT t.typname AS name,
49
+ n.nspname AS schema,
50
+ t.typowner::regrole::text AS owner,
51
+ format_type(t.typbasetype, t.typtypmod) AS base_type,
52
+ (SELECT n2.nspname FROM pg_type bt JOIN pg_namespace n2 ON n2.oid = bt.typnamespace WHERE bt.oid = t.typbasetype) AS base_type_schema,
53
+ t.typnotnull AS not_null,
54
+ pg_catalog.pg_get_expr(t.typdefaultbin, t.typbasetype) AS default_value,
55
+ pg_catalog.pg_get_constraintdef(con.oid) AS check_constraint,
56
+ pg_catalog.obj_description(t.oid, 'pg_type') AS comment,
57
+ CASE WHEN t.typcollation != 0 THEN t.typcollation::regcollation::text ELSE NULL END AS collation,
58
+ t.typacl AS privileges,
59
+ COALESCE(con.convalidated, true) AS is_validated,
60
+ t.typtypmod AS typmod,
61
+ bt.typlen AS length
62
+ FROM pg_catalog.pg_type t
63
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
64
+ LEFT JOIN pg_catalog.pg_constraint con ON con.contypid = t.oid AND con.contype = 'c'
65
+ LEFT JOIN pg_catalog.pg_type bt ON bt.oid = t.typbasetype
66
+ WHERE t.typtype = 'd'
67
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema')
68
+ AND n.nspname NOT LIKE 'pg_temp_%'
69
+ ORDER BY n.nspname, t.typname
70
+ `;
71
+
72
+ const RANGES_QUERY = `
73
+ SELECT t.typname AS name,
74
+ n.nspname AS schema,
75
+ t.typowner::regrole::text AS owner,
76
+ format_type(r.rngsubtype, NULL) AS subtype,
77
+ (SELECT n2.nspname FROM pg_type st JOIN pg_namespace n2 ON n2.oid = st.typnamespace WHERE st.oid = r.rngsubtype) AS subtype_schema,
78
+ r.rngmultitypid::regtype AS multirange_type,
79
+ CASE WHEN r.rngcollation != 0 THEN r.rngcollation::regcollation::text ELSE NULL END AS collation,
80
+ opc.opcname AS subtype_opclass,
81
+ CASE WHEN r.rngsubdiff != 0 THEN r.rngsubdiff::regproc::text ELSE NULL END AS subtype_diff,
82
+ CASE WHEN r.rngcanonical != 0 THEN r.rngcanonical::regproc::text ELSE NULL END AS canonical_function,
83
+ pg_catalog.obj_description(t.oid, 'pg_type') AS comment,
84
+ t.typacl AS privileges
85
+ FROM pg_catalog.pg_range r
86
+ JOIN pg_catalog.pg_type t ON t.oid = r.rngtypid
87
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
88
+ LEFT JOIN pg_catalog.pg_opclass opc ON opc.oid = r.rngsubopc
89
+ WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
90
+ AND n.nspname NOT LIKE 'pg_temp_%'
91
+ ORDER BY n.nspname, t.typname
92
+ `;
93
+
94
+ /**
95
+ * @param {import('pg').Pool} pool
96
+ * @returns {Promise<{enums:Array,composites:Array,domains:Array,ranges:Array}>}
97
+ */
98
+ export async function queryTypes(pool) {
99
+ const [enums, composites, domains, ranges] = await Promise.all([
100
+ pool.query(ENUMS_QUERY).then(r => r.rows),
101
+ pool.query(COMPOSITES_QUERY).then(r => r.rows),
102
+ pool.query(DOMAINS_QUERY).then(r => r.rows),
103
+ pool.query(RANGES_QUERY).then(r => r.rows),
104
+ ]);
105
+ return { enums, composites, domains, ranges };
106
+ }
@@ -0,0 +1,146 @@
1
+ const VIEWS_QUERY = `
2
+ SELECT c.relname AS name,
3
+ n.nspname AS schema,
4
+ c.relowner::regrole::text AS owner,
5
+ pg_catalog.pg_get_viewdef(c.oid, true) AS definition,
6
+ CASE c.relkind WHEN 'v' THEN 'VIEW' ELSE 'MATERIALIZED_VIEW' END AS kind,
7
+ CASE
8
+ WHEN c.relkind = 'v' AND c.relchecks > 0 THEN
9
+ CASE WHEN EXISTS (
10
+ SELECT 1 FROM pg_catalog.pg_rewrite r
11
+ WHERE r.ev_class = c.oid AND r.ev_type = '1'
12
+ AND pg_catalog.pg_get_expr(r.ev_qual, r.ev_class) LIKE '%check_option=cascaded%'
13
+ ) THEN 'CASCADED'
14
+ WHEN EXISTS (
15
+ SELECT 1 FROM pg_catalog.pg_rewrite r
16
+ WHERE r.ev_class = c.oid AND r.ev_type = '1'
17
+ AND pg_catalog.pg_get_expr(r.ev_qual, r.ev_class) LIKE '%check_option=local%'
18
+ ) THEN 'LOCAL'
19
+ ELSE NULL
20
+ END
21
+ ELSE NULL
22
+ END AS check_option,
23
+ c.reloptions AS rel_options,
24
+ c.relacl AS privileges,
25
+ pg_catalog.obj_description(c.oid, 'pg_class') AS comment
26
+ FROM pg_catalog.pg_class c
27
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
28
+ WHERE c.relkind IN ('v', 'm')
29
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
30
+ AND n.nspname NOT LIKE 'pg_temp_%'
31
+ ORDER BY n.nspname, c.relname
32
+ `;
33
+
34
+ const VIEW_COLUMNS_QUERY = `
35
+ SELECT n.nspname AS schema,
36
+ c.relname AS name,
37
+ a.attname AS column_name,
38
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type,
39
+ NOT a.attnotnull AS nullable,
40
+ pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS default_value
41
+ FROM pg_catalog.pg_class c
42
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
43
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
44
+ LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
45
+ WHERE c.relkind = 'v'
46
+ AND a.attnum > 0 AND NOT a.attisdropped
47
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
48
+ AND n.nspname NOT LIKE 'pg_temp_%'
49
+ ORDER BY n.nspname, c.relname, a.attnum
50
+ `;
51
+
52
+ const MATVIEW_EXTRA_QUERY = `
53
+ SELECT c.relname AS name,
54
+ n.nspname AS schema,
55
+ c.relispopulated AS is_populated,
56
+ ts.spcname AS tablespace,
57
+ c.reloptions AS storage_options,
58
+ c.relacl AS privileges
59
+ FROM pg_catalog.pg_class c
60
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
61
+ LEFT JOIN pg_catalog.pg_tablespace ts ON ts.oid = c.reltablespace
62
+ WHERE c.relkind = 'm'
63
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
64
+ AND n.nspname NOT LIKE 'pg_temp_%'
65
+ `;
66
+
67
+ const MATVIEW_COLUMNS_QUERY = `
68
+ SELECT n.nspname AS schema,
69
+ c.relname AS name,
70
+ a.attname AS column_name,
71
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type,
72
+ NOT a.attnotnull AS nullable
73
+ FROM pg_catalog.pg_class c
74
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
75
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
76
+ WHERE c.relkind = 'm'
77
+ AND a.attnum > 0 AND NOT a.attisdropped
78
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
79
+ AND n.nspname NOT LIKE 'pg_temp_%'
80
+ ORDER BY n.nspname, c.relname, a.attnum
81
+ `;
82
+
83
+
84
+ /**
85
+ * @param {import('pg').Pool} pool
86
+ * @returns {Promise<{views:Array,materializedViews:Array}>}
87
+ */
88
+ export async function queryViews(pool) {
89
+ const viewResult = await pool.query(VIEWS_QUERY);
90
+ const matResult = await pool.query(MATVIEW_EXTRA_QUERY);
91
+ const viewColsResult = await pool.query(VIEW_COLUMNS_QUERY);
92
+ const matColsResult = await pool.query(MATVIEW_COLUMNS_QUERY);
93
+
94
+ const matExtras = new Map();
95
+ for (const row of matResult.rows) {
96
+ matExtras.set(`${row.schema}.${row.name}`, row);
97
+ }
98
+
99
+ const viewColumns = new Map();
100
+ for (const row of viewColsResult.rows) {
101
+ const key = `${row.schema}.${row.name}`;
102
+ if (!viewColumns.has(key)) viewColumns.set(key, []);
103
+ viewColumns.get(key).push({
104
+ name: row.column_name,
105
+ type: row.column_type,
106
+ nullable: row.nullable,
107
+ default: row.default_value || undefined,
108
+ });
109
+ }
110
+
111
+ const matColumns = new Map();
112
+ for (const row of matColsResult.rows) {
113
+ const key = `${row.schema}.${row.name}`;
114
+ if (!matColumns.has(key)) matColumns.set(key, []);
115
+ matColumns.get(key).push({
116
+ name: row.column_name,
117
+ type: row.column_type,
118
+ nullable: row.nullable,
119
+ });
120
+ }
121
+
122
+ const views = [];
123
+ const materializedViews = [];
124
+
125
+ for (const row of viewResult.rows) {
126
+ const key = `${row.schema}.${row.name}`;
127
+ if (row.kind === 'VIEW') {
128
+ views.push({
129
+ ...row,
130
+ columns: viewColumns.get(key) || [],
131
+ });
132
+ } else {
133
+ const extra = matExtras.get(key) || {};
134
+ materializedViews.push({
135
+ ...row,
136
+ is_populated: extra.is_populated ?? true,
137
+ tablespace: extra.tablespace ?? null,
138
+ storage_options: extra.storage_options ?? null,
139
+ columns: matColumns.get(key) || [],
140
+ privileges: extra.privileges ?? null,
141
+ });
142
+ }
143
+ }
144
+
145
+ return { views, materializedViews };
146
+ }