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.
@@ -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
- try {
104
- const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
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
- const groupedPolicies = {};
120
- for (const row of result.rows) {
121
- const tableKey = `${row.schemaname}.${row.tablename}`;
122
- if (!groupedPolicies[tableKey]) {
123
- groupedPolicies[tableKey] = [];
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
- const tableKeys = Object.keys(groupedPolicies);
128
- // Initialize progress
129
- if (progress) {
130
- progress.rls.total = tableKeys.length;
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
- for (let i = 0; i < tableKeys.length; i++) {
133
- const tableKey = tableKeys[i];
134
- const tablePolicies = groupedPolicies[tableKey];
135
- const firstPolicy = tablePolicies[0];
136
- const schemaName = firstPolicy.schemaname;
137
- const tableName = firstPolicy.tablename;
138
- // Update progress
139
- if (progress && spinner) {
140
- progress.rls.current = i + 1;
141
- displayProgress(progress, spinner);
142
- }
143
- // Add RLS policy description at top
144
- let ddl = `-- RLS Policies for ${schemaName}.${tableName}\n`;
145
- ddl += `-- Row Level Security policies to control data access at the row level\n\n`;
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
- if (policy.qual) {
169
- ddl += ` USING (${policy.qual})\n`;
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 (policy.with_check) {
172
- ddl += ` WITH CHECK (${policy.with_check})\n`;
187
+ if (roles && roles.trim() !== '') {
188
+ ddl += ` TO ${roles}\n`;
173
189
  }
174
- ddl += ';\n\n';
175
190
  }
176
- policies.push({
177
- name: `${schemaName}_${tableName}_policies`,
178
- type: 'rls',
179
- category: `${schemaName}.${tableName}`,
180
- schema: schemaName,
181
- ddl,
182
- timestamp: Math.floor(Date.now() / 1000)
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
- return policies;
186
- }
187
- catch (error) {
188
- console.warn('Skipping RLS policies extraction:', error instanceof Error ? error.message : String(error));
189
- return [];
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
- // Skip when pg_cron extension is not present
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
- // Process tables/views with limited concurrency (cap connection count)
659
- // Max configurable via env (default 20, max 50)
660
- const envValue = process.env.SUPATOOL_MAX_CONCURRENT || '20';
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
- // Table case
679
- try {
680
- // Get table last updated time
681
- const tableStatsResult = await client.query(`
682
- SELECT
683
- EXTRACT(EPOCH FROM GREATEST(
684
- COALESCE(last_vacuum, '1970-01-01'::timestamp),
685
- COALESCE(last_autovacuum, '1970-01-01'::timestamp),
686
- COALESCE(last_analyze, '1970-01-01'::timestamp),
687
- COALESCE(last_autoanalyze, '1970-01-01'::timestamp)
688
- ))::bigint as last_updated
689
- FROM pg_stat_user_tables
690
- WHERE relname = $1 AND schemaname = $2
691
- `, [name, schemaName]);
692
- if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
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
- try {
703
- const tableCommentResult = await client.query(`
704
- SELECT obj_description(c.oid) as table_comment
705
- FROM pg_class c
706
- JOIN pg_namespace n ON c.relnamespace = n.oid
707
- WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
708
- `, [name, schemaName]);
709
- if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
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
- try {
720
- // Get view definition and security_invoker setting
721
- const viewResult = await client.query(`
722
- SELECT
723
- pv.definition,
724
- c.relname,
725
- c.reloptions
726
- FROM pg_views pv
727
- JOIN pg_class c ON c.relname = pv.viewname
728
- JOIN pg_namespace n ON c.relnamespace = n.oid
729
- WHERE pv.schemaname = $1
730
- AND pv.viewname = $2
731
- AND n.nspname = $1
732
- AND c.relkind = 'v'
733
- `, [schemaName, name]);
734
- if (viewResult.rows.length > 0) {
735
- const view = viewResult.rows[0];
736
- // Get view comment
737
- const viewCommentResult = await client.query(`
738
- SELECT obj_description(c.oid) as view_comment
739
- FROM pg_class c
740
- JOIN pg_namespace n ON c.relnamespace = n.oid
741
- WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
742
- `, [name, schemaName]);
743
- // Add view comment at top
744
- if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
745
- comment = viewCommentResult.rows[0].view_comment;
746
- ddl = `-- ${comment}\n`;
747
- }
748
- else {
749
- ddl = `-- View: ${name}\n`;
750
- }
751
- // Add view definition
752
- let ddlStart = `CREATE OR REPLACE VIEW ${schemaName}.${name}`;
753
- // Check security_invoker setting
754
- if (view.reloptions) {
755
- for (const option of view.reloptions) {
756
- if (option.startsWith('security_invoker=')) {
757
- const value = option.split('=')[1];
758
- if (value === 'on' || value === 'true') {
759
- ddlStart += ' WITH (security_invoker = on)';
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
- ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
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
- catch (error) {
797
- // On error no comment
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
- console.error(`Error processing ${obj.type} ${obj.tablename}:`, error);
841
- return null;
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 excluding nulls
850
+ // Add to definitions
849
851
  for (const result of processedResults) {
850
- if (result) {
851
- const { isTable, ...definition } = result;
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 information_schema.table_constraints tc
887
- JOIN information_schema.key_column_usage kcu
888
- ON tc.constraint_catalog = kcu.constraint_catalog
889
- AND tc.constraint_schema = kcu.constraint_schema
890
- AND tc.constraint_name = kcu.constraint_name
891
- WHERE tc.table_schema = $1
892
- AND tc.table_name = $2
893
- AND tc.constraint_type = 'PRIMARY KEY'
894
- ORDER BY kcu.ordinal_position
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
- tc.constraint_name,
921
- string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns
922
- FROM information_schema.table_constraints tc
923
- JOIN information_schema.key_column_usage kcu
924
- ON tc.constraint_catalog = kcu.constraint_catalog
925
- AND tc.constraint_schema = kcu.constraint_schema
926
- AND tc.constraint_name = kcu.constraint_name
927
- WHERE tc.table_schema = $1
928
- AND tc.table_name = $2
929
- AND tc.constraint_type = 'UNIQUE'
930
- GROUP BY tc.constraint_name
931
- ORDER BY tc.constraint_name
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
- try {
1561
- const rlsCountResult = await client.query(`
1562
- SELECT COUNT(DISTINCT tablename) as count
1563
- FROM pg_policies
1564
- WHERE schemaname = 'public'
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
- try {
1657
- allSchemas = await fetchAllSchemas(client);
1658
- relations = await fetchRelationList(client, schemas);
1659
- const funcDefs = allDefinitions.filter(d => d.type === 'function');
1660
- for (const f of funcDefs) {
1661
- const tables = extractTableRefsFromFunctionDdl(f.ddl, f.schema ?? 'public');
1662
- if (tables.length > 0) {
1663
- rpcTables.push({
1664
- rpc: f.schema ? `${f.schema}.${f.name}` : f.name,
1665
- tables
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
- try {
1678
- const tableDefs = allDefinitions.filter(d => d.type === 'table');
1679
- if (tableDefs.length > 0) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supatool",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "CLI for PostgreSQL (Cloud SQL / Supabase): extract schema to files, deploy schema diffs, apply migrations, seed export.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",