supatool 0.6.5 → 0.6.6
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/dist/sync/definitionExtractor.js +241 -251
- package/package.json +1 -1
|
@@ -33,9 +33,33 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.resolveMaxConcurrent = resolveMaxConcurrent;
|
|
37
|
+
exports.resolveConnectionTimeoutMs = resolveConnectionTimeoutMs;
|
|
38
|
+
exports.resolveQueryTimeoutMs = resolveQueryTimeoutMs;
|
|
36
39
|
exports.extractDefinitions = extractDefinitions;
|
|
40
|
+
exports.fetchTableDefinitions = fetchTableDefinitions;
|
|
37
41
|
exports.generateCreateTableDDL = generateCreateTableDDL;
|
|
38
42
|
const pg_1 = require("pg");
|
|
43
|
+
const DEFAULT_MAX_CONCURRENT = 5;
|
|
44
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 15000;
|
|
45
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 60000;
|
|
46
|
+
function resolveBoundedPositiveInteger(rawValue, fallback, maximum) {
|
|
47
|
+
if (rawValue === undefined || rawValue.trim() === '')
|
|
48
|
+
return fallback;
|
|
49
|
+
const parsed = Number(rawValue);
|
|
50
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
51
|
+
return fallback;
|
|
52
|
+
return Math.min(maximum, parsed);
|
|
53
|
+
}
|
|
54
|
+
function resolveMaxConcurrent(rawValue = process.env.SUPATOOL_MAX_CONCURRENT) {
|
|
55
|
+
return resolveBoundedPositiveInteger(rawValue, DEFAULT_MAX_CONCURRENT, 50);
|
|
56
|
+
}
|
|
57
|
+
function resolveConnectionTimeoutMs(rawValue = process.env.SUPATOOL_CONNECTION_TIMEOUT_MS) {
|
|
58
|
+
return resolveBoundedPositiveInteger(rawValue, DEFAULT_CONNECTION_TIMEOUT_MS, 300000);
|
|
59
|
+
}
|
|
60
|
+
function resolveQueryTimeoutMs(rawValue = process.env.SUPATOOL_QUERY_TIMEOUT_MS) {
|
|
61
|
+
return resolveBoundedPositiveInteger(rawValue, DEFAULT_QUERY_TIMEOUT_MS, 300000);
|
|
62
|
+
}
|
|
39
63
|
/**
|
|
40
64
|
* Display progress
|
|
41
65
|
*/
|
|
@@ -100,9 +124,8 @@ function stopProgressDisplay() {
|
|
|
100
124
|
*/
|
|
101
125
|
async function fetchRlsPolicies(client, spinner, progress, schemas = ['public']) {
|
|
102
126
|
const policies = [];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const result = await client.query(`
|
|
127
|
+
const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
|
|
128
|
+
const result = await client.query(`
|
|
106
129
|
SELECT
|
|
107
130
|
schemaname,
|
|
108
131
|
tablename,
|
|
@@ -116,78 +139,73 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
|
|
|
116
139
|
WHERE schemaname IN (${schemaPlaceholders})
|
|
117
140
|
ORDER BY schemaname, tablename, policyname
|
|
118
141
|
`, schemas);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
groupedPolicies[tableKey].push(row);
|
|
142
|
+
const groupedPolicies = {};
|
|
143
|
+
for (const row of result.rows) {
|
|
144
|
+
const tableKey = `${row.schemaname}.${row.tablename}`;
|
|
145
|
+
if (!groupedPolicies[tableKey]) {
|
|
146
|
+
groupedPolicies[tableKey] = [];
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
groupedPolicies[tableKey].push(row);
|
|
149
|
+
}
|
|
150
|
+
const tableKeys = Object.keys(groupedPolicies);
|
|
151
|
+
// Initialize progress
|
|
152
|
+
if (progress) {
|
|
153
|
+
progress.rls.total = tableKeys.length;
|
|
154
|
+
}
|
|
155
|
+
for (let i = 0; i < tableKeys.length; i++) {
|
|
156
|
+
const tableKey = tableKeys[i];
|
|
157
|
+
const tablePolicies = groupedPolicies[tableKey];
|
|
158
|
+
const firstPolicy = tablePolicies[0];
|
|
159
|
+
const schemaName = firstPolicy.schemaname;
|
|
160
|
+
const tableName = firstPolicy.tablename;
|
|
161
|
+
// Update progress
|
|
162
|
+
if (progress && spinner) {
|
|
163
|
+
progress.rls.current = i + 1;
|
|
164
|
+
displayProgress(progress, spinner);
|
|
131
165
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
ddl += `ALTER TABLE ${schemaName}.${tableName} ENABLE ROW LEVEL SECURITY;\n\n`;
|
|
147
|
-
for (const policy of tablePolicies) {
|
|
148
|
-
ddl += `CREATE POLICY ${policy.policyname}\n`;
|
|
149
|
-
ddl += ` ON ${schemaName}.${tableName}\n`;
|
|
150
|
-
ddl += ` AS ${policy.permissive || 'PERMISSIVE'}\n`;
|
|
151
|
-
ddl += ` FOR ${policy.cmd || 'ALL'}\n`;
|
|
152
|
-
if (policy.roles) {
|
|
153
|
-
// Handle roles as array or string
|
|
154
|
-
let roles;
|
|
155
|
-
if (Array.isArray(policy.roles)) {
|
|
156
|
-
roles = policy.roles.join(', ');
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
// Handle PostgreSQL array literal "{role1,role2}" or plain string
|
|
160
|
-
roles = String(policy.roles)
|
|
161
|
-
.replace(/[{}]/g, '') // Remove braces
|
|
162
|
-
.replace(/"/g, ''); // Remove double quotes
|
|
163
|
-
}
|
|
164
|
-
if (roles && roles.trim() !== '') {
|
|
165
|
-
ddl += ` TO ${roles}\n`;
|
|
166
|
-
}
|
|
166
|
+
// Add RLS policy description at top
|
|
167
|
+
let ddl = `-- RLS Policies for ${schemaName}.${tableName}\n`;
|
|
168
|
+
ddl += `-- Row Level Security policies to control data access at the row level\n\n`;
|
|
169
|
+
ddl += `ALTER TABLE ${schemaName}.${tableName} ENABLE ROW LEVEL SECURITY;\n\n`;
|
|
170
|
+
for (const policy of tablePolicies) {
|
|
171
|
+
ddl += `CREATE POLICY ${policy.policyname}\n`;
|
|
172
|
+
ddl += ` ON ${schemaName}.${tableName}\n`;
|
|
173
|
+
ddl += ` AS ${policy.permissive || 'PERMISSIVE'}\n`;
|
|
174
|
+
ddl += ` FOR ${policy.cmd || 'ALL'}\n`;
|
|
175
|
+
if (policy.roles) {
|
|
176
|
+
// Handle roles as array or string
|
|
177
|
+
let roles;
|
|
178
|
+
if (Array.isArray(policy.roles)) {
|
|
179
|
+
roles = policy.roles.join(', ');
|
|
167
180
|
}
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
else {
|
|
182
|
+
// Handle PostgreSQL array literal "{role1,role2}" or plain string
|
|
183
|
+
roles = String(policy.roles)
|
|
184
|
+
.replace(/[{}]/g, '') // Remove braces
|
|
185
|
+
.replace(/"/g, ''); // Remove double quotes
|
|
170
186
|
}
|
|
171
|
-
if (
|
|
172
|
-
ddl += `
|
|
187
|
+
if (roles && roles.trim() !== '') {
|
|
188
|
+
ddl += ` TO ${roles}\n`;
|
|
173
189
|
}
|
|
174
|
-
ddl += ';\n\n';
|
|
175
190
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
191
|
+
if (policy.qual) {
|
|
192
|
+
ddl += ` USING (${policy.qual})\n`;
|
|
193
|
+
}
|
|
194
|
+
if (policy.with_check) {
|
|
195
|
+
ddl += ` WITH CHECK (${policy.with_check})\n`;
|
|
196
|
+
}
|
|
197
|
+
ddl += ';\n\n';
|
|
184
198
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
199
|
+
policies.push({
|
|
200
|
+
name: `${schemaName}_${tableName}_policies`,
|
|
201
|
+
type: 'rls',
|
|
202
|
+
category: `${schemaName}.${tableName}`,
|
|
203
|
+
schema: schemaName,
|
|
204
|
+
ddl,
|
|
205
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
206
|
+
});
|
|
190
207
|
}
|
|
208
|
+
return policies;
|
|
191
209
|
}
|
|
192
210
|
/**
|
|
193
211
|
* Fetch RLS enabled flag and policy count for all tables (pg_class.relrowsecurity + pg_policies)
|
|
@@ -463,7 +481,13 @@ async function fetchCronJobs(client, spinner, progress) {
|
|
|
463
481
|
}
|
|
464
482
|
}
|
|
465
483
|
catch (error) {
|
|
466
|
-
//
|
|
484
|
+
// pg_cron is optional. Only its absence is safe to skip; timeouts,
|
|
485
|
+
// permission failures, and other catalog errors must fail closed.
|
|
486
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
487
|
+
? String(error.code)
|
|
488
|
+
: undefined;
|
|
489
|
+
if (code !== '42P01')
|
|
490
|
+
throw error;
|
|
467
491
|
}
|
|
468
492
|
return cronJobs;
|
|
469
493
|
}
|
|
@@ -655,12 +679,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
655
679
|
progress.tables.total = tableCount;
|
|
656
680
|
progress.views.total = viewCount;
|
|
657
681
|
}
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
const
|
|
661
|
-
const MAX_CONCURRENT = Math.min(50, parseInt(envValue));
|
|
662
|
-
// Use env value (capped at minimum 5)
|
|
663
|
-
const CONCURRENT_LIMIT = Math.max(5, MAX_CONCURRENT);
|
|
682
|
+
// The client serializes queries on one connection. Keep the queue bounded and
|
|
683
|
+
// honour a requested value of 1 for constrained production catalog reads.
|
|
684
|
+
const CONCURRENT_LIMIT = resolveMaxConcurrent();
|
|
664
685
|
// Debug log (development only)
|
|
665
686
|
if (process.env.NODE_ENV === 'development' || process.env.SUPATOOL_DEBUG) {
|
|
666
687
|
console.log(`Processing ${allObjects.length} objects with ${CONCURRENT_LIMIT} concurrent operations`);
|
|
@@ -675,126 +696,107 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
675
696
|
let comment = '';
|
|
676
697
|
let timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
|
|
677
698
|
if (type === 'table') {
|
|
678
|
-
//
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
timestamp = tableStatsResult.rows[0].last_updated;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
catch (error) {
|
|
697
|
-
// On error use default timestamp
|
|
699
|
+
// Get table last updated time. Catalog errors must abort extraction so
|
|
700
|
+
// an incomplete definition set is never written.
|
|
701
|
+
const tableStatsResult = await client.query(`
|
|
702
|
+
SELECT
|
|
703
|
+
EXTRACT(EPOCH FROM GREATEST(
|
|
704
|
+
COALESCE(last_vacuum, '1970-01-01'::timestamp),
|
|
705
|
+
COALESCE(last_autovacuum, '1970-01-01'::timestamp),
|
|
706
|
+
COALESCE(last_analyze, '1970-01-01'::timestamp),
|
|
707
|
+
COALESCE(last_autoanalyze, '1970-01-01'::timestamp)
|
|
708
|
+
))::bigint as last_updated
|
|
709
|
+
FROM pg_stat_user_tables
|
|
710
|
+
WHERE relname = $1 AND schemaname = $2
|
|
711
|
+
`, [name, schemaName]);
|
|
712
|
+
if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
|
|
713
|
+
timestamp = tableStatsResult.rows[0].last_updated;
|
|
698
714
|
}
|
|
699
715
|
// Generate CREATE TABLE statement
|
|
700
716
|
ddl = await generateCreateTableDDL(client, name, schemaName);
|
|
701
717
|
// Get table comment
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
comment = tableCommentResult.rows[0].table_comment;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
catch (error) {
|
|
714
|
-
// On error no comment
|
|
718
|
+
const tableCommentResult = await client.query(`
|
|
719
|
+
SELECT obj_description(c.oid) as table_comment
|
|
720
|
+
FROM pg_class c
|
|
721
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
722
|
+
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
|
|
723
|
+
`, [name, schemaName]);
|
|
724
|
+
if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
|
|
725
|
+
comment = tableCommentResult.rows[0].table_comment;
|
|
715
726
|
}
|
|
716
727
|
}
|
|
717
728
|
else {
|
|
718
729
|
// View case
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
else if (value === 'off' || value === 'false') {
|
|
762
|
-
ddlStart += ' WITH (security_invoker = off)';
|
|
763
|
-
}
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
730
|
+
// Get view definition and security_invoker setting
|
|
731
|
+
const viewResult = await client.query(`
|
|
732
|
+
SELECT
|
|
733
|
+
pv.definition,
|
|
734
|
+
c.relname,
|
|
735
|
+
c.reloptions
|
|
736
|
+
FROM pg_views pv
|
|
737
|
+
JOIN pg_class c ON c.relname = pv.viewname
|
|
738
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
739
|
+
WHERE pv.schemaname = $1
|
|
740
|
+
AND pv.viewname = $2
|
|
741
|
+
AND n.nspname = $1
|
|
742
|
+
AND c.relkind = 'v'
|
|
743
|
+
`, [schemaName, name]);
|
|
744
|
+
if (viewResult.rows.length === 0) {
|
|
745
|
+
throw new Error(`View definition not found for ${schemaName}.${name}`);
|
|
746
|
+
}
|
|
747
|
+
const view = viewResult.rows[0];
|
|
748
|
+
// Get view comment
|
|
749
|
+
const viewCommentResult = await client.query(`
|
|
750
|
+
SELECT obj_description(c.oid) as view_comment
|
|
751
|
+
FROM pg_class c
|
|
752
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
753
|
+
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
|
|
754
|
+
`, [name, schemaName]);
|
|
755
|
+
// Add view comment at top
|
|
756
|
+
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
757
|
+
comment = viewCommentResult.rows[0].view_comment;
|
|
758
|
+
ddl = `-- ${comment}\n`;
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
ddl = `-- View: ${name}\n`;
|
|
762
|
+
}
|
|
763
|
+
// Add view definition
|
|
764
|
+
let ddlStart = `CREATE OR REPLACE VIEW ${schemaName}.${name}`;
|
|
765
|
+
// Check security_invoker setting
|
|
766
|
+
if (view.reloptions) {
|
|
767
|
+
for (const option of view.reloptions) {
|
|
768
|
+
if (option.startsWith('security_invoker=')) {
|
|
769
|
+
const value = option.split('=')[1];
|
|
770
|
+
if (value === 'on' || value === 'true') {
|
|
771
|
+
ddlStart += ' WITH (security_invoker = on)';
|
|
766
772
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
// Add COMMENT ON statement
|
|
770
|
-
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
771
|
-
ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
|
|
772
|
-
}
|
|
773
|
-
else {
|
|
774
|
-
ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
|
|
775
|
-
}
|
|
776
|
-
// Get view creation time (if available)
|
|
777
|
-
try {
|
|
778
|
-
const viewStatsResult = await client.query(`
|
|
779
|
-
SELECT EXTRACT(EPOCH FROM GREATEST(
|
|
780
|
-
COALESCE(pg_stat_get_last_vacuum_time(c.oid), '1970-01-01'::timestamp),
|
|
781
|
-
COALESCE(pg_stat_get_last_analyze_time(c.oid), '1970-01-01'::timestamp)
|
|
782
|
-
))::bigint as last_updated
|
|
783
|
-
FROM pg_class c
|
|
784
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
785
|
-
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
|
|
786
|
-
`, [name, schemaName]);
|
|
787
|
-
if (viewStatsResult.rows.length > 0 && viewStatsResult.rows[0].last_updated > 0) {
|
|
788
|
-
timestamp = viewStatsResult.rows[0].last_updated;
|
|
773
|
+
else if (value === 'off' || value === 'false') {
|
|
774
|
+
ddlStart += ' WITH (security_invoker = off)';
|
|
789
775
|
}
|
|
790
|
-
|
|
791
|
-
catch (error) {
|
|
792
|
-
// On error use default timestamp
|
|
776
|
+
break;
|
|
793
777
|
}
|
|
794
778
|
}
|
|
795
779
|
}
|
|
796
|
-
|
|
797
|
-
|
|
780
|
+
ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
|
|
781
|
+
// Add COMMENT ON statement
|
|
782
|
+
if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
|
|
783
|
+
ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
|
|
787
|
+
}
|
|
788
|
+
// Get view creation time (if available)
|
|
789
|
+
const viewStatsResult = await client.query(`
|
|
790
|
+
SELECT EXTRACT(EPOCH FROM GREATEST(
|
|
791
|
+
COALESCE(pg_stat_get_last_vacuum_time(c.oid), '1970-01-01'::timestamp),
|
|
792
|
+
COALESCE(pg_stat_get_last_analyze_time(c.oid), '1970-01-01'::timestamp)
|
|
793
|
+
))::bigint as last_updated
|
|
794
|
+
FROM pg_class c
|
|
795
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
796
|
+
WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
|
|
797
|
+
`, [name, schemaName]);
|
|
798
|
+
if (viewStatsResult.rows.length > 0 && viewStatsResult.rows[0].last_updated > 0) {
|
|
799
|
+
timestamp = viewStatsResult.rows[0].last_updated;
|
|
798
800
|
}
|
|
799
801
|
}
|
|
800
802
|
return {
|
|
@@ -837,20 +839,18 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
|
|
|
837
839
|
return result;
|
|
838
840
|
}
|
|
839
841
|
catch (error) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
843
|
+
throw new Error(`Failed to extract ${obj.schemaname}.${obj.tablename} (${obj.type}): ${detail}`);
|
|
842
844
|
}
|
|
843
845
|
});
|
|
844
846
|
// Wait for batch to complete
|
|
845
847
|
const batchResults = await Promise.all(batchPromises);
|
|
846
848
|
processedResults.push(...batchResults);
|
|
847
849
|
}
|
|
848
|
-
// Add to definitions
|
|
850
|
+
// Add to definitions
|
|
849
851
|
for (const result of processedResults) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
definitions.push(definition);
|
|
853
|
-
}
|
|
852
|
+
const { isTable, ...definition } = result;
|
|
853
|
+
definitions.push(definition);
|
|
854
854
|
}
|
|
855
855
|
return definitions;
|
|
856
856
|
}
|
|
@@ -880,18 +880,19 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
880
880
|
AND c.table_name = $2
|
|
881
881
|
ORDER BY c.ordinal_position
|
|
882
882
|
`, [schemaName, tableName]),
|
|
883
|
-
// Get primary key info
|
|
883
|
+
// Get primary key info directly from pg_catalog. information_schema's
|
|
884
|
+
// constraint views are substantially more expensive on large catalogs.
|
|
884
885
|
client.query(`
|
|
885
|
-
SELECT column_name
|
|
886
|
-
FROM
|
|
887
|
-
JOIN
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
WHERE
|
|
892
|
-
AND
|
|
893
|
-
AND
|
|
894
|
-
ORDER BY
|
|
886
|
+
SELECT a.attname AS column_name
|
|
887
|
+
FROM pg_constraint con
|
|
888
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
889
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
|
890
|
+
CROSS JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS keys(attnum, ord)
|
|
891
|
+
JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = keys.attnum
|
|
892
|
+
WHERE con.contype = 'p'
|
|
893
|
+
AND nsp.nspname = $1
|
|
894
|
+
AND rel.relname = $2
|
|
895
|
+
ORDER BY keys.ord
|
|
895
896
|
`, [schemaName, tableName]),
|
|
896
897
|
// Get table comment
|
|
897
898
|
client.query(`
|
|
@@ -914,21 +915,21 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
|
|
|
914
915
|
AND pgn.nspname = $1
|
|
915
916
|
ORDER BY c.ordinal_position
|
|
916
917
|
`, [schemaName, tableName]),
|
|
917
|
-
// Get UNIQUE constraints
|
|
918
|
+
// Get UNIQUE constraints directly from pg_catalog for the same reason.
|
|
918
919
|
client.query(`
|
|
919
|
-
SELECT
|
|
920
|
-
|
|
921
|
-
string_agg(
|
|
922
|
-
FROM
|
|
923
|
-
JOIN
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
WHERE
|
|
928
|
-
AND
|
|
929
|
-
AND
|
|
930
|
-
GROUP BY
|
|
931
|
-
ORDER BY
|
|
920
|
+
SELECT
|
|
921
|
+
con.conname AS constraint_name,
|
|
922
|
+
string_agg(a.attname, ', ' ORDER BY keys.ord) AS columns
|
|
923
|
+
FROM pg_constraint con
|
|
924
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
925
|
+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
|
926
|
+
CROSS JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS keys(attnum, ord)
|
|
927
|
+
JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = keys.attnum
|
|
928
|
+
WHERE con.contype = 'u'
|
|
929
|
+
AND nsp.nspname = $1
|
|
930
|
+
AND rel.relname = $2
|
|
931
|
+
GROUP BY con.oid, con.conname
|
|
932
|
+
ORDER BY con.conname
|
|
932
933
|
`, [schemaName, tableName]),
|
|
933
934
|
// Get FOREIGN KEY constraints
|
|
934
935
|
// Use pg_constraint directly to avoid the N² row explosion that occurs when
|
|
@@ -1514,6 +1515,9 @@ async function extractDefinitions(options) {
|
|
|
1514
1515
|
const spinner = ora('Connecting to database...').start();
|
|
1515
1516
|
const client = new pg_1.Client({
|
|
1516
1517
|
connectionString: encodedConnectionString,
|
|
1518
|
+
connectionTimeoutMillis: resolveConnectionTimeoutMs(),
|
|
1519
|
+
query_timeout: resolveQueryTimeoutMs(),
|
|
1520
|
+
statement_timeout: resolveQueryTimeoutMs(),
|
|
1517
1521
|
ssl: {
|
|
1518
1522
|
rejectUnauthorized: false,
|
|
1519
1523
|
ca: undefined
|
|
@@ -1557,17 +1561,12 @@ async function extractDefinitions(options) {
|
|
|
1557
1561
|
progress.tables.total = parseInt(tablesCountResult.rows[0].count);
|
|
1558
1562
|
progress.views.total = parseInt(viewsCountResult.rows[0].count);
|
|
1559
1563
|
// Get total RLS policy count (per table)
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
progress.rls.total = parseInt(rlsCountResult.rows[0].count);
|
|
1567
|
-
}
|
|
1568
|
-
catch (error) {
|
|
1569
|
-
progress.rls.total = 0;
|
|
1570
|
-
}
|
|
1564
|
+
const rlsCountResult = await client.query(`
|
|
1565
|
+
SELECT COUNT(DISTINCT tablename) as count
|
|
1566
|
+
FROM pg_policies
|
|
1567
|
+
WHERE schemaname = 'public'
|
|
1568
|
+
`);
|
|
1569
|
+
progress.rls.total = parseInt(rlsCountResult.rows[0].count);
|
|
1571
1570
|
// Get total functions count
|
|
1572
1571
|
const functionsCountResult = await client.query(`
|
|
1573
1572
|
SELECT COUNT(*) as count
|
|
@@ -1591,6 +1590,11 @@ async function extractDefinitions(options) {
|
|
|
1591
1590
|
progress.cronJobs.total = parseInt(cronCountResult.rows[0].count);
|
|
1592
1591
|
}
|
|
1593
1592
|
catch (error) {
|
|
1593
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
1594
|
+
? String(error.code)
|
|
1595
|
+
: undefined;
|
|
1596
|
+
if (code !== '42P01')
|
|
1597
|
+
throw error;
|
|
1594
1598
|
progress.cronJobs.total = 0;
|
|
1595
1599
|
}
|
|
1596
1600
|
// Get total custom types count
|
|
@@ -1653,37 +1657,23 @@ async function extractDefinitions(options) {
|
|
|
1653
1657
|
let relations = [];
|
|
1654
1658
|
let rpcTables = [];
|
|
1655
1659
|
let allSchemas = [];
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
catch (err) {
|
|
1671
|
-
if (process.env.SUPATOOL_DEBUG) {
|
|
1672
|
-
console.warn('RELATIONS/RPC_TABLES extraction skipped:', err);
|
|
1660
|
+
allSchemas = await fetchAllSchemas(client);
|
|
1661
|
+
relations = await fetchRelationList(client, schemas);
|
|
1662
|
+
const funcDefs = allDefinitions.filter(d => d.type === 'function');
|
|
1663
|
+
for (const f of funcDefs) {
|
|
1664
|
+
const tables = extractTableRefsFromFunctionDdl(f.ddl, f.schema ?? 'public');
|
|
1665
|
+
if (tables.length > 0) {
|
|
1666
|
+
rpcTables.push({
|
|
1667
|
+
rpc: f.schema ? `${f.schema}.${f.name}` : f.name,
|
|
1668
|
+
tables
|
|
1669
|
+
});
|
|
1673
1670
|
}
|
|
1674
1671
|
}
|
|
1675
1672
|
// RLS status (for Tables docs, rls_warnings.md, and extract-time warning)
|
|
1676
1673
|
let tableRlsStatus = [];
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
tableRlsStatus = await fetchTableRlsStatus(client, schemas);
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
catch (err) {
|
|
1684
|
-
if (process.env.SUPATOOL_DEBUG) {
|
|
1685
|
-
console.warn('RLS status fetch skipped:', err);
|
|
1686
|
-
}
|
|
1674
|
+
const tableDefs = allDefinitions.filter(d => d.type === 'table');
|
|
1675
|
+
if (tableDefs.length > 0) {
|
|
1676
|
+
tableRlsStatus = await fetchTableRlsStatus(client, schemas);
|
|
1687
1677
|
}
|
|
1688
1678
|
// Save definitions (table+RLS+triggers merged, schema folders)
|
|
1689
1679
|
spinner.text = 'Saving definitions to files...';
|
package/package.json
CHANGED