supatool 0.3.7 → 0.4.1

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,110 @@ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public'])
192
190
  }
193
191
  }
194
192
  /**
195
- * 関数を取得
193
+ * Fetch RLS enabled flag and policy count for all tables (pg_class.relrowsecurity + pg_policies)
194
+ */
195
+ async function fetchTableRlsStatus(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
+ n.nspname AS schema_name,
202
+ c.relname AS table_name,
203
+ COALESCE(c.relrowsecurity, false) AS rls_enabled
204
+ FROM pg_class c
205
+ JOIN pg_namespace n ON n.oid = c.relnamespace
206
+ WHERE c.relkind = 'r'
207
+ AND n.nspname IN (${schemaPlaceholders})
208
+ ORDER BY n.nspname, c.relname
209
+ `, schemas);
210
+ const policyCountMap = new Map();
211
+ const policyResult = await client.query(`
212
+ SELECT schemaname, tablename, COUNT(*) AS cnt
213
+ FROM pg_policies
214
+ WHERE schemaname IN (${schemaPlaceholders})
215
+ GROUP BY schemaname, tablename
216
+ `, schemas);
217
+ for (const row of policyResult.rows) {
218
+ policyCountMap.set(`${row.schemaname}.${row.tablename}`, parseInt(row.cnt, 10));
219
+ }
220
+ return result.rows.map((r) => {
221
+ const key = `${r.schema_name}.${r.table_name}`;
222
+ return {
223
+ schema: r.schema_name,
224
+ table: r.table_name,
225
+ rlsEnabled: !!r.rls_enabled,
226
+ policyCount: policyCountMap.get(key) ?? 0
227
+ };
228
+ });
229
+ }
230
+ /**
231
+ * Fetch FK relations list (for llms.txt RELATIONS)
232
+ */
233
+ async function fetchRelationList(client, schemas = ['public']) {
234
+ if (schemas.length === 0)
235
+ return [];
236
+ const schemaPlaceholders = schemas.map((_, i) => `$${i + 1}`).join(', ');
237
+ const result = await client.query(`
238
+ SELECT
239
+ tc.table_schema AS from_schema,
240
+ tc.table_name AS from_table,
241
+ ccu.table_schema AS to_schema,
242
+ ccu.table_name AS to_table
243
+ FROM information_schema.table_constraints tc
244
+ JOIN information_schema.constraint_column_usage ccu
245
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
246
+ WHERE tc.constraint_type = 'FOREIGN KEY'
247
+ AND tc.table_schema IN (${schemaPlaceholders})
248
+ ORDER BY tc.table_schema, tc.table_name
249
+ `, schemas);
250
+ return result.rows.map((r) => ({
251
+ from: `${r.from_schema}.${r.from_table}`,
252
+ to: `${r.to_schema}.${r.to_table}`
253
+ }));
254
+ }
255
+ /**
256
+ * Fetch all schemas in DB (for llms.txt ALL_SCHEMAS / unrextracted schemas)
257
+ */
258
+ async function fetchAllSchemas(client) {
259
+ const result = await client.query(`
260
+ SELECT schema_name
261
+ FROM information_schema.schemata
262
+ WHERE schema_name NOT LIKE 'pg_%'
263
+ AND schema_name != 'information_schema'
264
+ AND schema_name != 'pg_catalog'
265
+ ORDER BY schema_name
266
+ `);
267
+ return result.rows.map((r) => r.schema_name);
268
+ }
269
+ /**
270
+ * Extract table refs from function DDL (heuristic, best-effort)
271
+ */
272
+ function extractTableRefsFromFunctionDdl(ddl, defaultSchema = 'public') {
273
+ const seen = new Set();
274
+ const add = (schema, table) => {
275
+ const key = `${schema}.${table}`;
276
+ if (!seen.has(key))
277
+ seen.add(key);
278
+ };
279
+ // schema.table form
280
+ 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;
281
+ let m;
282
+ while ((m = schemaTableRe.exec(ddl)) !== null) {
283
+ add(m[1], m[2]);
284
+ }
285
+ // Unqualified table name (after FROM / JOIN / INSERT INTO / UPDATE / DELETE FROM)
286
+ const unqualifiedRe = /(?:FROM|JOIN|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s|$|\)|,)/gi;
287
+ while ((m = unqualifiedRe.exec(ddl)) !== null) {
288
+ const name = m[1].toLowerCase();
289
+ if (!['select', 'where', 'on', 'and', 'or', 'inner', 'left', 'right', 'outer', 'cross', 'lateral'].includes(name)) {
290
+ add(defaultSchema, m[1]);
291
+ }
292
+ }
293
+ return Array.from(seen).sort();
294
+ }
295
+ /**
296
+ * Fetch functions
196
297
  */
197
298
  async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
198
299
  const functions = [];
@@ -211,20 +312,20 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
211
312
  AND p.prokind IN ('f', 'p') -- functions and procedures
212
313
  ORDER BY n.nspname, p.proname
213
314
  `, schemas);
214
- // 進行状況の初期化
315
+ // Initialize progress
215
316
  if (progress) {
216
317
  progress.functions.total = result.rows.length;
217
318
  }
218
319
  for (let i = 0; i < result.rows.length; i++) {
219
320
  const row = result.rows[i];
220
- // 進行状況を更新
321
+ // Update progress
221
322
  if (progress && spinner) {
222
323
  progress.functions.current = i + 1;
223
324
  displayProgress(progress, spinner);
224
325
  }
225
- // 正確な関数シグネチャを構築(スキーマ名と引数の型を含む)
326
+ // Build exact function signature (schema name and argument types)
226
327
  const functionSignature = `${row.schema_name}.${row.name}(${row.identity_args || ''})`;
227
- // 関数コメントを先頭に追加
328
+ // Add function comment at top
228
329
  let ddl = '';
229
330
  if (!row.comment) {
230
331
  ddl += `-- Function: ${functionSignature}\n`;
@@ -232,13 +333,13 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
232
333
  else {
233
334
  ddl += `-- ${row.comment}\n`;
234
335
  }
235
- // 関数定義を追加(セミコロンを確実に付与)
336
+ // Add function definition (ensure semicolon)
236
337
  let functionDef = row.definition;
237
338
  if (!functionDef.trim().endsWith(';')) {
238
339
  functionDef += ';';
239
340
  }
240
341
  ddl += functionDef + '\n\n';
241
- // COMMENT ON文を追加
342
+ // Add COMMENT ON statement
242
343
  if (!row.comment) {
243
344
  ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
244
345
  }
@@ -248,6 +349,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
248
349
  functions.push({
249
350
  name: row.name,
250
351
  type: 'function',
352
+ schema: row.schema_name,
251
353
  ddl,
252
354
  comment: row.comment,
253
355
  timestamp: Math.floor(Date.now() / 1000)
@@ -256,7 +358,7 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
256
358
  return functions;
257
359
  }
258
360
  /**
259
- * トリガーを取得
361
+ * Fetch triggers
260
362
  */
261
363
  async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
262
364
  const triggers = [];
@@ -274,18 +376,18 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
274
376
  AND NOT t.tgisinternal
275
377
  ORDER BY n.nspname, c.relname, t.tgname
276
378
  `, schemas);
277
- // 進行状況の初期化
379
+ // Initialize progress
278
380
  if (progress) {
279
381
  progress.triggers.total = result.rows.length;
280
382
  }
281
383
  for (let i = 0; i < result.rows.length; i++) {
282
384
  const row = result.rows[i];
283
- // 進行状況を更新
385
+ // Update progress
284
386
  if (progress && spinner) {
285
387
  progress.triggers.current = i + 1;
286
388
  displayProgress(progress, spinner);
287
389
  }
288
- // トリガー説明を先頭に追加
390
+ // Add trigger description at top
289
391
  let ddl = `-- Trigger: ${row.trigger_name} on ${row.schema_name}.${row.table_name}\n`;
290
392
  ddl += `-- Database trigger that automatically executes in response to certain events\n\n`;
291
393
  ddl += row.definition + ';';
@@ -293,6 +395,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
293
395
  name: `${row.schema_name}_${row.table_name}_${row.trigger_name}`,
294
396
  type: 'trigger',
295
397
  category: `${row.schema_name}.${row.table_name}`,
398
+ schema: row.schema_name,
296
399
  ddl,
297
400
  timestamp: Math.floor(Date.now() / 1000)
298
401
  });
@@ -300,7 +403,7 @@ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
300
403
  return triggers;
301
404
  }
302
405
  /**
303
- * Cronジョブを取得(pg_cron拡張)
406
+ * Fetch Cron jobs (pg_cron extension)
304
407
  */
305
408
  async function fetchCronJobs(client, spinner, progress) {
306
409
  const cronJobs = [];
@@ -315,18 +418,18 @@ async function fetchCronJobs(client, spinner, progress) {
315
418
  FROM cron.job
316
419
  ORDER BY jobid
317
420
  `);
318
- // 進行状況の初期化
421
+ // Initialize progress
319
422
  if (progress) {
320
423
  progress.cronJobs.total = result.rows.length;
321
424
  }
322
425
  for (let i = 0; i < result.rows.length; i++) {
323
426
  const row = result.rows[i];
324
- // 進行状況を更新
427
+ // Update progress
325
428
  if (progress && spinner) {
326
429
  progress.cronJobs.current = i + 1;
327
430
  displayProgress(progress, spinner);
328
431
  }
329
- // Cronジョブ説明を先頭に追加
432
+ // Add Cron job description at top
330
433
  let ddl = `-- Cron Job: ${row.jobname || `job_${row.jobid}`}\n`;
331
434
  ddl += `-- Scheduled job that runs automatically at specified intervals\n`;
332
435
  ddl += `-- Schedule: ${row.schedule}\n`;
@@ -341,12 +444,12 @@ async function fetchCronJobs(client, spinner, progress) {
341
444
  }
342
445
  }
343
446
  catch (error) {
344
- // pg_cron拡張がない場合はスキップ
447
+ // Skip when pg_cron extension is not present
345
448
  }
346
449
  return cronJobs;
347
450
  }
348
451
  /**
349
- * カスタム型を取得
452
+ * Fetch custom types
350
453
  */
351
454
  async function fetchCustomTypes(client, spinner, progress, schemas = ['public']) {
352
455
  const types = [];
@@ -368,41 +471,41 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
368
471
  JOIN pg_namespace n ON t.typnamespace = n.oid
369
472
  WHERE n.nspname IN (${schemaPlaceholders})
370
473
  AND t.typtype IN ('e', 'c', 'd')
371
- AND t.typisdefined = true -- 定義済みの型のみ
372
- AND NOT t.typarray = 0 -- 配列の基底型を除外
474
+ AND t.typisdefined = true -- defined types only
475
+ AND NOT t.typarray = 0 -- exclude array base types
373
476
  AND NOT EXISTS (
374
- -- テーブル、ビュー、インデックス、シーケンス、複合型と同名のものを除外
477
+ -- exclude same-name as table, view, index, sequence, composite
375
478
  SELECT 1 FROM pg_class c
376
479
  WHERE c.relname = t.typname
377
480
  AND c.relnamespace = n.oid
378
481
  )
379
482
  AND NOT EXISTS (
380
- -- 関数・プロシージャと同名のものを除外
483
+ -- exclude same-name as function/procedure
381
484
  SELECT 1 FROM pg_proc p
382
485
  WHERE p.proname = t.typname
383
486
  AND p.pronamespace = n.oid
384
487
  )
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_%' -- 一時的な型を除外
488
+ AND t.typname NOT LIKE 'pg_%' -- exclude PostgreSQL built-in types
489
+ AND t.typname NOT LIKE '_%' -- exclude array types (leading underscore)
490
+ AND t.typname NOT LIKE '%_old' -- exclude deprecated types
491
+ AND t.typname NOT LIKE '%_bak' -- exclude backup types
492
+ AND t.typname NOT LIKE 'tmp_%' -- exclude temporary types
390
493
  ORDER BY n.nspname, t.typname
391
494
  `, schemas);
392
- // 進行状況の初期化
495
+ // Initialize progress
393
496
  if (progress) {
394
497
  progress.customTypes.total = result.rows.length;
395
498
  }
396
499
  for (let i = 0; i < result.rows.length; i++) {
397
500
  const row = result.rows[i];
398
- // 進行状況を更新
501
+ // Update progress
399
502
  if (progress && spinner) {
400
503
  progress.customTypes.current = i + 1;
401
504
  displayProgress(progress, spinner);
402
505
  }
403
506
  let ddl = '';
404
507
  if (row.type_category === 'enum') {
405
- // ENUM型の詳細を取得
508
+ // Fetch ENUM type details
406
509
  const enumResult = await client.query(`
407
510
  SELECT enumlabel
408
511
  FROM pg_enum
@@ -414,14 +517,14 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
414
517
  )
415
518
  ORDER BY enumsortorder
416
519
  `, [row.type_name, row.schema_name]);
417
- // ENUM値が存在する場合のみDDLを生成
520
+ // Generate DDL only when ENUM values exist
418
521
  if (enumResult.rows.length > 0) {
419
522
  const labels = enumResult.rows.map(r => `'${r.enumlabel}'`).join(', ');
420
523
  ddl = `CREATE TYPE ${row.type_name} AS ENUM (${labels});`;
421
524
  }
422
525
  }
423
526
  else if (row.type_category === 'composite') {
424
- // COMPOSITE型の詳細を取得
527
+ // Fetch COMPOSITE type details
425
528
  const compositeResult = await client.query(`
426
529
  SELECT
427
530
  a.attname as column_name,
@@ -434,10 +537,10 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
434
537
  )
435
538
  )
436
539
  AND a.attnum > 0
437
- AND NOT a.attisdropped -- 削除されたカラムを除外
540
+ AND NOT a.attisdropped -- exclude dropped columns
438
541
  ORDER BY a.attnum
439
542
  `, [row.type_name, row.schema_name]);
440
- // コンポジット型の属性が存在する場合のみDDLを生成
543
+ // Generate DDL only when composite type has attributes
441
544
  if (compositeResult.rows.length > 0) {
442
545
  const columns = compositeResult.rows
443
546
  .map(r => ` ${r.column_name} ${r.column_type}`)
@@ -446,7 +549,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
446
549
  }
447
550
  }
448
551
  else if (row.type_category === 'domain') {
449
- // DOMAIN型の詳細を取得
552
+ // Fetch DOMAIN type details
450
553
  const domainResult = await client.query(`
451
554
  SELECT
452
555
  pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
@@ -476,7 +579,7 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
476
579
  }
477
580
  }
478
581
  if (ddl) {
479
- // 型コメントを先頭に追加
582
+ // Add type comment at top
480
583
  let finalDdl = '';
481
584
  if (!row.comment) {
482
585
  finalDdl += `-- Type: ${row.type_name}\n`;
@@ -484,9 +587,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
484
587
  else {
485
588
  finalDdl += `-- ${row.comment}\n`;
486
589
  }
487
- // 型定義を追加
590
+ // Add type definition
488
591
  finalDdl += ddl + '\n\n';
489
- // COMMENT ON文を追加
592
+ // Add COMMENT ON statement
490
593
  if (!row.comment) {
491
594
  finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
492
595
  }
@@ -494,8 +597,9 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
494
597
  finalDdl += `COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '${row.comment}';\n\n`;
495
598
  }
496
599
  types.push({
497
- name: `${row.schema_name}_${row.type_name}`,
600
+ name: row.type_name,
498
601
  type: 'type',
602
+ schema: row.schema_name,
499
603
  ddl: finalDdl,
500
604
  comment: row.comment,
501
605
  timestamp: Math.floor(Date.now() / 1000)
@@ -505,19 +609,19 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
505
609
  return types;
506
610
  }
507
611
  /**
508
- * データベースからテーブル定義を取得
612
+ * Fetch table definitions from database
509
613
  */
510
614
  async function fetchTableDefinitions(client, spinner, progress, schemas = ['public']) {
511
615
  const definitions = [];
512
616
  const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
513
- // テーブル一覧を取得
617
+ // Fetch table list
514
618
  const tablesResult = await client.query(`
515
619
  SELECT tablename, schemaname, 'table' as type
516
620
  FROM pg_tables
517
621
  WHERE schemaname IN (${schemaPlaceholders})
518
622
  ORDER BY schemaname, tablename
519
623
  `, schemas);
520
- // ビュー一覧を取得
624
+ // Fetch view list
521
625
  const viewsResult = await client.query(`
522
626
  SELECT viewname as tablename, schemaname, 'view' as type
523
627
  FROM pg_views
@@ -527,22 +631,22 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
527
631
  const allObjects = [...tablesResult.rows, ...viewsResult.rows];
528
632
  const tableCount = tablesResult.rows.length;
529
633
  const viewCount = viewsResult.rows.length;
530
- // 進行状況の初期化
634
+ // Initialize progress
531
635
  if (progress) {
532
636
  progress.tables.total = tableCount;
533
637
  progress.views.total = viewCount;
534
638
  }
535
- // 制限付き並行処理でテーブル/ビューを処理(接続数を制限)
536
- // 環境変数で最大値を設定可能(デフォルト20、最大50
639
+ // Process tables/views with limited concurrency (cap connection count)
640
+ // Max configurable via env (default 20, max 50)
537
641
  const envValue = process.env.SUPATOOL_MAX_CONCURRENT || '20';
538
642
  const MAX_CONCURRENT = Math.min(50, parseInt(envValue));
539
- // 環境変数で設定された値を使用(最小5でキャップ)
643
+ // Use env value (capped at minimum 5)
540
644
  const CONCURRENT_LIMIT = Math.max(5, MAX_CONCURRENT);
541
- // デバッグログ(開発時のみ)
645
+ // Debug log (development only)
542
646
  if (process.env.NODE_ENV === 'development' || process.env.SUPATOOL_DEBUG) {
543
647
  console.log(`Processing ${allObjects.length} objects with ${CONCURRENT_LIMIT} concurrent operations`);
544
648
  }
545
- // テーブル/ビュー処理のPromise生成関数
649
+ // Promise factory for table/view processing
546
650
  const processObject = async (obj, index) => {
547
651
  const isTable = obj.type === 'table';
548
652
  const name = obj.tablename;
@@ -552,9 +656,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
552
656
  let comment = '';
553
657
  let timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
554
658
  if (type === 'table') {
555
- // テーブルの場合
659
+ // Table case
556
660
  try {
557
- // テーブルの最終更新時刻を取得
661
+ // Get table last updated time
558
662
  const tableStatsResult = await client.query(`
559
663
  SELECT
560
664
  EXTRACT(EPOCH FROM GREATEST(
@@ -571,11 +675,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
571
675
  }
572
676
  }
573
677
  catch (error) {
574
- // エラーの場合はデフォルトタイムスタンプを使用
678
+ // On error use default timestamp
575
679
  }
576
- // CREATE TABLE文を生成
680
+ // Generate CREATE TABLE statement
577
681
  ddl = await generateCreateTableDDL(client, name, schemaName);
578
- // テーブルコメントを取得
682
+ // Get table comment
579
683
  try {
580
684
  const tableCommentResult = await client.query(`
581
685
  SELECT obj_description(c.oid) as table_comment
@@ -588,13 +692,13 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
588
692
  }
589
693
  }
590
694
  catch (error) {
591
- // エラーの場合はコメントなし
695
+ // On error no comment
592
696
  }
593
697
  }
594
698
  else {
595
- // ビューの場合
699
+ // View case
596
700
  try {
597
- // ビューの定義とsecurity_invoker設定を取得
701
+ // Get view definition and security_invoker setting
598
702
  const viewResult = await client.query(`
599
703
  SELECT
600
704
  pv.definition,
@@ -610,14 +714,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
610
714
  `, [schemaName, name]);
611
715
  if (viewResult.rows.length > 0) {
612
716
  const view = viewResult.rows[0];
613
- // ビューのコメントを取得
717
+ // Get view comment
614
718
  const viewCommentResult = await client.query(`
615
719
  SELECT obj_description(c.oid) as view_comment
616
720
  FROM pg_class c
617
721
  JOIN pg_namespace n ON c.relnamespace = n.oid
618
722
  WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
619
723
  `, [name, schemaName]);
620
- // ビューコメントを先頭に追加
724
+ // Add view comment at top
621
725
  if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
622
726
  comment = viewCommentResult.rows[0].view_comment;
623
727
  ddl = `-- ${comment}\n`;
@@ -625,9 +729,9 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
625
729
  else {
626
730
  ddl = `-- View: ${name}\n`;
627
731
  }
628
- // ビュー定義を追加
629
- let ddlStart = `CREATE OR REPLACE VIEW ${name}`;
630
- // security_invoker設定をチェック
732
+ // Add view definition
733
+ let ddlStart = `CREATE OR REPLACE VIEW ${schemaName}.${name}`;
734
+ // Check security_invoker setting
631
735
  if (view.reloptions) {
632
736
  for (const option of view.reloptions) {
633
737
  if (option.startsWith('security_invoker=')) {
@@ -643,14 +747,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
643
747
  }
644
748
  }
645
749
  ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
646
- // COMMENT ON文を追加
750
+ // Add COMMENT ON statement
647
751
  if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
648
752
  ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
649
753
  }
650
754
  else {
651
755
  ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
652
756
  }
653
- // ビューの作成時刻を取得(可能であれば)
757
+ // Get view creation time (if available)
654
758
  try {
655
759
  const viewStatsResult = await client.query(`
656
760
  SELECT EXTRACT(EPOCH FROM GREATEST(
@@ -666,41 +770,42 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
666
770
  }
667
771
  }
668
772
  catch (error) {
669
- // エラーの場合はデフォルトタイムスタンプを使用
773
+ // On error use default timestamp
670
774
  }
671
775
  }
672
776
  }
673
777
  catch (error) {
674
- // エラーの場合はコメントなし
778
+ // On error no comment
675
779
  }
676
780
  }
677
781
  return {
678
- name: schemaName === 'public' ? name : `${schemaName}_${name}`,
782
+ name,
679
783
  type,
784
+ schema: schemaName,
680
785
  ddl,
681
786
  timestamp,
682
787
  comment: comment || undefined,
683
788
  isTable
684
789
  };
685
790
  };
686
- // シンプルなバッチ並行処理(確実な進行状況更新)
791
+ // Simple batch concurrency (reliable progress updates)
687
792
  const processedResults = [];
688
793
  for (let i = 0; i < allObjects.length; i += CONCURRENT_LIMIT) {
689
794
  const batch = allObjects.slice(i, i + CONCURRENT_LIMIT);
690
- // バッチを並行処理
795
+ // Process batch in parallel
691
796
  const batchPromises = batch.map(async (obj, batchIndex) => {
692
797
  try {
693
798
  const globalIndex = i + batchIndex;
694
- // デバッグ: 処理開始
799
+ // Debug: start
695
800
  if (process.env.SUPATOOL_DEBUG) {
696
801
  console.log(`Starting ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
697
802
  }
698
803
  const result = await processObject(obj, globalIndex);
699
- // デバッグ: 処理完了
804
+ // Debug: done
700
805
  if (process.env.SUPATOOL_DEBUG) {
701
806
  console.log(`Completed ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
702
807
  }
703
- // 個別完了時に即座に進行状況を更新
808
+ // Update progress immediately on each completion
704
809
  if (result && progress && spinner) {
705
810
  if (result.isTable) {
706
811
  progress.tables.current = Math.min(progress.tables.current + 1, progress.tables.total);
@@ -717,11 +822,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
717
822
  return null;
718
823
  }
719
824
  });
720
- // バッチの完了を待機
825
+ // Wait for batch to complete
721
826
  const batchResults = await Promise.all(batchPromises);
722
827
  processedResults.push(...batchResults);
723
828
  }
724
- // null値を除外してdefinitionsに追加
829
+ // Add to definitions excluding nulls
725
830
  for (const result of processedResults) {
726
831
  if (result) {
727
832
  const { isTable, ...definition } = result;
@@ -731,12 +836,12 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
731
836
  return definitions;
732
837
  }
733
838
  /**
734
- * CREATE TABLE DDLを生成(並行処理版)
839
+ * Generate CREATE TABLE DDL (concurrent version)
735
840
  */
736
841
  async function generateCreateTableDDL(client, tableName, schemaName = 'public') {
737
- // 全てのクエリを並行実行
842
+ // Run all queries in parallel
738
843
  const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult, foreignKeyResult] = await Promise.all([
739
- // カラム情報を取得
844
+ // Get column info
740
845
  client.query(`
741
846
  SELECT
742
847
  c.column_name,
@@ -754,7 +859,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
754
859
  AND c.table_name = $2
755
860
  ORDER BY c.ordinal_position
756
861
  `, [schemaName, tableName]),
757
- // 主キー情報を取得
862
+ // Get primary key info
758
863
  client.query(`
759
864
  SELECT column_name
760
865
  FROM information_schema.table_constraints tc
@@ -765,14 +870,14 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
765
870
  AND tc.constraint_type = 'PRIMARY KEY'
766
871
  ORDER BY kcu.ordinal_position
767
872
  `, [schemaName, tableName]),
768
- // テーブルコメントを取得
873
+ // Get table comment
769
874
  client.query(`
770
875
  SELECT obj_description(c.oid) as table_comment
771
876
  FROM pg_class c
772
877
  JOIN pg_namespace n ON c.relnamespace = n.oid
773
878
  WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
774
879
  `, [tableName, schemaName]),
775
- // カラムコメントを取得
880
+ // Get column comments
776
881
  client.query(`
777
882
  SELECT
778
883
  c.column_name,
@@ -786,7 +891,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
786
891
  AND pgn.nspname = $1
787
892
  ORDER BY c.ordinal_position
788
893
  `, [schemaName, tableName]),
789
- // UNIQUE制約を取得
894
+ // Get UNIQUE constraints
790
895
  client.query(`
791
896
  SELECT
792
897
  tc.constraint_name,
@@ -800,7 +905,7 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
800
905
  GROUP BY tc.constraint_name
801
906
  ORDER BY tc.constraint_name
802
907
  `, [schemaName, tableName]),
803
- // FOREIGN KEY制約を取得
908
+ // Get FOREIGN KEY constraints
804
909
  client.query(`
805
910
  SELECT
806
911
  tc.constraint_name,
@@ -826,51 +931,51 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
826
931
  columnComments.set(row.column_name, row.column_comment);
827
932
  }
828
933
  });
829
- // テーブルコメントを先頭に追加(スキーマ名を含む)
934
+ // Add table comment at top (with schema name)
830
935
  let ddl = '';
831
- // テーブルコメントを先頭に追加
936
+ // Add table comment at top
832
937
  if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
833
938
  ddl += `-- ${tableCommentResult.rows[0].table_comment}\n`;
834
939
  }
835
940
  else {
836
- ddl += `-- Table: ${tableName}\n`;
941
+ ddl += `-- Table: ${schemaName}.${tableName}\n`;
837
942
  }
838
- // CREATE TABLE文を生成
839
- ddl += `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
943
+ // Generate CREATE TABLE (schema-qualified)
944
+ ddl += `CREATE TABLE IF NOT EXISTS ${schemaName}.${tableName} (\n`;
840
945
  const columnDefs = [];
841
946
  for (const col of columnsResult.rows) {
842
947
  const rawType = col.full_type ||
843
948
  ((col.data_type === 'USER-DEFINED' && col.udt_name) ? col.udt_name : col.data_type);
844
949
  let colDef = ` ${col.column_name} ${rawType}`;
845
- // 長さ指定
950
+ // Length spec
846
951
  if (col.character_maximum_length) {
847
952
  colDef += `(${col.character_maximum_length})`;
848
953
  }
849
- // NOT NULL制約
954
+ // NOT NULL constraint
850
955
  if (col.is_nullable === 'NO') {
851
956
  colDef += ' NOT NULL';
852
957
  }
853
- // デフォルト値
958
+ // Default value
854
959
  if (col.column_default) {
855
960
  colDef += ` DEFAULT ${col.column_default}`;
856
961
  }
857
962
  columnDefs.push(colDef);
858
963
  }
859
964
  ddl += columnDefs.join(',\n');
860
- // 主キー制約
965
+ // Primary key constraint
861
966
  if (primaryKeyResult.rows.length > 0) {
862
967
  const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
863
968
  ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
864
969
  }
865
- // UNIQUE制約をCREATE TABLE内に追加
970
+ // Add UNIQUE constraints inside CREATE TABLE
866
971
  for (const unique of uniqueConstraintResult.rows) {
867
972
  ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
868
973
  }
869
- // FOREIGN KEY制約をCREATE TABLE内に追加
974
+ // Add FOREIGN KEY constraints inside CREATE TABLE
870
975
  for (const fk of foreignKeyResult.rows) {
871
976
  ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
872
977
  }
873
- // CHECK制約をCREATE TABLE内に追加(必ず最後)
978
+ // Add CHECK constraints inside CREATE TABLE (must be last)
874
979
  const checkConstraintResult = await client.query(`
875
980
  SELECT
876
981
  con.conname as constraint_name,
@@ -887,16 +992,16 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
887
992
  ddl += `,\n CONSTRAINT ${check.constraint_name} ${check.check_clause}`;
888
993
  }
889
994
  ddl += '\n);\n\n';
890
- // COMMENT ON文を追加
995
+ // Add COMMENT ON statements
891
996
  if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
892
997
  ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
893
998
  }
894
999
  else {
895
1000
  ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
896
1001
  }
897
- // カラムコメントを追加(スキーマ名を含む)
1002
+ // Add column comments (with schema name)
898
1003
  if (columnComments.size > 0) {
899
- ddl += '\n-- カラムコメント\n';
1004
+ ddl += '\n-- Column comments\n';
900
1005
  for (const [columnName, comment] of columnComments) {
901
1006
  ddl += `COMMENT ON COLUMN ${schemaName}.${tableName}.${columnName} IS '${comment}';\n`;
902
1007
  }
@@ -904,271 +1009,386 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
904
1009
  return ddl;
905
1010
  }
906
1011
  /**
907
- * 定義をファイルに保存
1012
+ * Save definitions to files (merge RLS/triggers into table/view; schema folders when multi-schema)
908
1013
  */
909
- async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true) {
1014
+ async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true, schemas = ['public'], relations = [], rpcTables = [], allSchemas = [], version, tableRlsStatus = []) {
910
1015
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
911
1016
  const path = await Promise.resolve().then(() => __importStar(require('path')));
912
- // 出力ディレクトリを作成
1017
+ const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
1018
+ const npmUrl = 'https://www.npmjs.com/package/supatool';
1019
+ const headerComment = version
1020
+ ? `-- Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n`
1021
+ : `-- Generated by supatool, ${outputDate} | ${npmUrl}\n`;
1022
+ const tables = definitions.filter(d => d.type === 'table');
1023
+ const views = definitions.filter(d => d.type === 'view');
1024
+ const rlsList = definitions.filter(d => d.type === 'rls');
1025
+ const triggersList = definitions.filter(d => d.type === 'trigger');
1026
+ const functions = definitions.filter(d => d.type === 'function');
1027
+ const cronJobs = definitions.filter(d => d.type === 'cron');
1028
+ const customTypes = definitions.filter(d => d.type === 'type');
1029
+ // schema.table -> RLS DDL
1030
+ const rlsByCategory = new Map();
1031
+ for (const r of rlsList) {
1032
+ if (r.category)
1033
+ rlsByCategory.set(r.category, r.ddl);
1034
+ }
1035
+ // schema.table -> trigger DDL array
1036
+ const triggersByCategory = new Map();
1037
+ for (const t of triggersList) {
1038
+ if (!t.category)
1039
+ continue;
1040
+ const list = triggersByCategory.get(t.category) ?? [];
1041
+ list.push(t.ddl);
1042
+ triggersByCategory.set(t.category, list);
1043
+ }
1044
+ // Build merged DDL (table/view + RLS + triggers)
1045
+ const mergeRlsAndTriggers = (def) => {
1046
+ const cat = def.schema && def.name ? `${def.schema}.${def.name}` : def.category ?? '';
1047
+ let ddl = def.ddl.trimEnd();
1048
+ const rlsDdl = rlsByCategory.get(cat);
1049
+ if (rlsDdl) {
1050
+ ddl += '\n\n' + rlsDdl.trim();
1051
+ }
1052
+ const trgList = triggersByCategory.get(cat);
1053
+ if (trgList && trgList.length > 0) {
1054
+ ddl += '\n\n' + trgList.map(t => t.trim()).join('\n\n');
1055
+ }
1056
+ return ddl.endsWith('\n') ? ddl : ddl + '\n';
1057
+ };
1058
+ const mergedTables = tables.map(t => ({
1059
+ ...t,
1060
+ ddl: mergeRlsAndTriggers(t)
1061
+ }));
1062
+ const mergedViews = views.map(v => ({
1063
+ ...v,
1064
+ ddl: mergeRlsAndTriggers(v)
1065
+ }));
1066
+ const toWrite = [
1067
+ ...mergedTables,
1068
+ ...mergedViews,
1069
+ ...functions,
1070
+ ...cronJobs,
1071
+ ...customTypes
1072
+ ];
1073
+ const multiSchema = schemas.length > 1;
1074
+ const typeDirNames = {
1075
+ table: 'tables',
1076
+ view: 'views',
1077
+ function: 'rpc',
1078
+ cron: 'cron',
1079
+ type: 'types'
1080
+ };
913
1081
  if (!fs.existsSync(outputDir)) {
914
1082
  fs.mkdirSync(outputDir, { recursive: true });
915
1083
  }
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 });
1084
+ const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
1085
+ for (const def of toWrite) {
1086
+ const typeDir = typeDirNames[def.type];
1087
+ const baseTypeDir = separateDirectories ? typeDir : '.';
1088
+ const targetDir = multiSchema && def.schema
1089
+ ? path.join(outputDir, def.schema, baseTypeDir)
1090
+ : path.join(outputDir, baseTypeDir);
1091
+ if (!fs.existsSync(targetDir)) {
1092
+ fs.mkdirSync(targetDir, { recursive: true });
940
1093
  }
941
- }
942
- // 並行ファイル書き込み
943
- const writePromises = definitions.map(async (def) => {
944
- const targetDir = typeDirectories[def.type];
945
- // ファイル名を決定(TypeとTriggerを区別しやすくする)
946
1094
  let fileName;
947
1095
  if (def.type === 'function') {
948
1096
  fileName = `fn_${def.name}.sql`;
949
1097
  }
950
- else if (def.type === 'trigger') {
951
- fileName = `trg_${def.name}.sql`;
952
- }
953
1098
  else {
954
1099
  fileName = `${def.name}.sql`;
955
1100
  }
956
1101
  const filePath = path.join(targetDir, fileName);
957
- // 最後に改行を追加
958
1102
  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);
1103
+ await fsPromises.writeFile(filePath, headerComment + ddlWithNewline);
1104
+ }
1105
+ await generateIndexFile(toWrite, outputDir, separateDirectories, multiSchema, relations, rpcTables, allSchemas, schemas, version, tableRlsStatus);
967
1106
  }
968
1107
  /**
969
- * データベースオブジェクトのインデックスファイルを生成
970
- * AIが構造を理解しやすいように1行ずつリストアップ
1108
+ * Generate index file for DB objects (RLS/triggers already merged into table/view)
971
1109
  */
972
- async function generateIndexFile(definitions, outputDir, separateDirectories = true) {
1110
+ async function generateIndexFile(definitions, outputDir, separateDirectories = true, multiSchema = false, relations = [], rpcTables = [], allSchemas = [], extractedSchemas = [], version, tableRlsStatus = []) {
973
1111
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
974
1112
  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')
1113
+ const outputDate = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' });
1114
+ const npmUrl = 'https://www.npmjs.com/package/supatool';
1115
+ const headerLine = version
1116
+ ? `Generated by supatool v${version}, ${outputDate} | ${npmUrl}\n\n`
1117
+ : `Generated by supatool, ${outputDate} | ${npmUrl}\n\n`;
1118
+ const readmeHeader = version
1119
+ ? `Generated by [supatool](${npmUrl}) v${version}, ${outputDate}\n\n`
1120
+ : `Generated by [supatool](${npmUrl}), ${outputDate}\n\n`;
1121
+ const typeDirNames = {
1122
+ table: 'tables',
1123
+ view: 'views',
1124
+ function: 'rpc',
1125
+ cron: 'cron',
1126
+ type: 'types'
984
1127
  };
985
1128
  const typeLabels = {
986
1129
  table: 'Tables',
987
1130
  view: 'Views',
988
- rls: 'RLS Policies',
989
1131
  function: 'Functions',
990
- trigger: 'Triggers',
991
1132
  cron: 'Cron Jobs',
992
1133
  type: 'Custom Types'
993
1134
  };
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`;
1135
+ const groupedDefs = {
1136
+ table: definitions.filter(def => def.type === 'table'),
1137
+ view: definitions.filter(def => def.type === 'view'),
1138
+ function: definitions.filter(def => def.type === 'function'),
1139
+ cron: definitions.filter(def => def.type === 'cron'),
1140
+ type: definitions.filter(def => def.type === 'type')
1141
+ };
1142
+ // schema.table -> RLS status (for Tables docs and warnings)
1143
+ const rlsMap = new Map();
1144
+ for (const s of tableRlsStatus) {
1145
+ rlsMap.set(`${s.schema}.${s.table}`, s);
1146
+ }
1147
+ const formatRlsNote = (schema, name) => {
1148
+ const s = rlsMap.get(`${schema}.${name}`);
1149
+ if (!s)
1150
+ return '';
1151
+ if (!s.rlsEnabled)
1152
+ return ' **⚠️ RLS disabled**';
1153
+ if (s.policyCount === 0)
1154
+ return ' (RLS: enabled, policies: 0)';
1155
+ return ` (RLS: enabled, policies: ${s.policyCount})`;
1156
+ };
1157
+ // Build relative path per file (schema/type/file when multiSchema)
1158
+ const getRelPath = (def) => {
1159
+ const typeDir = separateDirectories ? (typeDirNames[def.type] ?? def.type) : '.';
1160
+ const fileName = def.type === 'function' ? `fn_${def.name}.sql` : `${def.name}.sql`;
1161
+ if (multiSchema && def.schema) {
1162
+ return `${def.schema}/${typeDir}/${fileName}`;
1001
1163
  }
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
- });
1164
+ return separateDirectories ? `${typeDir}/${fileName}` : fileName;
1165
+ };
1166
+ // === Human-readable README.md (description + link to llms.txt) ===
1167
+ let readmeContent = readmeHeader;
1168
+ readmeContent += '# Schema (extract output)\n\n';
1169
+ readmeContent += 'This folder contains DDL exported by `supatool extract`.\n\n';
1170
+ readmeContent += '- **tables/** – Table definitions (with RLS and triggers in the same file)\n';
1171
+ readmeContent += '- **views/** – View definitions\n';
1172
+ readmeContent += '- **rpc/** – Functions\n';
1173
+ readmeContent += '- **cron/** – Cron jobs\n';
1174
+ readmeContent += '- **types/** Custom types\n\n';
1175
+ if (multiSchema) {
1176
+ readmeContent += 'When multiple schemas are extracted, each schema has its own subfolder (e.g. `public/tables/`, `agent/views/`).\n\n';
1177
+ }
1178
+ readmeContent += 'Full catalog and relations: [llms.txt](llms.txt)\n';
1179
+ if (tableRlsStatus.some(s => !s.rlsEnabled)) {
1180
+ readmeContent += '\n⚠️ Tables with RLS disabled: [rls_warnings.md](rls_warnings.md)\n';
1044
1181
  }
1045
- indexContent += '```\n';
1046
- // === AI向け llms.txt ===
1047
- let llmsContent = 'Database Schema - Complete Objects Catalog\n\n';
1048
- // Summary section
1182
+ // === llms.txt ===
1183
+ let llmsContent = headerLine;
1184
+ llmsContent += 'Database Schema - Complete Objects Catalog\n';
1185
+ llmsContent += '(Tables/Views include RLS and Triggers in the same file)\n\n';
1049
1186
  llmsContent += 'SUMMARY\n';
1050
1187
  Object.entries(groupedDefs).forEach(([type, defs]) => {
1051
- if (defs.length > 0) {
1188
+ if (defs.length > 0 && typeLabels[type]) {
1052
1189
  llmsContent += `${typeLabels[type]}: ${defs.length}\n`;
1053
1190
  }
1054
1191
  });
1055
- llmsContent += '\n';
1056
- // Flat list for AI processing (single format)
1057
- llmsContent += 'OBJECTS\n';
1192
+ llmsContent += '\nOBJECTS\n';
1058
1193
  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`;
1194
+ const filePath = getRelPath(def);
1195
+ const commentSuffix = def.comment ? ` # ${def.comment}` : '';
1196
+ const displayName = def.schema ? `${def.schema}.${def.name}` : def.name;
1197
+ const rlsSuffix = def.type === 'table' && def.schema ? formatRlsNote(def.schema, def.name) : '';
1198
+ llmsContent += `${def.type}:${displayName}:${filePath}${commentSuffix}${rlsSuffix}\n`;
1076
1199
  });
1077
- // ファイル保存
1078
- const indexPath = path.join(outputDir, 'index.md');
1200
+ if (relations.length > 0) {
1201
+ llmsContent += '\nRELATIONS\n';
1202
+ relations.forEach(r => {
1203
+ llmsContent += `${r.from} -> ${r.to}\n`;
1204
+ });
1205
+ }
1206
+ if (rpcTables.length > 0) {
1207
+ llmsContent += '\nRPC_TABLES\n';
1208
+ rpcTables.forEach(rt => {
1209
+ llmsContent += `${rt.rpc}: ${rt.tables.join(', ')}\n`;
1210
+ });
1211
+ }
1212
+ if (allSchemas.length > 0) {
1213
+ const extractedSet = new Set(extractedSchemas);
1214
+ const extractedList = allSchemas.filter(s => extractedSet.has(s));
1215
+ const notExtractedList = allSchemas.filter(s => !extractedSet.has(s));
1216
+ llmsContent += '\nALL_SCHEMAS\n';
1217
+ llmsContent += 'EXTRACTED\n';
1218
+ extractedList.forEach(schemaName => {
1219
+ llmsContent += `${schemaName}\n`;
1220
+ });
1221
+ llmsContent += '\nNOT_EXTRACTED\n';
1222
+ notExtractedList.forEach(schemaName => {
1223
+ llmsContent += `${schemaName}\n`;
1224
+ });
1225
+ }
1226
+ const readmePath = path.join(outputDir, 'README.md');
1079
1227
  const llmsPath = path.join(outputDir, 'llms.txt');
1080
- fs.writeFileSync(indexPath, indexContent);
1228
+ fs.writeFileSync(readmePath, readmeContent);
1081
1229
  fs.writeFileSync(llmsPath, llmsContent);
1230
+ // schema_index.json (same data for agents that parse JSON)
1231
+ const schemaIndex = {
1232
+ objects: definitions.map(def => {
1233
+ const base = {
1234
+ type: def.type,
1235
+ name: def.schema ? `${def.schema}.${def.name}` : def.name,
1236
+ path: getRelPath(def),
1237
+ ...(def.comment && { comment: def.comment })
1238
+ };
1239
+ if (def.type === 'table' && def.schema) {
1240
+ const s = rlsMap.get(`${def.schema}.${def.name}`);
1241
+ if (s) {
1242
+ base.rls = s.rlsEnabled ? (s.policyCount === 0 ? 'enabled_no_policies' : `enabled_${s.policyCount}_policies`) : 'disabled';
1243
+ }
1244
+ }
1245
+ return base;
1246
+ }),
1247
+ relations: relations.map(r => ({ from: r.from, to: r.to })),
1248
+ rpc_tables: rpcTables.map(rt => ({ rpc: rt.rpc, tables: rt.tables })),
1249
+ all_schemas: allSchemas.length > 0
1250
+ ? {
1251
+ extracted: allSchemas.filter(s => extractedSchemas.includes(s)),
1252
+ not_extracted: allSchemas.filter(s => !extractedSchemas.includes(s))
1253
+ }
1254
+ : undefined
1255
+ };
1256
+ fs.writeFileSync(path.join(outputDir, 'schema_index.json'), JSON.stringify(schemaIndex, null, 2), 'utf8');
1257
+ // schema_summary.md (one-file overview for AI) — include RLS status per table
1258
+ let summaryMd = '# Schema summary\n\n';
1259
+ const tableDefs = definitions.filter(d => d.type === 'table' || d.type === 'view');
1260
+ if (tableDefs.length > 0) {
1261
+ summaryMd += '## Tables / Views\n';
1262
+ tableDefs.forEach(d => {
1263
+ const name = d.schema ? `${d.schema}.${d.name}` : d.name;
1264
+ const rlsNote = d.type === 'table' && d.schema ? formatRlsNote(d.schema, d.name) : '';
1265
+ summaryMd += d.comment ? `- ${name}${rlsNote} (# ${d.comment})\n` : `- ${name}${rlsNote}\n`;
1266
+ });
1267
+ summaryMd += '\n';
1268
+ }
1269
+ if (relations.length > 0) {
1270
+ summaryMd += '## Relations\n';
1271
+ relations.forEach(r => {
1272
+ summaryMd += `- ${r.from} -> ${r.to}\n`;
1273
+ });
1274
+ summaryMd += '\n';
1275
+ }
1276
+ if (rpcTables.length > 0) {
1277
+ summaryMd += '## RPC → Tables\n';
1278
+ rpcTables.forEach(rt => {
1279
+ summaryMd += `- ${rt.rpc}: ${rt.tables.join(', ')}\n`;
1280
+ });
1281
+ summaryMd += '\n';
1282
+ }
1283
+ if (allSchemas.length > 0) {
1284
+ const extractedSet = new Set(extractedSchemas);
1285
+ summaryMd += '## Schemas\n';
1286
+ summaryMd += `- Extracted: ${allSchemas.filter(s => extractedSet.has(s)).join(', ') || '(none)'}\n`;
1287
+ summaryMd += `- Not extracted: ${allSchemas.filter(s => !extractedSet.has(s)).join(', ') || '(none)'}\n`;
1288
+ }
1289
+ fs.writeFileSync(path.join(outputDir, 'schema_summary.md'), summaryMd, 'utf8');
1290
+ // RLS disabled tables warning doc (tables only; RLS enabled with 0 policies is not warned)
1291
+ const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
1292
+ if (rlsNotEnabled.length > 0) {
1293
+ let warnMd = '# Tables with RLS disabled (warning)\n\n';
1294
+ warnMd += 'The following tables do not have Row Level Security enabled.\n';
1295
+ warnMd += 'Enabling RLS is recommended for production security.\n\n';
1296
+ warnMd += '| Schema | Table |\n|--------|-------|\n';
1297
+ rlsNotEnabled.forEach(s => {
1298
+ warnMd += `| ${s.schema} | ${s.table} |\n`;
1299
+ });
1300
+ fs.writeFileSync(path.join(outputDir, 'rls_warnings.md'), warnMd, 'utf8');
1301
+ }
1082
1302
  }
1083
1303
  /**
1084
- * 定義を分類して出力
1304
+ * Classify and output definitions
1085
1305
  */
1086
1306
  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証明書検証を無効化
1307
+ const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'], version } = options;
1308
+ // Disable Node.js SSL certificate verification
1089
1309
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
1090
- // 接続文字列の検証
1310
+ // Connection string validation
1091
1311
  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 設定ファイル');
1312
+ throw new Error('Connection string is not configured. Please set it using one of:\n' +
1313
+ '1. --connection option\n' +
1314
+ '2. SUPABASE_CONNECTION_STRING environment variable\n' +
1315
+ '3. DATABASE_URL environment variable\n' +
1316
+ '4. supatool.config.json configuration file');
1097
1317
  }
1098
- // 接続文字列の形式検証
1318
+ // Connection string format validation
1099
1319
  if (!connectionString.startsWith('postgresql://') && !connectionString.startsWith('postgres://')) {
1100
- throw new Error(`不正な接続文字列形式です: ${connectionString}\n` +
1101
- '正しい形式: postgresql://username:password@host:port/database');
1320
+ throw new Error(`Invalid connection string format: ${connectionString}\n` +
1321
+ 'Correct format: postgresql://username:password@host:port/database');
1102
1322
  }
1103
- // パスワード部分をURLエンコード
1323
+ // URL encode password part
1104
1324
  let encodedConnectionString = connectionString;
1105
- console.log('🔍 元の接続文字列:', connectionString);
1325
+ console.log('🔍 Original connection string:', connectionString);
1106
1326
  try {
1107
- // パスワードに@が含まれる場合の特別処理
1327
+ // Special handling when password contains @
1108
1328
  if (connectionString.includes('@') && connectionString.split('@').length > 2) {
1109
- console.log('⚠️ パスワードに@が含まれているため特別処理を実行');
1110
- // 最後の@を区切り文字として使用
1329
+ console.log('⚠️ Password contains @, executing special handling');
1330
+ // Use last @ as delimiter
1111
1331
  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
- // パスワード部分をエンコード
1332
+ const lastPart = parts.pop(); // Last part (host:port/database)
1333
+ const firstParts = parts.join('@'); // First part (postgresql://user:password)
1334
+ console.log(' Split result:');
1335
+ console.log(' First part:', firstParts);
1336
+ console.log(' Last part:', lastPart);
1337
+ // Encode password part
1118
1338
  const colonIndex = firstParts.lastIndexOf(':');
1119
1339
  if (colonIndex > 0) {
1120
1340
  const protocolAndUser = firstParts.substring(0, colonIndex);
1121
1341
  const password = firstParts.substring(colonIndex + 1);
1122
1342
  const encodedPassword = encodeURIComponent(password);
1123
1343
  encodedConnectionString = `${protocolAndUser}:${encodedPassword}@${lastPart}`;
1124
- console.log(' エンコード結果:');
1125
- console.log(' プロトコル+ユーザー:', protocolAndUser);
1126
- console.log(' 元パスワード:', password);
1127
- console.log(' エンコードパスワード:', encodedPassword);
1128
- console.log(' 最終接続文字列:', encodedConnectionString);
1344
+ console.log(' Encode result:');
1345
+ console.log(' Protocol+User:', protocolAndUser);
1346
+ console.log(' Original password:', password);
1347
+ console.log(' Encoded password:', encodedPassword);
1348
+ console.log(' Final connection string:', encodedConnectionString);
1129
1349
  }
1130
1350
  }
1131
1351
  else {
1132
- console.log('✅ 通常のURL解析を実行');
1133
- // 通常のURL解析
1352
+ console.log('✅ Executing normal URL parsing');
1353
+ // Normal URL parsing
1134
1354
  const url = new URL(connectionString);
1135
- // ユーザー名にドットが含まれる場合の処理
1355
+ // Handle username containing dots
1136
1356
  if (url.username && url.username.includes('.')) {
1137
- console.log(`ユーザー名(ドット含む): ${url.username}`);
1357
+ console.log(`Username (with dots): ${url.username}`);
1138
1358
  }
1139
1359
  if (url.password) {
1140
- // パスワード部分のみをエンコード
1360
+ // Encode only password part
1141
1361
  const encodedPassword = encodeURIComponent(url.password);
1142
1362
  url.password = encodedPassword;
1143
1363
  encodedConnectionString = url.toString();
1144
- console.log(' パスワードエンコード:', encodedPassword);
1364
+ console.log(' Password encoded:', encodedPassword);
1145
1365
  }
1146
1366
  }
1147
- // Supabase接続用にSSL設定を追加
1367
+ // Add SSL settings for Supabase connection
1148
1368
  if (!encodedConnectionString.includes('sslmode=')) {
1149
1369
  const separator = encodedConnectionString.includes('?') ? '&' : '?';
1150
1370
  encodedConnectionString += `${separator}sslmode=require`;
1151
- console.log(' SSL設定を追加:', encodedConnectionString);
1371
+ console.log(' SSL setting added:', encodedConnectionString);
1152
1372
  }
1153
- // デバッグ情報を表示(パスワードは隠す)
1373
+ // Display debug info (password hidden)
1154
1374
  const debugUrl = new URL(encodedConnectionString);
1155
1375
  const maskedPassword = debugUrl.password ? '*'.repeat(debugUrl.password.length) : '';
1156
1376
  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}`);
1377
+ console.log('🔍 Connection info:');
1378
+ console.log(` Host: ${debugUrl.hostname}`);
1379
+ console.log(` Port: ${debugUrl.port}`);
1380
+ console.log(` Database: ${debugUrl.pathname.slice(1)}`);
1381
+ console.log(` User: ${debugUrl.username}`);
1162
1382
  console.log(` SSL: ${debugUrl.searchParams.get('sslmode') || 'require'}`);
1163
1383
  }
1164
1384
  catch (error) {
1165
- // URL解析に失敗した場合は元の文字列を使用
1166
- console.warn('接続文字列のURL解析に失敗しました。特殊文字が含まれている可能性があります。');
1167
- console.warn('エラー詳細:', error instanceof Error ? error.message : String(error));
1385
+ // Use original string if URL parsing fails
1386
+ console.warn('Failed to parse connection string URL. May contain special characters.');
1387
+ console.warn('Error details:', error instanceof Error ? error.message : String(error));
1168
1388
  }
1169
1389
  const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1170
1390
  const readline = await Promise.resolve().then(() => __importStar(require('readline')));
1171
- // 上書き確認
1391
+ // Overwrite confirmation
1172
1392
  if (!force && fs.existsSync(outputDir)) {
1173
1393
  const files = fs.readdirSync(outputDir);
1174
1394
  if (files.length > 0) {
@@ -1186,7 +1406,7 @@ async function extractDefinitions(options) {
1186
1406
  }
1187
1407
  }
1188
1408
  }
1189
- // スピナーを動的インポート
1409
+ // Dynamic import for spinner
1190
1410
  const { default: ora } = await Promise.resolve().then(() => __importStar(require('ora')));
1191
1411
  const spinner = ora('Connecting to database...').start();
1192
1412
  const client = new pg_1.Client({
@@ -1197,14 +1417,14 @@ async function extractDefinitions(options) {
1197
1417
  }
1198
1418
  });
1199
1419
  try {
1200
- // 接続前のデバッグ情報
1201
- console.log('🔧 接続設定:');
1420
+ // Debug before connect
1421
+ console.log('🔧 Connection settings:');
1202
1422
  console.log(` SSL: rejectUnauthorized=false`);
1203
- console.log(` 接続文字列長: ${encodedConnectionString.length}`);
1423
+ console.log(` Connection string length: ${encodedConnectionString.length}`);
1204
1424
  await client.connect();
1205
1425
  spinner.text = 'Connected to database';
1206
1426
  let allDefinitions = [];
1207
- // 進行状況トラッカーを初期化
1427
+ // Initialize progress tracker
1208
1428
  const progress = {
1209
1429
  tables: { current: 0, total: 0 },
1210
1430
  views: { current: 0, total: 0 },
@@ -1215,14 +1435,14 @@ async function extractDefinitions(options) {
1215
1435
  customTypes: { current: 0, total: 0 }
1216
1436
  };
1217
1437
  if (all) {
1218
- // 事前に各オブジェクトの総数を取得
1438
+ // Get total count for each object type first
1219
1439
  spinner.text = 'Counting database objects...';
1220
- // テーブル・ビューの総数を取得
1440
+ // Get total tables/views count
1221
1441
  const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
1222
1442
  const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
1223
1443
  progress.tables.total = parseInt(tablesCountResult.rows[0].count);
1224
1444
  progress.views.total = parseInt(viewsCountResult.rows[0].count);
1225
- // RLS ポリシーの総数を取得(テーブル単位)
1445
+ // Get total RLS policy count (per table)
1226
1446
  try {
1227
1447
  const rlsCountResult = await client.query(`
1228
1448
  SELECT COUNT(DISTINCT tablename) as count
@@ -1234,7 +1454,7 @@ async function extractDefinitions(options) {
1234
1454
  catch (error) {
1235
1455
  progress.rls.total = 0;
1236
1456
  }
1237
- // 関数の総数を取得
1457
+ // Get total functions count
1238
1458
  const functionsCountResult = await client.query(`
1239
1459
  SELECT COUNT(*) as count
1240
1460
  FROM pg_proc p
@@ -1242,7 +1462,7 @@ async function extractDefinitions(options) {
1242
1462
  WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p')
1243
1463
  `);
1244
1464
  progress.functions.total = parseInt(functionsCountResult.rows[0].count);
1245
- // トリガーの総数を取得
1465
+ // Get total triggers count
1246
1466
  const triggersCountResult = await client.query(`
1247
1467
  SELECT COUNT(*) as count
1248
1468
  FROM pg_trigger t
@@ -1251,7 +1471,7 @@ async function extractDefinitions(options) {
1251
1471
  WHERE n.nspname = 'public' AND NOT t.tgisinternal
1252
1472
  `);
1253
1473
  progress.triggers.total = parseInt(triggersCountResult.rows[0].count);
1254
- // Cronジョブの総数を取得
1474
+ // Get total Cron jobs count
1255
1475
  try {
1256
1476
  const cronCountResult = await client.query('SELECT COUNT(*) as count FROM cron.job');
1257
1477
  progress.cronJobs.total = parseInt(cronCountResult.rows[0].count);
@@ -1259,7 +1479,7 @@ async function extractDefinitions(options) {
1259
1479
  catch (error) {
1260
1480
  progress.cronJobs.total = 0;
1261
1481
  }
1262
- // カスタム型の総数を取得
1482
+ // Get total custom types count
1263
1483
  const typesCountResult = await client.query(`
1264
1484
  SELECT COUNT(*) as count
1265
1485
  FROM pg_type t
@@ -1276,7 +1496,7 @@ async function extractDefinitions(options) {
1276
1496
  AND t.typname NOT LIKE '_%'
1277
1497
  `);
1278
1498
  progress.customTypes.total = parseInt(typesCountResult.rows[0].count);
1279
- // --all フラグが指定された場合は全てのオブジェクトを取得(順次処理)
1499
+ // When --all: fetch all objects (sequential)
1280
1500
  const tables = await fetchTableDefinitions(client, spinner, progress, schemas);
1281
1501
  const rlsPolicies = await fetchRlsPolicies(client, spinner, progress, schemas);
1282
1502
  const functions = await fetchFunctions(client, spinner, progress, schemas);
@@ -1293,8 +1513,8 @@ async function extractDefinitions(options) {
1293
1513
  ];
1294
1514
  }
1295
1515
  else {
1296
- // 従来の処理(テーブル・ビューのみ)
1297
- // テーブル・ビューの総数を取得
1516
+ // Legacy path (tables/views only)
1517
+ // Get total tables/views count
1298
1518
  const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
1299
1519
  const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
1300
1520
  progress.tables.total = parseInt(tablesCountResult.rows[0].count);
@@ -1310,15 +1530,63 @@ async function extractDefinitions(options) {
1310
1530
  allDefinitions = definitions;
1311
1531
  }
1312
1532
  }
1313
- // パターンマッチング
1533
+ // Pattern matching
1314
1534
  if (tablePattern !== '*') {
1315
1535
  const regex = new RegExp(tablePattern.replace(/\*/g, '.*'));
1316
1536
  allDefinitions = allDefinitions.filter(def => regex.test(def.name));
1317
1537
  }
1318
- // 定義を保存
1538
+ // Fetch for RELATIONS / RPC_TABLES (llms.txt upgrade)
1539
+ let relations = [];
1540
+ let rpcTables = [];
1541
+ let allSchemas = [];
1542
+ try {
1543
+ allSchemas = await fetchAllSchemas(client);
1544
+ relations = await fetchRelationList(client, schemas);
1545
+ const funcDefs = allDefinitions.filter(d => d.type === 'function');
1546
+ for (const f of funcDefs) {
1547
+ const tables = extractTableRefsFromFunctionDdl(f.ddl, f.schema ?? 'public');
1548
+ if (tables.length > 0) {
1549
+ rpcTables.push({
1550
+ rpc: f.schema ? `${f.schema}.${f.name}` : f.name,
1551
+ tables
1552
+ });
1553
+ }
1554
+ }
1555
+ }
1556
+ catch (err) {
1557
+ if (process.env.SUPATOOL_DEBUG) {
1558
+ console.warn('RELATIONS/RPC_TABLES extraction skipped:', err);
1559
+ }
1560
+ }
1561
+ // RLS status (for Tables docs, rls_warnings.md, and extract-time warning)
1562
+ let tableRlsStatus = [];
1563
+ try {
1564
+ const tableDefs = allDefinitions.filter(d => d.type === 'table');
1565
+ if (tableDefs.length > 0) {
1566
+ tableRlsStatus = await fetchTableRlsStatus(client, schemas);
1567
+ }
1568
+ }
1569
+ catch (err) {
1570
+ if (process.env.SUPATOOL_DEBUG) {
1571
+ console.warn('RLS status fetch skipped:', err);
1572
+ }
1573
+ }
1574
+ // When force: remove output dir then write (so removed tables don't leave files)
1575
+ if (force && fs.existsSync(outputDir)) {
1576
+ fs.rmSync(outputDir, { recursive: true });
1577
+ }
1578
+ // Save definitions (table+RLS+triggers merged, schema folders)
1319
1579
  spinner.text = 'Saving definitions to files...';
1320
- await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories);
1321
- // 統計を表示
1580
+ await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories, schemas, relations, rpcTables, allSchemas, version, tableRlsStatus);
1581
+ // Warn at extract time when any table has RLS disabled
1582
+ const rlsNotEnabled = tableRlsStatus.filter(s => !s.rlsEnabled);
1583
+ if (rlsNotEnabled.length > 0) {
1584
+ console.warn('');
1585
+ console.warn('⚠️ Tables with RLS disabled: ' + rlsNotEnabled.map(s => `${s.schema}.${s.table}`).join(', '));
1586
+ console.warn(' Details: ' + outputDir + '/rls_warnings.md');
1587
+ console.warn('');
1588
+ }
1589
+ // Show stats
1322
1590
  const counts = {
1323
1591
  table: allDefinitions.filter(def => def.type === 'table').length,
1324
1592
  view: allDefinitions.filter(def => def.type === 'view').length,
@@ -1328,7 +1596,7 @@ async function extractDefinitions(options) {
1328
1596
  cron: allDefinitions.filter(def => def.type === 'cron').length,
1329
1597
  type: allDefinitions.filter(def => def.type === 'type').length
1330
1598
  };
1331
- // 進捗表示を停止
1599
+ // Stop progress display
1332
1600
  stopProgressDisplay();
1333
1601
  spinner.succeed(`Extraction completed: ${outputDir}`);
1334
1602
  if (counts.table > 0)
@@ -1348,7 +1616,7 @@ async function extractDefinitions(options) {
1348
1616
  console.log('');
1349
1617
  }
1350
1618
  catch (error) {
1351
- // 進捗表示を停止(エラー時)
1619
+ // Stop progress display (on error)
1352
1620
  stopProgressDisplay();
1353
1621
  spinner.fail('Extraction failed');
1354
1622
  console.error('Error:', error);
@@ -1359,7 +1627,7 @@ async function extractDefinitions(options) {
1359
1627
  await client.end();
1360
1628
  }
1361
1629
  catch (closeError) {
1362
- // データベース接続の終了エラーは無視(既に切断されている場合など)
1630
+ // Ignore DB connection close errors (e.g. already disconnected)
1363
1631
  }
1364
1632
  }
1365
1633
  }