supatool 0.6.2 → 0.6.4

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.
@@ -927,23 +927,30 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
927
927
  ORDER BY tc.constraint_name
928
928
  `, [schemaName, tableName]),
929
929
  // Get FOREIGN KEY constraints
930
+ // Use pg_constraint directly to avoid the N² row explosion that occurs when
931
+ // joining kcu × ccu on constraint_name alone for composite FKs.
930
932
  client.query(`
931
- SELECT
932
- tc.constraint_name,
933
- string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns,
934
- ccu.table_schema AS foreign_table_schema,
935
- ccu.table_name AS foreign_table_name,
936
- string_agg(ccu.column_name, ', ' ORDER BY kcu.ordinal_position) as foreign_columns
937
- FROM information_schema.table_constraints tc
938
- JOIN information_schema.key_column_usage kcu
939
- ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
940
- JOIN information_schema.constraint_column_usage ccu
941
- ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
942
- WHERE tc.table_schema = $1
943
- AND tc.table_name = $2
944
- AND tc.constraint_type = 'FOREIGN KEY'
945
- GROUP BY tc.constraint_name, ccu.table_schema, ccu.table_name
946
- ORDER BY tc.constraint_name
933
+ SELECT
934
+ c.conname AS constraint_name,
935
+ (SELECT string_agg(a.attname, ', ' ORDER BY u.ord)
936
+ FROM unnest(c.conkey) WITH ORDINALITY AS u(attnum, ord)
937
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = u.attnum
938
+ ) AS columns,
939
+ fn.nspname AS foreign_table_schema,
940
+ fc.relname AS foreign_table_name,
941
+ (SELECT string_agg(a.attname, ', ' ORDER BY u.ord)
942
+ FROM unnest(c.confkey) WITH ORDINALITY AS u(attnum, ord)
943
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = u.attnum
944
+ ) AS foreign_columns
945
+ FROM pg_constraint c
946
+ JOIN pg_class rel ON rel.oid = c.conrelid
947
+ JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
948
+ JOIN pg_class fc ON fc.oid = c.confrelid
949
+ JOIN pg_namespace fn ON fn.oid = fc.relnamespace
950
+ WHERE c.contype = 'f'
951
+ AND nsp.nspname = $1
952
+ AND rel.relname = $2
953
+ ORDER BY c.conname
947
954
  `, [schemaName, tableName])
948
955
  ]);
949
956
  const columnComments = new Map();
@@ -1353,7 +1360,7 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1353
1360
  }
1354
1361
  : undefined
1355
1362
  };
1356
- writeFileIfChanged(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2) + '\n');
1363
+ writeFileIfChanged(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2));
1357
1364
  // schema_summary.md (one-file overview for AI) — include RLS status per table
1358
1365
  let summaryMd = '# Schema summary\n\n';
1359
1366
  const tableDefs = definitions.filter(d => d.type === 'table' || d.type === 'view');
@@ -1389,6 +1396,7 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1389
1396
  writeFileIfChanged(path.join(outputDir, 'schema_summary.md'), summaryMd);
1390
1397
  // RLS disabled tables warning doc (tables only; RLS enabled with 0 policies is not warned)
1391
1398
  const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
1399
+ const rlsWarningsPath = path.join(outputDir, 'rls_warnings.md');
1392
1400
  if (rlsNotEnabled.length > 0) {
1393
1401
  let warnMd = '# Tables with RLS disabled (warning)\n\n';
1394
1402
  warnMd += 'The following tables do not have Row Level Security enabled.\n';
@@ -1397,7 +1405,11 @@ async function generateIndexFile(definitions, outputDir, separateDirectories = t
1397
1405
  rlsNotEnabled.forEach(s => {
1398
1406
  warnMd += `| ${s.schema} | ${s.table} |\n`;
1399
1407
  });
1400
- writeFileIfChanged(path.join(outputDir, 'rls_warnings.md'), warnMd);
1408
+ writeFileIfChanged(rlsWarningsPath, warnMd);
1409
+ }
1410
+ else if (fs.existsSync(rlsWarningsPath)) {
1411
+ // All RLS issues resolved — remove stale warning file
1412
+ fs.unlinkSync(rlsWarningsPath);
1401
1413
  }
1402
1414
  }
1403
1415
  /**
@@ -1529,7 +1541,10 @@ async function extractDefinitions(options) {
1529
1541
  cronJobs: { current: 0, total: 0 },
1530
1542
  customTypes: { current: 0, total: 0 }
1531
1543
  };
1532
- if (all) {
1544
+ // --schema-only implies full extraction of all object types for the target schema,
1545
+ // so that --force does not incorrectly delete rpc/cron/types files.
1546
+ const effectiveAll = all || schemaOnly;
1547
+ if (effectiveAll) {
1533
1548
  // Get total count for each object type first
1534
1549
  spinner.text = 'Counting database objects...';
1535
1550
  // Get total tables/views count
@@ -1591,7 +1606,7 @@ async function extractDefinitions(options) {
1591
1606
  AND t.typname NOT LIKE '_%'
1592
1607
  `);
1593
1608
  progress.customTypes.total = parseInt(typesCountResult.rows[0].count);
1594
- // When --all: fetch all objects (sequential)
1609
+ // When --all / --schema-only: fetch all objects (sequential)
1595
1610
  const tables = await fetchTableDefinitions(client, spinner, progress, schemas);
1596
1611
  const rlsPolicies = await fetchRlsPolicies(client, spinner, progress, schemas);
1597
1612
  const functions = await fetchFunctions(client, spinner, progress, schemas);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supatool",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",