supatool 0.3.7 → 0.4.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.
@@ -37,60 +37,57 @@ exports.extractDefinitions = extractDefinitions;
37
37
  exports.generateCreateTableDDL = generateCreateTableDDL;
38
38
  const pg_1 = require("pg");
39
39
  /**
40
- * 進行状況を表示
40
+ * Display progress
41
41
  */
42
- // グローバルなプログレス状態を保存
42
+ // Store global progress state
43
43
  let globalProgress = null;
44
44
  let progressUpdateInterval = null;
45
45
  function displayProgress(progress, spinner) {
46
- // グローバル状態を更新
46
+ // Update global state
47
47
  globalProgress = progress;
48
- // 定期更新を開始(まだ開始していない場合)
48
+ // Start periodic update if not already
49
49
  if (!progressUpdateInterval) {
50
50
  progressUpdateInterval = setInterval(() => {
51
51
  if (globalProgress && spinner) {
52
52
  updateSpinnerDisplay(globalProgress, spinner);
53
53
  }
54
- }, 80); // 80msで常時更新
54
+ }, 80); // Update every 80ms
55
55
  }
56
- // 即座に表示を更新
56
+ // Update display immediately
57
57
  updateSpinnerDisplay(progress, spinner);
58
58
  }
59
59
  function updateSpinnerDisplay(progress, spinner) {
60
- // 全体進捗を計算
60
+ // Compute overall progress
61
61
  const totalObjects = progress.tables.total + progress.views.total + progress.rls.total +
62
62
  progress.functions.total + progress.triggers.total + progress.cronJobs.total +
63
63
  progress.customTypes.total;
64
64
  const completedObjects = progress.tables.current + progress.views.current + progress.rls.current +
65
65
  progress.functions.current + progress.triggers.current + progress.cronJobs.current +
66
66
  progress.customTypes.current;
67
- // プログレスバーを生成(稲妻が単純に増えていく)
67
+ // Build progress bar
68
68
  const createProgressBar = (current, total, width = 20) => {
69
69
  if (total === 0)
70
70
  return '░'.repeat(width);
71
71
  const percentage = Math.min(current / total, 1);
72
72
  const filled = Math.floor(percentage * width);
73
73
  const empty = width - filled;
74
- // 稲妻で単純に埋める(文字の入れ替えなし)
75
74
  return '⚡'.repeat(filled) + '░'.repeat(empty);
76
75
  };
77
- // 全体プログレスバーのみ表示(コンソール幅に収める)
76
+ // Show overall bar only (fit to console width)
78
77
  const overallBar = createProgressBar(completedObjects, totalObjects, 20);
79
78
  const overallPercent = totalObjects > 0 ? Math.floor((completedObjects / totalObjects) * 100) : 0;
80
- // 緑色回転スピナー(常時回転)
79
+ // Green rotating spinner
81
80
  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
82
81
  const spinnerFrame = Math.floor((Date.now() / 80) % spinnerFrames.length);
83
82
  const greenSpinner = `\x1b[32m${spinnerFrames[spinnerFrame]}\x1b[0m`;
84
- // ドットアニメーション
83
+ // Dot animation
85
84
  const dotFrames = ["", ".", "..", "..."];
86
85
  const dotFrame = Math.floor((Date.now() / 400) % dotFrames.length);
87
- // 解析中メッセージ
88
86
  const statusMessage = "Extracting";
89
- // 改行制御付きコンパクト表示(緑色スピナー付き)
90
87
  const display = `\r${greenSpinner} [${overallBar}] ${overallPercent}% (${completedObjects}/${totalObjects}) ${statusMessage}${dotFrames[dotFrame]}`;
91
88
  spinner.text = display;
92
89
  }
93
- // 進捗表示を停止する関数
90
+ // Stop progress display
94
91
  function stopProgressDisplay() {
95
92
  if (progressUpdateInterval) {
96
93
  clearInterval(progressUpdateInterval);
@@ -99,7 +96,7 @@ function stopProgressDisplay() {
99
96
  globalProgress = null;
100
97
  }
101
98
  /**
102
- * RLSポリシーを取得
99
+ * Fetch RLS policies
103
100
  */
104
101
  async function fetchRlsPolicies(client, spinner, progress, schemas = ['public']) {
105
102
  const policies = [];
@@ -128,7 +125,7 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
128
125
  groupedPolicies[tableKey].push(row);
129
126
  }
130
127
  const tableKeys = Object.keys(groupedPolicies);
131
- // 進行状況の初期化
128
+ // Initialize progress
132
129
  if (progress) {
133
130
  progress.rls.total = tableKeys.length;
134
131
  }
@@ -138,12 +135,12 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
138
135
  const firstPolicy = tablePolicies[0];
139
136
  const schemaName = firstPolicy.schemaname;
140
137
  const tableName = firstPolicy.tablename;
141
- // 進行状況を更新
138
+ // Update progress
142
139
  if (progress && spinner) {
143
140
  progress.rls.current = i + 1;
144
141
  displayProgress(progress, spinner);
145
142
  }
146
- // RLSポリシー説明を先頭に追加
143
+ // Add RLS policy description at top
147
144
  let ddl = `-- RLS Policies for ${schemaName}.${tableName}\n`;
148
145
  ddl += `-- Row Level Security policies to control data access at the row level\n\n`;
149
146
  ddl += `ALTER TABLE ${schemaName}.${tableName} ENABLE ROW LEVEL SECURITY;\n\n`;
@@ -153,16 +150,16 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
153
150
  ddl += ` AS ${policy.permissive || 'PERMISSIVE'}\n`;
154
151
  ddl += ` FOR ${policy.cmd || 'ALL'}\n`;
155
152
  if (policy.roles) {
156
- // rolesが配列の場合と文字列の場合を処理
153
+ // Handle roles as array or string
157
154
  let roles;
158
155
  if (Array.isArray(policy.roles)) {
159
156
  roles = policy.roles.join(', ');
160
157
  }
161
158
  else {
162
- // PostgreSQLの配列リテラル形式 "{role1,role2}" または単純な文字列を処理
159
+ // Handle PostgreSQL array literal "{role1,role2}" or plain string
163
160
  roles = String(policy.roles)
164
- .replace(/[{}]/g, '') // 中括弧を除去
165
- .replace(/"/g, ''); // ダブルクォートを除去
161
+ .replace(/[{}]/g, '') // Remove braces
162
+ .replace(/"/g, ''); // Remove double quotes
166
163
  }
167
164
  if (roles && roles.trim() !== '') {
168
165
  ddl += ` TO ${roles}\n`;
@@ -180,6 +177,7 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
180
177
  name: `${schemaName}_${tableName}_policies`,
181
178
  type: 'rls',
182
179
  category: `${schemaName}.${tableName}`,
180
+ schema: schemaName,
183
181
  ddl,
184
182
  timestamp: Math.floor(Date.now() / 1000)
185
183
  });
@@ -192,7 +190,72 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
192
190
  }
193
191
  }
194
192
  /**
195
- * 関数を取得
193
+ * Fetch FK relations list (for llms.txt RELATIONS)
194
+ */
195
+ async function fetchRelationList(client, schemas = ['public']) {
196
+ if (schemas.length === 0)
197
+ return [];
198
+ const schemaPlaceholders = schemas.map((_, i) => `$${i + 1}`).join(', ');
199
+ const result = await client.query(`
200
+ SELECT
201
+ tc.table_schema AS from_schema,
202
+ tc.table_name AS from_table,
203
+ ccu.table_schema AS to_schema,
204
+ ccu.table_name AS to_table
205
+ FROM information_schema.table_constraints tc
206
+ JOIN information_schema.constraint_column_usage ccu
207
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
208
+ WHERE tc.constraint_type = 'FOREIGN KEY'
209
+ AND tc.table_schema IN (${schemaPlaceholders})
210
+ ORDER BY tc.table_schema, tc.table_name
211
+ `, schemas);
212
+ return result.rows.map((r) => ({
213
+ from: `${r.from_schema}.${r.from_table}`,
214
+ to: `${r.to_schema}.${r.to_table}`
215
+ }));
216
+ }
217
+ /**
218
+ * Fetch all schemas in DB (for llms.txt ALL_SCHEMAS / unrextracted schemas)
219
+ */
220
+ async function fetchAllSchemas(client) {
221
+ const result = await client.query(`
222
+ SELECT schema_name
223
+ FROM information_schema.schemata
224
+ WHERE schema_name NOT LIKE 'pg_%'
225
+ AND schema_name != 'information_schema'
226
+ AND schema_name != 'pg_catalog'
227
+ ORDER BY schema_name
228
+ `);
229
+ return result.rows.map((r) => r.schema_name);
230
+ }
231
+ /**
232
+ * Extract table refs from function DDL (heuristic, best-effort)
233
+ */
234
+ function extractTableRefsFromFunctionDdl(ddl, defaultSchema = 'public') {
235
+ const seen = new Set();
236
+ const add = (schema, table) => {
237
+ const key = `${schema}.${table}`;
238
+ if (!seen.has(key))
239
+ seen.add(key);
240
+ };
241
+ // schema.table form
242
+ const schemaTableRe = /(?:FROM|JOIN|INTO|UPDATE|DELETE\s+FROM|INSERT\s+INTO)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)/gi;
243
+ let m;
244
+ while ((m = schemaTableRe.exec(ddl)) !== null) {
245
+ add(m[1], m[2]);
246
+ }
247
+ // Unqualified table name (after FROM / JOIN / INSERT INTO / UPDATE / DELETE FROM)
248
+ const unqualifiedRe = /(?:FROM|JOIN|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s|$|\)|,)/gi;
249
+ while ((m = unqualifiedRe.exec(ddl)) !== null) {
250
+ const name = m[1].toLowerCase();
251
+ if (!['select', 'where', 'on', 'and', 'or', 'inner', 'left', 'right', 'outer', 'cross', 'lateral'].includes(name)) {
252
+ add(defaultSchema, m[1]);
253
+ }
254
+ }
255
+ return Array.from(seen).sort();
256
+ }
257
+ /**
258
+ * Fetch functions
196
259
  */
197
260
  async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
198
261
  const functions = [];
@@ -211,20 +274,20 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
211
274
  AND p.prokind IN ('f', 'p') -- functions and procedures
212
275
  ORDER BY n.nspname, p.proname
213
276
  `, schemas);
214
- // 進行状況の初期化
277
+ // Initialize progress
215
278
  if (progress) {
216
279
  progress.functions.total = result.rows.length;
217
280
  }
218
281
  for (let i = 0; i < result.rows.length; i++) {
219
282
  const row = result.rows[i];
220
- // 進行状況を更新
283
+ // Update progress
221
284
  if (progress && spinner) {
222
285
  progress.functions.current = i + 1;
223
286
  displayProgress(progress, spinner);
224
287
  }
225
- // 正確な関数シグネチャを構築(スキーマ名と引数の型を含む)
288
+ // Build exact function signature (schema name and argument types)
226
289
  const functionSignature = `${row.schema_name}.${row.name}(${row.identity_args || ''})`;
227
- // 関数コメントを先頭に追加
290
+ // Add function comment at top
228
291
  let ddl = '';
229
292
  if (!row.comment) {
230
293
  ddl += `-- Function: ${functionSignature}\n`;
@@ -232,13 +295,13 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
232
295
  else {
233
296
  ddl += `-- ${row.comment}\n`;
234
297
  }
235
- // 関数定義を追加(セミコロンを確実に付与)
298
+ // Add function definition (ensure semicolon)
236
299
  let functionDef = row.definition;
237
300
  if (!functionDef.trim().endsWith(';')) {
238
301
  functionDef += ';';
239
302
  }
240
303
  ddl += functionDef + '\n\n';
241
- // COMMENT ON文を追加
304
+ // Add COMMENT ON statement
242
305
  if (!row.comment) {
243
306
  ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
244
307
  }
@@ -248,6 +311,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
248
311
  functions.push({
249
312
  name: row.name,
250
313
  type: 'function',
314
+ schema: row.schema_name,
251
315
  ddl,
252
316
  comment: row.comment,
253
317
  timestamp: Math.floor(Date.now() / 1000)
@@ -256,7 +320,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
256
320
  return functions;
257
321
  }
258
322
  /**
259
- * トリガーを取得
323
+ * Fetch triggers
260
324
  */
261
325
  async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
262
326
  const triggers = [];
@@ -274,18 +338,18 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
274
338
  AND NOT t.tgisinternal
275
339
  ORDER BY n.nspname, c.relname, t.tgname
276
340
  `, schemas);
277
- // 進行状況の初期化
341
+ // Initialize progress
278
342
  if (progress) {
279
343
  progress.triggers.total = result.rows.length;
280
344
  }
281
345
  for (let i = 0; i < result.rows.length; i++) {
282
346
  const row = result.rows[i];
283
- // 進行状況を更新
347
+ // Update progress
284
348
  if (progress && spinner) {
285
349
  progress.triggers.current = i + 1;
286
350
  displayProgress(progress, spinner);
287
351
  }
288
- // トリガー説明を先頭に追加
352
+ // Add trigger description at top
289
353
  let ddl = `-- Trigger: ${row.trigger_name} on ${row.schema_name}.${row.table_name}\n`;
290
354
  ddl += `-- Database trigger that automatically executes in response to certain events\n\n`;
291
355
  ddl += row.definition + ';';
@@ -293,6 +357,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
293
357
  name: `${row.schema_name}_${row.table_name}_${row.trigger_name}`,
294
358
  type: 'trigger',
295
359
  category: `${row.schema_name}.${row.table_name}`,
360
+ schema: row.schema_name,
296
361
  ddl,
297
362
  timestamp: Math.floor(Date.now() / 1000)
298
363
  });
@@ -300,7 +365,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
300
365
  return triggers;
301
366
  }
302
367
  /**
303
- * Cronジョブを取得(pg_cron拡張)
368
+ * Fetch Cron jobs (pg_cron extension)
304
369
  */
305
370
  async function fetchCronJobs(client, spinner, progress) {
306
371
  const cronJobs = [];
@@ -315,18 +380,18 @@ async function fetchCronJobs(client, spinner, progress) {
315
380
  FROM cron.job
316
381
  ORDER BY jobid
317
382
  `);
318
- // 進行状況の初期化
383
+ // Initialize progress
319
384
  if (progress) {
320
385
  progress.cronJobs.total = result.rows.length;
321
386
  }
322
387
  for (let i = 0; i < result.rows.length; i++) {
323
388
  const row = result.rows[i];
324
- // 進行状況を更新
389
+ // Update progress
325
390
  if (progress && spinner) {
326
391
  progress.cronJobs.current = i + 1;
327
392
  displayProgress(progress, spinner);
328
393
  }
329
- // Cronジョブ説明を先頭に追加
394
+ // Add Cron job description at top
330
395
  let ddl = `-- Cron Job: ${row.jobname || `job_${row.jobid}`}\n`;
331
396
  ddl += `-- Scheduled job that runs automatically at specified intervals\n`;
332
397
  ddl += `-- Schedule: ${row.schedule}\n`;
@@ -341,12 +406,12 @@ async function fetchCronJobs(client, spinner, progress) {
341
406
  }
342
407
  }
343
408
  catch (error) {
344
- // pg_cron拡張がない場合はスキップ
409
+ // Skip when pg_cron extension is not present
345
410
  }
346
411
  return cronJobs;
347
412
  }
348
413
  /**
349
- * カスタム型を取得
414
+ * Fetch custom types
350
415
  */
351
416
  async function fetchCustomTypes(client, spinner, progress, schemas = ['public']) {
352
417
  const types = [];
@@ -368,41 +433,41 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
368
433
  JOIN pg_namespace n ON t.typnamespace = n.oid
369
434
  WHERE n.nspname IN (${schemaPlaceholders})
370
435
  AND t.typtype IN ('e', 'c', 'd')
371
- AND t.typisdefined = true -- 定義済みの型のみ
372
- AND NOT t.typarray = 0 -- 配列の基底型を除外
436
+ AND t.typisdefined = true -- defined types only
437
+ AND NOT t.typarray = 0 -- exclude array base types
373
438
  AND NOT EXISTS (
374
- -- テーブル、ビュー、インデックス、シーケンス、複合型と同名のものを除外
439
+ -- exclude same-name as table, view, index, sequence, composite
375
440
  SELECT 1 FROM pg_class c
376
441
  WHERE c.relname = t.typname
377
442
  AND c.relnamespace = n.oid
378
443
  )
379
444
  AND NOT EXISTS (
380
- -- 関数・プロシージャと同名のものを除外
445
+ -- exclude same-name as function/procedure
381
446
  SELECT 1 FROM pg_proc p
382
447
  WHERE p.proname = t.typname
383
448
  AND p.pronamespace = n.oid
384
449
  )
385
- AND t.typname NOT LIKE 'pg_%' -- PostgreSQL内部型を除外
386
- AND t.typname NOT LIKE '_%' -- 配列型(アンダースコアで始まる)を除外
387
- AND t.typname NOT LIKE '%_old' -- 削除予定の型を除外
388
- AND t.typname NOT LIKE '%_bak' -- バックアップ型を除外
389
- AND t.typname NOT LIKE 'tmp_%' -- 一時的な型を除外
450
+ AND t.typname NOT LIKE 'pg_%' -- exclude PostgreSQL built-in types
451
+ AND t.typname NOT LIKE '_%' -- exclude array types (leading underscore)
452
+ AND t.typname NOT LIKE '%_old' -- exclude deprecated types
453
+ AND t.typname NOT LIKE '%_bak' -- exclude backup types
454
+ AND t.typname NOT LIKE 'tmp_%' -- exclude temporary types
390
455
  ORDER BY n.nspname, t.typname
391
456
  `, schemas);
392
- // 進行状況の初期化
457
+ // Initialize progress
393
458
  if (progress) {
394
459
  progress.customTypes.total = result.rows.length;
395
460
  }
396
461
  for (let i = 0; i < result.rows.length; i++) {
397
462
  const row = result.rows[i];
398
- // 進行状況を更新
463
+ // Update progress
399
464
  if (progress && spinner) {
400
465
  progress.customTypes.current = i + 1;
401
466
  displayProgress(progress, spinner);
402
467
  }
403
468
  let ddl = '';
404
469
  if (row.type_category === 'enum') {
405
- // ENUM型の詳細を取得
470
+ // Fetch ENUM type details
406
471
  const enumResult = await client.query(`
407
472
  SELECT enumlabel
408
473
  FROM pg_enum
@@ -414,14 +479,14 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
414
479
  )
415
480
  ORDER BY enumsortorder
416
481
  `, [row.type_name, row.schema_name]);
417
- // ENUM値が存在する場合のみDDLを生成
482
+ // Generate DDL only when ENUM values exist
418
483
  if (enumResult.rows.length > 0) {
419
484
  const labels = enumResult.rows.map(r => `'${r.enumlabel}'`).join(', ');
420
485
  ddl = `CREATE TYPE ${row.type_name} AS ENUM (${labels});`;
421
486
  }
422
487
  }
423
488
  else if (row.type_category === 'composite') {
424
- // COMPOSITE型の詳細を取得
489
+ // Fetch COMPOSITE type details
425
490
  const compositeResult = await client.query(`
426
491
  SELECT
427
492
  a.attname as column_name,
@@ -434,10 +499,10 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
434
499
  )
435
500
  )
436
501
  AND a.attnum > 0
437
- AND NOT a.attisdropped -- 削除されたカラムを除外
502
+ AND NOT a.attisdropped -- exclude dropped columns
438
503
  ORDER BY a.attnum
439
504
  `, [row.type_name, row.schema_name]);
440
- // コンポジット型の属性が存在する場合のみDDLを生成
505
+ // Generate DDL only when composite type has attributes
441
506
  if (compositeResult.rows.length > 0) {
442
507
  const columns = compositeResult.rows
443
508
  .map(r => ` ${r.column_name} ${r.column_type}`)
@@ -446,7 +511,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
446
511
  }
447
512
  }
448
513
  else if (row.type_category === 'domain') {
449
- // DOMAIN型の詳細を取得
514
+ // Fetch DOMAIN type details
450
515
  const domainResult = await client.query(`
451
516
  SELECT
452
517
  pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
@@ -476,7 +541,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
476
541
  }
477
542
  }
478
543
  if (ddl) {
479
- // 型コメントを先頭に追加
544
+ // Add type comment at top
480
545
  let finalDdl = '';
481
546
  if (!row.comment) {
482
547
  finalDdl += `-- Type: ${row.type_name}\n`;
@@ -484,9 +549,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
484
549
  else {
485
550
  finalDdl += `-- ${row.comment}\n`;
486
551
  }
487
- // 型定義を追加
552
+ // Add type definition
488
553
  finalDdl += ddl + '\n\n';
489
- // COMMENT ON文を追加
554
+ // Add COMMENT ON statement
490
555
  if (!row.comment) {
491
556
  finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
492
557
  }
@@ -494,8 +559,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
494
559
  finalDdl += `COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '${row.comment}';\n\n`;
495
560
  }
496
561
  types.push({
497
- name: `${row.schema_name}_${row.type_name}`,
562
+ name: row.type_name,
498
563
  type: 'type',
564
+ schema: row.schema_name,
499
565
  ddl: finalDdl,
500
566
  comment: row.comment,
501
567
  timestamp: Math.floor(Date.now() / 1000)
@@ -505,19 +571,19 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
505
571
  return types;
506
572
  }
507
573
  /**
508
- * データベースからテーブル定義を取得
574
+ * Fetch table definitions from database
509
575
  */
510
576
  async function fetchTableDefinitions(client, spinner, progress, schemas = ['public']) {
511
577
  const definitions = [];
512
578
  const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
513
- // テーブル一覧を取得
579
+ // Fetch table list
514
580
  const tablesResult = await client.query(`
515
581
  SELECT tablename, schemaname, 'table' as type
516
582
  FROM pg_tables
517
583
  WHERE schemaname IN (${schemaPlaceholders})
518
584
  ORDER BY schemaname, tablename
519
585
  `, schemas);
520
- // ビュー一覧を取得
586
+ // Fetch view list
521
587
  const viewsResult = await client.query(`
522
588
  SELECT viewname as tablename, schemaname, 'view' as type
523
589
  FROM pg_views
@@ -527,22 +593,22 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
527
593
  const allObjects = [...tablesResult.rows, ...viewsResult.rows];
528
594
  const tableCount = tablesResult.rows.length;
529
595
  const viewCount = viewsResult.rows.length;
530
- // 進行状況の初期化
596
+ // Initialize progress
531
597
  if (progress) {
532
598
  progress.tables.total = tableCount;
533
599
  progress.views.total = viewCount;
534
600
  }
535
- // 制限付き並行処理でテーブル/ビューを処理(接続数を制限)
536
- // 環境変数で最大値を設定可能(デフォルト20、最大50
601
+ // Process tables/views with limited concurrency (cap connection count)
602
+ // Max configurable via env (default 20, max 50)
537
603
  const envValue = process.env.SUPATOOL_MAX_CONCURRENT || '20';
538
604
  const MAX_CONCURRENT = Math.min(50, parseInt(envValue));
539
- // 環境変数で設定された値を使用(最小5でキャップ)
605
+ // Use env value (capped at minimum 5)
540
606
  const CONCURRENT_LIMIT = Math.max(5, MAX_CONCURRENT);
541
- // デバッグログ(開発時のみ)
607
+ // Debug log (development only)
542
608
  if (process.env.NODE_ENV === 'development' || process.env.SUPATOOL_DEBUG) {
543
609
  console.log(`Processing ${allObjects.length} objects with ${CONCURRENT_LIMIT} concurrent operations`);
544
610
  }
545
- // テーブル/ビュー処理のPromise生成関数
611
+ // Promise factory for table/view processing
546
612
  const processObject = async (obj, index) => {
547
613
  const isTable = obj.type === 'table';
548
614
  const name = obj.tablename;
@@ -552,9 +618,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
552
618
  let comment = '';
553
619
  let timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
554
620
  if (type === 'table') {
555
- // テーブルの場合
621
+ // Table case
556
622
  try {
557
- // テーブルの最終更新時刻を取得
623
+ // Get table last updated time
558
624
  const tableStatsResult = await client.query(`
559
625
  SELECT
560
626
  EXTRACT(EPOCH FROM GREATEST(
@@ -571,11 +637,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
571
637
  }
572
638
  }
573
639
  catch (error) {
574
- // エラーの場合はデフォルトタイムスタンプを使用
640
+ // On error use default timestamp
575
641
  }
576
- // CREATE TABLE文を生成
642
+ // Generate CREATE TABLE statement
577
643
  ddl = await generateCreateTableDDL(client, name, schemaName);
578
- // テーブルコメントを取得
644
+ // Get table comment
579
645
  try {
580
646
  const tableCommentResult = await client.query(`
581
647
  SELECT obj_description(c.oid) as table_comment
@@ -588,13 +654,13 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
588
654
  }
589
655
  }
590
656
  catch (error) {
591
- // エラーの場合はコメントなし
657
+ // On error no comment
592
658
  }
593
659
  }
594
660
  else {
595
- // ビューの場合
661
+ // View case
596
662
  try {
597
- // ビューの定義とsecurity_invoker設定を取得
663
+ // Get view definition and security_invoker setting
598
664
  const viewResult = await client.query(`
599
665
  SELECT
600
666
  pv.definition,
@@ -610,14 +676,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
610
676
  `, [schemaName, name]);
611
677
  if (viewResult.rows.length > 0) {
612
678
  const view = viewResult.rows[0];
613
- // ビューのコメントを取得
679
+ // Get view comment
614
680
  const viewCommentResult = await client.query(`
615
681
  SELECT obj_description(c.oid) as view_comment
616
682
  FROM pg_class c
617
683
  JOIN pg_namespace n ON c.relnamespace = n.oid
618
684
  WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
619
685
  `, [name, schemaName]);
620
- // ビューコメントを先頭に追加
686
+ // Add view comment at top
621
687
  if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
622
688
  comment = viewCommentResult.rows[0].view_comment;
623
689
  ddl = `-- ${comment}\n`;
@@ -625,9 +691,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
625
691
  else {
626
692
  ddl = `-- View: ${name}\n`;
627
693
  }
628
- // ビュー定義を追加
629
- let ddlStart = `CREATE OR REPLACE VIEW ${name}`;
630
- // security_invoker設定をチェック
694
+ // Add view definition
695
+ let ddlStart = `CREATE OR REPLACE VIEW ${schemaName}.${name}`;
696
+ // Check security_invoker setting
631
697
  if (view.reloptions) {
632
698
  for (const option of view.reloptions) {
633
699
  if (option.startsWith('security_invoker=')) {
@@ -643,14 +709,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
643
709
  }
644
710
  }
645
711
  ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
646
- // COMMENT ON文を追加
712
+ // Add COMMENT ON statement
647
713
  if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
648
714
  ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
649
715
  }
650
716
  else {
651
717
  ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
652
718
  }
653
- // ビューの作成時刻を取得(可能であれば)
719
+ // Get view creation time (if available)
654
720
  try {
655
721
  const viewStatsResult = await client.query(`
656
722
  SELECT EXTRACT(EPOCH FROM GREATEST(
@@ -666,41 +732,42 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
666
732
  }
667
733
  }
668
734
  catch (error) {
669
- // エラーの場合はデフォルトタイムスタンプを使用
735
+ // On error use default timestamp
670
736
  }
671
737
  }
672
738
  }
673
739
  catch (error) {
674
- // エラーの場合はコメントなし
740
+ // On error no comment
675
741
  }
676
742
  }
677
743
  return {
678
- name: schemaName === 'public' ? name : `${schemaName}_${name}`,
744
+ name,
679
745
  type,
746
+ schema: schemaName,
680
747
  ddl,
681
748
  timestamp,
682
749
  comment: comment || undefined,
683
750
  isTable
684
751
  };
685
752
  };
686
- // シンプルなバッチ並行処理(確実な進行状況更新)
753
+ // Simple batch concurrency (reliable progress updates)
687
754
  const processedResults = [];
688
755
  for (let i = 0; i < allObjects.length; i += CONCURRENT_LIMIT) {
689
756
  const batch = allObjects.slice(i, i + CONCURRENT_LIMIT);
690
- // バッチを並行処理
757
+ // Process batch in parallel
691
758
  const batchPromises = batch.map(async (obj, batchIndex) => {
692
759
  try {
693
760
  const globalIndex = i + batchIndex;
694
- // デバッグ: 処理開始
761
+ // Debug: start
695
762
  if (process.env.SUPATOOL_DEBUG) {
696
763
  console.log(`Starting ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
697
764
  }
698
765
  const result = await processObject(obj, globalIndex);
699
- // デバッグ: 処理完了
766
+ // Debug: done
700
767
  if (process.env.SUPATOOL_DEBUG) {
701
768
  console.log(`Completed ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
702
769
  }
703
- // 個別完了時に即座に進行状況を更新
770
+ // Update progress immediately on each completion
704
771
  if (result && progress && spinner) {
705
772
  if (result.isTable) {
706
773
  progress.tables.current = Math.min(progress.tables.current + 1, progress.tables.total);
@@ -717,11 +784,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
717
784
  return null;
718
785
  }
719
786
  });
720
- // バッチの完了を待機
787
+ // Wait for batch to complete
721
788
  const batchResults = await Promise.all(batchPromises);
722
789
  processedResults.push(...batchResults);
723
790
  }
724
- // null値を除外してdefinitionsに追加
791
+ // Add to definitions excluding nulls
725
792
  for (const result of processedResults) {
726
793
  if (result) {
727
794
  const { isTable, ...definition } = result;
@@ -731,12 +798,12 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
731
798
  return definitions;
732
799
  }
733
800
  /**
734
- * CREATE TABLE DDLを生成(並行処理版)
801
+ * Generate CREATE TABLE DDL (concurrent version)
735
802
  */
736
803
  async function generateCreateTableDDL(client, tableName, schemaName = 'public') {
737
- // 全てのクエリを並行実行
804
+ // Run all queries in parallel
738
805
  const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult, foreignKeyResult] = await Promise.all([
739
- // カラム情報を取得
806
+ // Get column info
740
807
  client.query(`
741
808
  SELECT
742
809
  c.column_name,
@@ -754,7 +821,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
754
821
  AND c.table_name = $2
755
822
  ORDER BY c.ordinal_position
756
823
  `, [schemaName, tableName]),
757
- // 主キー情報を取得
824
+ // Get primary key info
758
825
  client.query(`
759
826
  SELECT column_name
760
827
  FROM information_schema.table_constraints tc
@@ -765,14 +832,14 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
765
832
  AND tc.constraint_type = 'PRIMARY KEY'
766
833
  ORDER BY kcu.ordinal_position
767
834
  `, [schemaName, tableName]),
768
- // テーブルコメントを取得
835
+ // Get table comment
769
836
  client.query(`
770
837
  SELECT obj_description(c.oid) as table_comment
771
838
  FROM pg_class c
772
839
  JOIN pg_namespace n ON c.relnamespace = n.oid
773
840
  WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
774
841
  `, [tableName, schemaName]),
775
- // カラムコメントを取得
842
+ // Get column comments
776
843
  client.query(`
777
844
  SELECT
778
845
  c.column_name,
@@ -786,7 +853,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
786
853
  AND pgn.nspname = $1
787
854
  ORDER BY c.ordinal_position
788
855
  `, [schemaName, tableName]),
789
- // UNIQUE制約を取得
856
+ // Get UNIQUE constraints
790
857
  client.query(`
791
858
  SELECT
792
859
  tc.constraint_name,
@@ -800,7 +867,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
800
867
  GROUP BY tc.constraint_name
801
868
  ORDER BY tc.constraint_name
802
869
  `, [schemaName, tableName]),
803
- // FOREIGN KEY制約を取得
870
+ // Get FOREIGN KEY constraints
804
871
  client.query(`
805
872
  SELECT
806
873
  tc.constraint_name,
@@ -826,51 +893,51 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
826
893
  columnComments.set(row.column_name, row.column_comment);
827
894
  }
828
895
  });
829
- // テーブルコメントを先頭に追加(スキーマ名を含む)
896
+ // Add table comment at top (with schema name)
830
897
  let ddl = '';
831
- // テーブルコメントを先頭に追加
898
+ // Add table comment at top
832
899
  if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
833
900
  ddl += `-- ${tableCommentResult.rows[0].table_comment}\n`;
834
901
  }
835
902
  else {
836
- ddl += `-- Table: ${tableName}\n`;
903
+ ddl += `-- Table: ${schemaName}.${tableName}\n`;
837
904
  }
838
- // CREATE TABLE文を生成
839
- ddl += `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
905
+ // Generate CREATE TABLE (schema-qualified)
906
+ ddl += `CREATE TABLE IF NOT EXISTS ${schemaName}.${tableName} (\n`;
840
907
  const columnDefs = [];
841
908
  for (const col of columnsResult.rows) {
842
909
  const rawType = col.full_type ||
843
910
  ((col.data_type === 'USER-DEFINED' && col.udt_name) ? col.udt_name : col.data_type);
844
911
  let colDef = ` ${col.column_name} ${rawType}`;
845
- // 長さ指定
912
+ // Length spec
846
913
  if (col.character_maximum_length) {
847
914
  colDef += `(${col.character_maximum_length})`;
848
915
  }
849
- // NOT NULL制約
916
+ // NOT NULL constraint
850
917
  if (col.is_nullable === 'NO') {
851
918
  colDef += ' NOT NULL';
852
919
  }
853
- // デフォルト値
920
+ // Default value
854
921
  if (col.column_default) {
855
922
  colDef += ` DEFAULT ${col.column_default}`;
856
923
  }
857
924
  columnDefs.push(colDef);
858
925
  }
859
926
  ddl += columnDefs.join(',\n');
860
- // 主キー制約
927
+ // Primary key constraint
861
928
  if (primaryKeyResult.rows.length > 0) {
862
929
  const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
863
930
  ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
864
931
  }
865
- // UNIQUE制約をCREATE TABLE内に追加
932
+ // Add UNIQUE constraints inside CREATE TABLE
866
933
  for (const unique of uniqueConstraintResult.rows) {
867
934
  ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
868
935
  }
869
- // FOREIGN KEY制約をCREATE TABLE内に追加
936
+ // Add FOREIGN KEY constraints inside CREATE TABLE
870
937
  for (const fk of foreignKeyResult.rows) {
871
938
  ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
872
939
  }
873
- // CHECK制約をCREATE TABLE内に追加(必ず最後)
940
+ // Add CHECK constraints inside CREATE TABLE (must be last)
874
941
  const checkConstraintResult = await client.query(`
875
942
  SELECT
876
943
  con.conname as constraint_name,
@@ -887,16 +954,16 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
887
954
  ddl += `,\n CONSTRAINT ${check.constraint_name} ${check.check_clause}`;
888
955
  }
889
956
  ddl += '\n);\n\n';
890
- // COMMENT ON文を追加
957
+ // Add COMMENT ON statements
891
958
  if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
892
959
  ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
893
960
  }
894
961
  else {
895
962
  ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
896
963
  }
897
- // カラムコメントを追加(スキーマ名を含む)
964
+ // Add column comments (with schema name)
898
965
  if (columnComments.size > 0) {
899
- ddl += '\n-- カラムコメント\n';
966
+ ddl += '\n-- Column comments\n';
900
967
  for (const [columnName, comment] of columnComments) {
901
968
  ddl += `COMMENT ON COLUMN ${schemaName}.${tableName}.${columnName} IS '${comment}';\n`;
902
969
  }
@@ -904,271 +971,345 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
904
971
  return ddl;
905
972
  }
906
973
  /**
907
- * 定義をファイルに保存
974
+ * Save definitions to files (merge RLS/triggers into table/view; schema folders when multi-schema)
908
975
  */
909
- async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true) {
976
+ async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version) {
910
977
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
911
978
  const path = await Promise.resolve().then(() => __importStar(require('path')));
912
- // 出力ディレクトリを作成
979
+ const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
980
+ const npmUrl = 'https://www.npmjs.com/package/supatool';
981
+ const headerComment = version
982
+ ? `-- Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n`
983
+ : `-- Generated by supatool, ${outputDate} | ${npmUrl}\n`;
984
+ const tables = definitions.filter(d => d.type === 'table');
985
+ const views = definitions.filter(d => d.type === 'view');
986
+ const rlsList = definitions.filter(d => d.type === 'rls');
987
+ const triggersList = definitions.filter(d => d.type === 'trigger');
988
+ const functions = definitions.filter(d => d.type === 'function');
989
+ const cronJobs = definitions.filter(d => d.type === 'cron');
990
+ const customTypes = definitions.filter(d => d.type === 'type');
991
+ // schema.table -> RLS DDL
992
+ const rlsByCategory = new Map();
993
+ for (const r of rlsList) {
994
+ if (r.category)
995
+ rlsByCategory.set(r.category, r.ddl);
996
+ }
997
+ // schema.table -> trigger DDL array
998
+ const triggersByCategory = new Map();
999
+ for (const t of triggersList) {
1000
+ if (!t.category)
1001
+ continue;
1002
+ const list = triggersByCategory.get(t.category) ?? [];
1003
+ list.push(t.ddl);
1004
+ triggersByCategory.set(t.category, list);
1005
+ }
1006
+ // Build merged DDL (table/view + RLS + triggers)
1007
+ const mergeRlsAndTriggers = (def) => {
1008
+ const cat = def.schema && def.name ? `${def.schema}.${def.name}` : def.category ?? '';
1009
+ let ddl = def.ddl.trimEnd();
1010
+ const rlsDdl = rlsByCategory.get(cat);
1011
+ if (rlsDdl) {
1012
+ ddl += '\n\n' + rlsDdl.trim();
1013
+ }
1014
+ const trgList = triggersByCategory.get(cat);
1015
+ if (trgList && trgList.length > 0) {
1016
+ ddl += '\n\n' + trgList.map(t => t.trim()).join('\n\n');
1017
+ }
1018
+ return ddl.endsWith('\n') ? ddl : ddl + '\n';
1019
+ };
1020
+ const mergedTables = tables.map(t => ({
1021
+ ...t,
1022
+ ddl: mergeRlsAndTriggers(t)
1023
+ }));
1024
+ const mergedViews = views.map(v => ({
1025
+ ...v,
1026
+ ddl: mergeRlsAndTriggers(v)
1027
+ }));
1028
+ const toWrite = [
1029
+ ...mergedTables,
1030
+ ...mergedViews,
1031
+ ...functions,
1032
+ ...cronJobs,
1033
+ ...customTypes
1034
+ ];
1035
+ const multiSchema = schemas.length > 1;
1036
+ const typeDirNames = {
1037
+ table: 'tables',
1038
+ view: 'views',
1039
+ function: 'rpc',
1040
+ cron: 'cron',
1041
+ type: 'types'
1042
+ };
913
1043
  if (!fs.existsSync(outputDir)) {
914
1044
  fs.mkdirSync(outputDir, { recursive: true });
915
1045
  }
916
- // 各タイプのディレクトリマッピング
917
- const typeDirectories = separateDirectories ? {
918
- table: path.join(outputDir, 'tables'), // テーブルもtables/フォルダに
919
- view: path.join(outputDir, 'views'),
920
- rls: path.join(outputDir, 'rls'),
921
- function: path.join(outputDir, 'rpc'),
922
- trigger: path.join(outputDir, 'rpc'), // トリガーもrpcディレクトリに
923
- cron: path.join(outputDir, 'cron'),
924
- type: path.join(outputDir, 'types')
925
- } : {
926
- // --no-separate の場合は全てルートに
927
- table: outputDir,
928
- view: outputDir,
929
- rls: outputDir,
930
- function: outputDir,
931
- trigger: outputDir,
932
- cron: outputDir,
933
- type: outputDir
934
- };
935
- // 必要なディレクトリを事前作成
936
- const requiredDirs = new Set(Object.values(typeDirectories));
937
- for (const dir of requiredDirs) {
938
- if (!fs.existsSync(dir)) {
939
- fs.mkdirSync(dir, { recursive: true });
1046
+ const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
1047
+ for (const def of toWrite) {
1048
+ const typeDir = typeDirNames[def.type];
1049
+ const baseTypeDir = separateDirectories ? typeDir : '.';
1050
+ const targetDir = multiSchema && def.schema
1051
+ ? path.join(outputDir, def.schema, baseTypeDir)
1052
+ : path.join(outputDir, baseTypeDir);
1053
+ if (!fs.existsSync(targetDir)) {
1054
+ fs.mkdirSync(targetDir, { recursive: true });
940
1055
  }
941
- }
942
- // 並行ファイル書き込み
943
- const writePromises = definitions.map(async (def) => {
944
- const targetDir = typeDirectories[def.type];
945
- // ファイル名を決定(TypeとTriggerを区別しやすくする)
946
1056
  let fileName;
947
1057
  if (def.type === 'function') {
948
1058
  fileName = `fn_${def.name}.sql`;
949
1059
  }
950
- else if (def.type === 'trigger') {
951
- fileName = `trg_${def.name}.sql`;
952
- }
953
1060
  else {
954
1061
  fileName = `${def.name}.sql`;
955
1062
  }
956
1063
  const filePath = path.join(targetDir, fileName);
957
- // 最後に改行を追加
958
1064
  const ddlWithNewline = def.ddl.endsWith('\n') ? def.ddl : def.ddl + '\n';
959
- // 非同期でファイル書き込み
960
- const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
961
- await fsPromises.writeFile(filePath, ddlWithNewline);
962
- });
963
- // 全ファイル書き込みの完了を待機
964
- await Promise.all(writePromises);
965
- // インデックスファイルを生成
966
- await generateIndexFile(definitions, outputDir, separateDirectories);
1065
+ await fsPromises.writeFile(filePath, headerComment + ddlWithNewline);
1066
+ }
1067
+ await generateIndexFile(toWrite, outputDir, separateDirectories, multiSchema, relations, rpcTables, allSchemas, schemas, version);
967
1068
  }
968
1069
  /**
969
- * データベースオブジェクトのインデックスファイルを生成
970
- * AIが構造を理解しやすいように1行ずつリストアップ
1070
+ * Generate index file for DB objects (RLS/triggers already merged into table/view)
971
1071
  */
972
- async function generateIndexFile(definitions, outputDir, separateDirectories = true) {
1072
+ async function generateIndexFile(definitions, outputDir, separateDirectories = true, multiSchema = false, relations = [], rpcTables = [], allSchemas = [], extractedSchemas = [], version) {
973
1073
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
974
1074
  const path = await Promise.resolve().then(() => __importStar(require('path')));
975
- // タイプ別にグループ化
976
- const groupedDefs = {
977
- table: definitions.filter(def => def.type === 'table'),
978
- view: definitions.filter(def => def.type === 'view'),
979
- rls: definitions.filter(def => def.type === 'rls'),
980
- function: definitions.filter(def => def.type === 'function'),
981
- trigger: definitions.filter(def => def.type === 'trigger'),
982
- cron: definitions.filter(def => def.type === 'cron'),
983
- type: definitions.filter(def => def.type === 'type')
1075
+ const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
1076
+ const npmUrl = 'https://www.npmjs.com/package/supatool';
1077
+ const headerLine = version
1078
+ ? `Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n\n`
1079
+ : `Generated by supatool, ${outputDate} | ${npmUrl}\n\n`;
1080
+ const readmeHeader = version
1081
+ ? `Generated by [supatool](${npmUrl}) v${version}, ${outputDate}\n\n`
1082
+ : `Generated by [supatool](${npmUrl}), ${outputDate}\n\n`;
1083
+ const typeDirNames = {
1084
+ table: 'tables',
1085
+ view: 'views',
1086
+ function: 'rpc',
1087
+ cron: 'cron',
1088
+ type: 'types'
984
1089
  };
985
1090
  const typeLabels = {
986
1091
  table: 'Tables',
987
1092
  view: 'Views',
988
- rls: 'RLS Policies',
989
1093
  function: 'Functions',
990
- trigger: 'Triggers',
991
1094
  cron: 'Cron Jobs',
992
1095
  type: 'Custom Types'
993
1096
  };
994
- // === 人間向け index.md ===
995
- let indexContent = '# Database Schema Index\n\n';
996
- // 統計サマリー
997
- indexContent += '## Summary\n\n';
998
- Object.entries(groupedDefs).forEach(([type, defs]) => {
999
- if (defs.length > 0) {
1000
- indexContent += `- ${typeLabels[type]}: ${defs.length} objects\n`;
1097
+ const groupedDefs = {
1098
+ table: definitions.filter(def => def.type === 'table'),
1099
+ view: definitions.filter(def => def.type === 'view'),
1100
+ function: definitions.filter(def => def.type === 'function'),
1101
+ cron: definitions.filter(def => def.type === 'cron'),
1102
+ type: definitions.filter(def => def.type === 'type')
1103
+ };
1104
+ // Build relative path per file (schema/type/file when multiSchema)
1105
+ const getRelPath = (def) => {
1106
+ const typeDir = separateDirectories ? (typeDirNames[def.type] ?? def.type) : '.';
1107
+ const fileName = def.type === 'function' ? `fn_${def.name}.sql` : `${def.name}.sql`;
1108
+ if (multiSchema && def.schema) {
1109
+ return `${def.schema}/${typeDir}/${fileName}`;
1001
1110
  }
1002
- });
1003
- indexContent += '\n';
1004
- // ファイル一覧(md形式)
1005
- Object.entries(groupedDefs).forEach(([type, defs]) => {
1006
- if (defs.length === 0)
1007
- return;
1008
- const label = typeLabels[type];
1009
- indexContent += `## ${label}\n\n`;
1010
- defs.forEach(def => {
1011
- const folderPath = separateDirectories
1012
- ? (type === 'trigger' ? 'rpc' : type === 'table' ? 'tables' : type)
1013
- : '.';
1014
- // ファイル名を決定(Functions/Triggersに接頭辞を付ける)
1015
- let fileName;
1016
- if (def.type === 'function') {
1017
- fileName = `fn_${def.name}.sql`;
1018
- }
1019
- else if (def.type === 'trigger') {
1020
- fileName = `trg_${def.name}.sql`;
1021
- }
1022
- else {
1023
- fileName = `${def.name}.sql`;
1024
- }
1025
- const filePath = separateDirectories ? `${folderPath}/${fileName}` : fileName;
1026
- const commentText = def.comment ? ` - ${def.comment}` : '';
1027
- indexContent += `- [${def.name}](${filePath})${commentText}\n`;
1028
- });
1029
- indexContent += '\n';
1030
- });
1031
- // ディレクトリ構造(フォルダのみ)
1032
- indexContent += '## Directory Structure\n\n';
1033
- indexContent += '```\n';
1034
- indexContent += 'schemas/\n';
1035
- indexContent += '├── index.md\n';
1036
- indexContent += '├── llms.txt\n';
1037
- if (separateDirectories) {
1038
- Object.entries(groupedDefs).forEach(([type, defs]) => {
1039
- if (defs.length === 0)
1040
- return;
1041
- const folderName = type === 'trigger' ? 'rpc' : type === 'table' ? 'tables' : type;
1042
- indexContent += `└── ${folderName}/\n`;
1043
- });
1111
+ return separateDirectories ? `${typeDir}/${fileName}` : fileName;
1112
+ };
1113
+ // === Human-readable README.md (description + link to llms.txt) ===
1114
+ let readmeContent = readmeHeader;
1115
+ readmeContent += '# Schema (extract output)\n\n';
1116
+ readmeContent += 'This folder contains DDL exported by `supatool extract`.\n\n';
1117
+ readmeContent += '- **tables/** – Table definitions (with RLS and triggers in the same file)\n';
1118
+ readmeContent += '- **views/** – View definitions\n';
1119
+ readmeContent += '- **rpc/** – Functions\n';
1120
+ readmeContent += '- **cron/** – Cron jobs\n';
1121
+ readmeContent += '- **types/** Custom types\n\n';
1122
+ if (multiSchema) {
1123
+ readmeContent += 'When multiple schemas are extracted, each schema has its own subfolder (e.g. `public/tables/`, `agent/views/`).\n\n';
1044
1124
  }
1045
- indexContent += '```\n';
1046
- // === AI向け llms.txt ===
1047
- let llmsContent = 'Database Schema - Complete Objects Catalog\n\n';
1048
- // Summary section
1125
+ readmeContent += 'Full catalog and relations: [llms.txt](llms.txt)\n';
1126
+ // === llms.txt ===
1127
+ let llmsContent = headerLine;
1128
+ llmsContent += 'Database Schema - Complete Objects Catalog\n';
1129
+ llmsContent += '(Tables/Views include RLS and Triggers in the same file)\n\n';
1049
1130
  llmsContent += 'SUMMARY\n';
1050
1131
  Object.entries(groupedDefs).forEach(([type, defs]) => {
1051
- if (defs.length > 0) {
1132
+ if (defs.length > 0 && typeLabels[type]) {
1052
1133
  llmsContent += `${typeLabels[type]}: ${defs.length}\n`;
1053
1134
  }
1054
1135
  });
1055
- llmsContent += '\n';
1056
- // Flat list for AI processing (single format)
1057
- llmsContent += 'OBJECTS\n';
1136
+ llmsContent += '\nOBJECTS\n';
1058
1137
  definitions.forEach(def => {
1059
- const folderPath = separateDirectories
1060
- ? (def.type === 'trigger' ? 'rpc' : def.type === 'table' ? 'tables' : def.type)
1061
- : '.';
1062
- // ファイル名を決定(Functions/Triggersに接頭辞を付ける)
1063
- let fileName;
1064
- if (def.type === 'function') {
1065
- fileName = `fn_${def.name}.sql`;
1066
- }
1067
- else if (def.type === 'trigger') {
1068
- fileName = `trg_${def.name}.sql`;
1069
- }
1070
- else {
1071
- fileName = `${def.name}.sql`;
1072
- }
1073
- const filePath = separateDirectories ? `${folderPath}/${fileName}` : fileName;
1074
- const commentText = def.comment ? `:${def.comment}` : '';
1075
- llmsContent += `${def.type}:${def.name}:${filePath}${commentText}\n`;
1138
+ const filePath = getRelPath(def);
1139
+ const commentSuffix = def.comment ? ` # ${def.comment}` : '';
1140
+ const displayName = def.schema ? `${def.schema}.${def.name}` : def.name;
1141
+ llmsContent += `${def.type}:${displayName}:${filePath}${commentSuffix}\n`;
1076
1142
  });
1077
- // ファイル保存
1078
- const indexPath = path.join(outputDir, 'index.md');
1143
+ if (relations.length > 0) {
1144
+ llmsContent += '\nRELATIONS\n';
1145
+ relations.forEach(r => {
1146
+ llmsContent += `${r.from} -> ${r.to}\n`;
1147
+ });
1148
+ }
1149
+ if (rpcTables.length > 0) {
1150
+ llmsContent += '\nRPC_TABLES\n';
1151
+ rpcTables.forEach(rt => {
1152
+ llmsContent += `${rt.rpc}: ${rt.tables.join(', ')}\n`;
1153
+ });
1154
+ }
1155
+ if (allSchemas.length > 0) {
1156
+ const extractedSet = new Set(extractedSchemas);
1157
+ const extractedList = allSchemas.filter(s => extractedSet.has(s));
1158
+ const notExtractedList = allSchemas.filter(s => !extractedSet.has(s));
1159
+ llmsContent += '\nALL_SCHEMAS\n';
1160
+ llmsContent += 'EXTRACTED\n';
1161
+ extractedList.forEach(schemaName => {
1162
+ llmsContent += `${schemaName}\n`;
1163
+ });
1164
+ llmsContent += '\nNOT_EXTRACTED\n';
1165
+ notExtractedList.forEach(schemaName => {
1166
+ llmsContent += `${schemaName}\n`;
1167
+ });
1168
+ }
1169
+ const readmePath = path.join(outputDir, 'README.md');
1079
1170
  const llmsPath = path.join(outputDir, 'llms.txt');
1080
- fs.writeFileSync(indexPath, indexContent);
1171
+ fs.writeFileSync(readmePath, readmeContent);
1081
1172
  fs.writeFileSync(llmsPath, llmsContent);
1173
+ // schema_index.json (same data for agents that parse JSON)
1174
+ const schemaIndex = {
1175
+ objects: definitions.map(def => ({
1176
+ type: def.type,
1177
+ name: def.schema ? `${def.schema}.${def.name}` : def.name,
1178
+ path: getRelPath(def),
1179
+ ...(def.comment && { comment: def.comment })
1180
+ })),
1181
+ relations: relations.map(r => ({ from: r.from, to: r.to })),
1182
+ rpc_tables: rpcTables.map(rt => ({ rpc: rt.rpc, tables: rt.tables })),
1183
+ all_schemas: allSchemas.length > 0
1184
+ ? {
1185
+ extracted: allSchemas.filter(s => extractedSchemas.includes(s)),
1186
+ not_extracted: allSchemas.filter(s => !extractedSchemas.includes(s))
1187
+ }
1188
+ : undefined
1189
+ };
1190
+ fs.writeFileSync(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2), 'utf8');
1191
+ // schema_summary.md (one-file overview for AI)
1192
+ let summaryMd = '# Schema summary\n\n';
1193
+ const tableDefs = definitions.filter(d => d.type === 'table' || d.type === 'view');
1194
+ if (tableDefs.length > 0) {
1195
+ summaryMd += '## Tables / Views\n';
1196
+ tableDefs.forEach(d => {
1197
+ const name = d.schema ? `${d.schema}.${d.name}` : d.name;
1198
+ summaryMd += d.comment ? `- ${name} (# ${d.comment})\n` : `- ${name}\n`;
1199
+ });
1200
+ summaryMd += '\n';
1201
+ }
1202
+ if (relations.length > 0) {
1203
+ summaryMd += '## Relations\n';
1204
+ relations.forEach(r => {
1205
+ summaryMd += `- ${r.from} -> ${r.to}\n`;
1206
+ });
1207
+ summaryMd += '\n';
1208
+ }
1209
+ if (rpcTables.length > 0) {
1210
+ summaryMd += '## RPC → Tables\n';
1211
+ rpcTables.forEach(rt => {
1212
+ summaryMd += `- ${rt.rpc}: ${rt.tables.join(', ')}\n`;
1213
+ });
1214
+ summaryMd += '\n';
1215
+ }
1216
+ if (allSchemas.length > 0) {
1217
+ const extractedSet = new Set(extractedSchemas);
1218
+ summaryMd += '## Schemas\n';
1219
+ summaryMd += `- Extracted: ${allSchemas.filter(s => extractedSet.has(s)).join(', ') || '(none)'}\n`;
1220
+ summaryMd += `- Not extracted: ${allSchemas.filter(s => !extractedSet.has(s)).join(', ') || '(none)'}\n`;
1221
+ }
1222
+ fs.writeFileSync(path.join(outputDir, 'schema_summary.md'), summaryMd, 'utf8');
1082
1223
  }
1083
1224
  /**
1084
- * 定義を分類して出力
1225
+ * Classify and output definitions
1085
1226
  */
1086
1227
  async function extractDefinitions(options) {
1087
- const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'] } = options;
1088
- // Node.jsSSL証明書検証を無効化
1228
+ const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'], version } = options;
1229
+ // Disable Node.js SSL certificate verification
1089
1230
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
1090
- // 接続文字列の検証
1231
+ // Connection string validation
1091
1232
  if (!connectionString) {
1092
- throw new Error('接続文字列が設定されていません。以下のいずれかで設定してください:\n' +
1093
- '1. --connection オプション\n' +
1094
- '2. SUPABASE_CONNECTION_STRING 環境変数\n' +
1095
- '3. DATABASE_URL 環境変数\n' +
1096
- '4. supatool.config.json 設定ファイル');
1233
+ throw new Error('Connection string is not configured. Please set it using one of:\n' +
1234
+ '1. --connection option\n' +
1235
+ '2. SUPABASE_CONNECTION_STRING environment variable\n' +
1236
+ '3. DATABASE_URL environment variable\n' +
1237
+ '4. supatool.config.json configuration file');
1097
1238
  }
1098
- // 接続文字列の形式検証
1239
+ // Connection string format validation
1099
1240
  if (!connectionString.startsWith('postgresql://') && !connectionString.startsWith('postgres://')) {
1100
- throw new Error(`不正な接続文字列形式です: ${connectionString}\n` +
1101
- '正しい形式: postgresql://username:password@host:port/database');
1241
+ throw new Error(`Invalid connection string format: ${connectionString}\n` +
1242
+ 'Correct format: postgresql://username:password@host:port/database');
1102
1243
  }
1103
- // パスワード部分をURLエンコード
1244
+ // URL encode password part
1104
1245
  let encodedConnectionString = connectionString;
1105
- console.log('🔍 元の接続文字列:', connectionString);
1246
+ console.log('🔍 Original connection string:', connectionString);
1106
1247
  try {
1107
- // パスワードに@が含まれる場合の特別処理
1248
+ // Special handling when password contains @
1108
1249
  if (connectionString.includes('@') && connectionString.split('@').length > 2) {
1109
- console.log('⚠️ パスワードに@が含まれているため特別処理を実行');
1110
- // 最後の@を区切り文字として使用
1250
+ console.log('⚠️ Password contains @, executing special handling');
1251
+ // Use last @ as delimiter
1111
1252
  const parts = connectionString.split('@');
1112
- const lastPart = parts.pop(); // 最後の部分(host:port/database
1113
- const firstParts = parts.join('@'); // 最初の部分(postgresql://user:password
1114
- console.log(' 分割結果:');
1115
- console.log(' 前半部分:', firstParts);
1116
- console.log(' 後半部分:', lastPart);
1117
- // パスワード部分をエンコード
1253
+ const lastPart = parts.pop(); // Last part (host:port/database)
1254
+ const firstParts = parts.join('@'); // First part (postgresql://user:password)
1255
+ console.log(' Split result:');
1256
+ console.log(' First part:', firstParts);
1257
+ console.log(' Last part:', lastPart);
1258
+ // Encode password part
1118
1259
  const colonIndex = firstParts.lastIndexOf(':');
1119
1260
  if (colonIndex > 0) {
1120
1261
  const protocolAndUser = firstParts.substring(0, colonIndex);
1121
1262
  const password = firstParts.substring(colonIndex + 1);
1122
1263
  const encodedPassword = encodeURIComponent(password);
1123
1264
  encodedConnectionString = `${protocolAndUser}:${encodedPassword}@${lastPart}`;
1124
- console.log(' エンコード結果:');
1125
- console.log(' プロトコル+ユーザー:', protocolAndUser);
1126
- console.log(' 元パスワード:', password);
1127
- console.log(' エンコードパスワード:', encodedPassword);
1128
- console.log(' 最終接続文字列:', encodedConnectionString);
1265
+ console.log(' Encode result:');
1266
+ console.log(' Protocol+User:', protocolAndUser);
1267
+ console.log(' Original password:', password);
1268
+ console.log(' Encoded password:', encodedPassword);
1269
+ console.log(' Final connection string:', encodedConnectionString);
1129
1270
  }
1130
1271
  }
1131
1272
  else {
1132
- console.log('✅ 通常のURL解析を実行');
1133
- // 通常のURL解析
1273
+ console.log('✅ Executing normal URL parsing');
1274
+ // Normal URL parsing
1134
1275
  const url = new URL(connectionString);
1135
- // ユーザー名にドットが含まれる場合の処理
1276
+ // Handle username containing dots
1136
1277
  if (url.username && url.username.includes('.')) {
1137
- console.log(`ユーザー名(ドット含む): ${url.username}`);
1278
+ console.log(`Username (with dots): ${url.username}`);
1138
1279
  }
1139
1280
  if (url.password) {
1140
- // パスワード部分のみをエンコード
1281
+ // Encode only password part
1141
1282
  const encodedPassword = encodeURIComponent(url.password);
1142
1283
  url.password = encodedPassword;
1143
1284
  encodedConnectionString = url.toString();
1144
- console.log(' パスワードエンコード:', encodedPassword);
1285
+ console.log(' Password encoded:', encodedPassword);
1145
1286
  }
1146
1287
  }
1147
- // Supabase接続用にSSL設定を追加
1288
+ // Add SSL settings for Supabase connection
1148
1289
  if (!encodedConnectionString.includes('sslmode=')) {
1149
1290
  const separator = encodedConnectionString.includes('?') ? '&' : '?';
1150
1291
  encodedConnectionString += `${separator}sslmode=require`;
1151
- console.log(' SSL設定を追加:', encodedConnectionString);
1292
+ console.log(' SSL setting added:', encodedConnectionString);
1152
1293
  }
1153
- // デバッグ情報を表示(パスワードは隠す)
1294
+ // Display debug info (password hidden)
1154
1295
  const debugUrl = new URL(encodedConnectionString);
1155
1296
  const maskedPassword = debugUrl.password ? '*'.repeat(debugUrl.password.length) : '';
1156
1297
  debugUrl.password = maskedPassword;
1157
- console.log('🔍 接続情報:');
1158
- console.log(` ホスト: ${debugUrl.hostname}`);
1159
- console.log(` ポート: ${debugUrl.port}`);
1160
- console.log(` データベース: ${debugUrl.pathname.slice(1)}`);
1161
- console.log(` ユーザー: ${debugUrl.username}`);
1298
+ console.log('🔍 Connection info:');
1299
+ console.log(` Host: ${debugUrl.hostname}`);
1300
+ console.log(` Port: ${debugUrl.port}`);
1301
+ console.log(` Database: ${debugUrl.pathname.slice(1)}`);
1302
+ console.log(` User: ${debugUrl.username}`);
1162
1303
  console.log(` SSL: ${debugUrl.searchParams.get('sslmode') || 'require'}`);
1163
1304
  }
1164
1305
  catch (error) {
1165
- // URL解析に失敗した場合は元の文字列を使用
1166
- console.warn('接続文字列のURL解析に失敗しました。特殊文字が含まれている可能性があります。');
1167
- console.warn('エラー詳細:', error instanceof Error ? error.message : String(error));
1306
+ // Use original string if URL parsing fails
1307
+ console.warn('Failed to parse connection string URL. May contain special characters.');
1308
+ console.warn('Error details:', error instanceof Error ? error.message : String(error));
1168
1309
  }
1169
1310
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1170
1311
  const readline = await Promise.resolve().then(() => __importStar(require('readline')));
1171
- // 上書き確認
1312
+ // Overwrite confirmation
1172
1313
  if (!force && fs.existsSync(outputDir)) {
1173
1314
  const files = fs.readdirSync(outputDir);
1174
1315
  if (files.length > 0) {
@@ -1186,7 +1327,7 @@ async function extractDefinitions(options) {
1186
1327
  }
1187
1328
  }
1188
1329
  }
1189
- // スピナーを動的インポート
1330
+ // Dynamic import for spinner
1190
1331
  const { default: ora } = await Promise.resolve().then(() => __importStar(require('ora')));
1191
1332
  const spinner = ora('Connecting to database...').start();
1192
1333
  const client = new pg_1.Client({
@@ -1197,14 +1338,14 @@ async function extractDefinitions(options) {
1197
1338
  }
1198
1339
  });
1199
1340
  try {
1200
- // 接続前のデバッグ情報
1201
- console.log('🔧 接続設定:');
1341
+ // Debug before connect
1342
+ console.log('🔧 Connection settings:');
1202
1343
  console.log(` SSL: rejectUnauthorized=false`);
1203
- console.log(` 接続文字列長: ${encodedConnectionString.length}`);
1344
+ console.log(` Connection string length: ${encodedConnectionString.length}`);
1204
1345
  await client.connect();
1205
1346
  spinner.text = 'Connected to database';
1206
1347
  let allDefinitions = [];
1207
- // 進行状況トラッカーを初期化
1348
+ // Initialize progress tracker
1208
1349
  const progress = {
1209
1350
  tables: { current: 0, total: 0 },
1210
1351
  views: { current: 0, total: 0 },
@@ -1215,14 +1356,14 @@ async function extractDefinitions(options) {
1215
1356
  customTypes: { current: 0, total: 0 }
1216
1357
  };
1217
1358
  if (all) {
1218
- // 事前に各オブジェクトの総数を取得
1359
+ // Get total count for each object type first
1219
1360
  spinner.text = 'Counting database objects...';
1220
- // テーブル・ビューの総数を取得
1361
+ // Get total tables/views count
1221
1362
  const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
1222
1363
  const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
1223
1364
  progress.tables.total = parseInt(tablesCountResult.rows[0].count);
1224
1365
  progress.views.total = parseInt(viewsCountResult.rows[0].count);
1225
- // RLS ポリシーの総数を取得(テーブル単位)
1366
+ // Get total RLS policy count (per table)
1226
1367
  try {
1227
1368
  const rlsCountResult = await client.query(`
1228
1369
  SELECT COUNT(DISTINCT tablename) as count
@@ -1234,7 +1375,7 @@ async function extractDefinitions(options) {
1234
1375
  catch (error) {
1235
1376
  progress.rls.total = 0;
1236
1377
  }
1237
- // 関数の総数を取得
1378
+ // Get total functions count
1238
1379
  const functionsCountResult = await client.query(`
1239
1380
  SELECT COUNT(*) as count
1240
1381
  FROM pg_proc p
@@ -1242,7 +1383,7 @@ async function extractDefinitions(options) {
1242
1383
  WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p')
1243
1384
  `);
1244
1385
  progress.functions.total = parseInt(functionsCountResult.rows[0].count);
1245
- // トリガーの総数を取得
1386
+ // Get total triggers count
1246
1387
  const triggersCountResult = await client.query(`
1247
1388
  SELECT COUNT(*) as count
1248
1389
  FROM pg_trigger t
@@ -1251,7 +1392,7 @@ async function extractDefinitions(options) {
1251
1392
  WHERE n.nspname = 'public' AND NOT t.tgisinternal
1252
1393
  `);
1253
1394
  progress.triggers.total = parseInt(triggersCountResult.rows[0].count);
1254
- // Cronジョブの総数を取得
1395
+ // Get total Cron jobs count
1255
1396
  try {
1256
1397
  const cronCountResult = await client.query('SELECT COUNT(*) as count FROM cron.job');
1257
1398
  progress.cronJobs.total = parseInt(cronCountResult.rows[0].count);
@@ -1259,7 +1400,7 @@ async function extractDefinitions(options) {
1259
1400
  catch (error) {
1260
1401
  progress.cronJobs.total = 0;
1261
1402
  }
1262
- // カスタム型の総数を取得
1403
+ // Get total custom types count
1263
1404
  const typesCountResult = await client.query(`
1264
1405
  SELECT COUNT(*) as count
1265
1406
  FROM pg_type t
@@ -1276,7 +1417,7 @@ async function extractDefinitions(options) {
1276
1417
  AND t.typname NOT LIKE '_%'
1277
1418
  `);
1278
1419
  progress.customTypes.total = parseInt(typesCountResult.rows[0].count);
1279
- // --all フラグが指定された場合は全てのオブジェクトを取得(順次処理)
1420
+ // When --all: fetch all objects (sequential)
1280
1421
  const tables = await fetchTableDefinitions(client, spinner, progress, schemas);
1281
1422
  const rlsPolicies = await fetchRlsPolicies(client, spinner, progress, schemas);
1282
1423
  const functions = await fetchFunctions(client, spinner, progress, schemas);
@@ -1293,8 +1434,8 @@ async function extractDefinitions(options) {
1293
1434
  ];
1294
1435
  }
1295
1436
  else {
1296
- // 従来の処理(テーブル・ビューのみ)
1297
- // テーブル・ビューの総数を取得
1437
+ // Legacy path (tables/views only)
1438
+ // Get total tables/views count
1298
1439
  const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
1299
1440
  const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
1300
1441
  progress.tables.total = parseInt(tablesCountResult.rows[0].count);
@@ -1310,15 +1451,42 @@ async function extractDefinitions(options) {
1310
1451
  allDefinitions = definitions;
1311
1452
  }
1312
1453
  }
1313
- // パターンマッチング
1454
+ // Pattern matching
1314
1455
  if (tablePattern !== '*') {
1315
1456
  const regex = new RegExp(tablePattern.replace(/\*/g, '.*'));
1316
1457
  allDefinitions = allDefinitions.filter(def => regex.test(def.name));
1317
1458
  }
1318
- // 定義を保存
1459
+ // Fetch for RELATIONS / RPC_TABLES (llms.txt upgrade)
1460
+ let relations = [];
1461
+ let rpcTables = [];
1462
+ let allSchemas = [];
1463
+ try {
1464
+ allSchemas = await fetchAllSchemas(client);
1465
+ relations = await fetchRelationList(client, schemas);
1466
+ const funcDefs = allDefinitions.filter(d => d.type === 'function');
1467
+ for (const f of funcDefs) {
1468
+ const tables = extractTableRefsFromFunctionDdl(f.ddl, f.schema ?? 'public');
1469
+ if (tables.length > 0) {
1470
+ rpcTables.push({
1471
+ rpc: f.schema ? `${f.schema}.${f.name}` : f.name,
1472
+ tables
1473
+ });
1474
+ }
1475
+ }
1476
+ }
1477
+ catch (err) {
1478
+ if (process.env.SUPATOOL_DEBUG) {
1479
+ console.warn('RELATIONS/RPC_TABLES extraction skipped:', err);
1480
+ }
1481
+ }
1482
+ // When force: remove output dir then write (so removed tables don't leave files)
1483
+ if (force && fs.existsSync(outputDir)) {
1484
+ fs.rmSync(outputDir, { recursive: true });
1485
+ }
1486
+ // Save definitions (table+RLS+triggers merged, schema folders)
1319
1487
  spinner.text = 'Saving definitions to files...';
1320
- await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories);
1321
- // 統計を表示
1488
+ await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version);
1489
+ // Show stats
1322
1490
  const counts = {
1323
1491
  table: allDefinitions.filter(def => def.type === 'table').length,
1324
1492
  view: allDefinitions.filter(def => def.type === 'view').length,
@@ -1328,7 +1496,7 @@ async function extractDefinitions(options) {
1328
1496
  cron: allDefinitions.filter(def => def.type === 'cron').length,
1329
1497
  type: allDefinitions.filter(def => def.type === 'type').length
1330
1498
  };
1331
- // 進捗表示を停止
1499
+ // Stop progress display
1332
1500
  stopProgressDisplay();
1333
1501
  spinner.succeed(`Extraction completed: ${outputDir}`);
1334
1502
  if (counts.table > 0)
@@ -1348,7 +1516,7 @@ async function extractDefinitions(options) {
1348
1516
  console.log('');
1349
1517
  }
1350
1518
  catch (error) {
1351
- // 進捗表示を停止(エラー時)
1519
+ // Stop progress display (on error)
1352
1520
  stopProgressDisplay();
1353
1521
  spinner.fail('Extraction failed');
1354
1522
  console.error('Error:', error);
@@ -1359,7 +1527,7 @@ async function extractDefinitions(options) {
1359
1527
  await client.end();
1360
1528
  }
1361
1529
  catch (closeError) {
1362
- // データベース接続の終了エラーは無視(既に切断されている場合など)
1530
+ // Ignore DB connection close errors (e.g. already disconnected)
1363
1531
  }
1364
1532
  }
1365
1533
  }