supatool 0.1.22 → 0.3.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.
@@ -0,0 +1,1205 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.extractDefinitions = extractDefinitions;
37
+ const pg_1 = require("pg");
38
+ /**
39
+ * 進行状況を表示
40
+ */
41
+ // グローバルなプログレス状態を保存
42
+ let globalProgress = null;
43
+ let progressUpdateInterval = null;
44
+ function displayProgress(progress, spinner) {
45
+ // グローバル状態を更新
46
+ globalProgress = progress;
47
+ // 定期更新を開始(まだ開始していない場合)
48
+ if (!progressUpdateInterval) {
49
+ progressUpdateInterval = setInterval(() => {
50
+ if (globalProgress && spinner) {
51
+ updateSpinnerDisplay(globalProgress, spinner);
52
+ }
53
+ }, 80); // 80msで常時更新
54
+ }
55
+ // 即座に表示を更新
56
+ updateSpinnerDisplay(progress, spinner);
57
+ }
58
+ function updateSpinnerDisplay(progress, spinner) {
59
+ // 全体進捗を計算
60
+ const totalObjects = progress.tables.total + progress.views.total + progress.rls.total +
61
+ progress.functions.total + progress.triggers.total + progress.cronJobs.total +
62
+ progress.customTypes.total;
63
+ const completedObjects = progress.tables.current + progress.views.current + progress.rls.current +
64
+ progress.functions.current + progress.triggers.current + progress.cronJobs.current +
65
+ progress.customTypes.current;
66
+ // プログレスバーを生成(稲妻が単純に増えていく)
67
+ const createProgressBar = (current, total, width = 20) => {
68
+ if (total === 0)
69
+ return '░'.repeat(width);
70
+ const percentage = Math.min(current / total, 1);
71
+ const filled = Math.floor(percentage * width);
72
+ const empty = width - filled;
73
+ // 稲妻で単純に埋める(文字の入れ替えなし)
74
+ return '⚡'.repeat(filled) + '░'.repeat(empty);
75
+ };
76
+ // 全体プログレスバーのみ表示(コンソール幅に収める)
77
+ const overallBar = createProgressBar(completedObjects, totalObjects, 20);
78
+ const overallPercent = totalObjects > 0 ? Math.floor((completedObjects / totalObjects) * 100) : 0;
79
+ // 緑色回転スピナー(常時回転)
80
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
81
+ const spinnerFrame = Math.floor((Date.now() / 80) % spinnerFrames.length);
82
+ const greenSpinner = `\x1b[32m${spinnerFrames[spinnerFrame]}\x1b[0m`;
83
+ // ドットアニメーション
84
+ const dotFrames = ["", ".", "..", "..."];
85
+ const dotFrame = Math.floor((Date.now() / 400) % dotFrames.length);
86
+ // 解析中メッセージ
87
+ const statusMessage = "Extracting";
88
+ // 改行制御付きコンパクト表示(緑色スピナー付き)
89
+ const display = `\r${greenSpinner} [${overallBar}] ${overallPercent}% (${completedObjects}/${totalObjects}) ${statusMessage}${dotFrames[dotFrame]}`;
90
+ spinner.text = display;
91
+ }
92
+ // 進捗表示を停止する関数
93
+ function stopProgressDisplay() {
94
+ if (progressUpdateInterval) {
95
+ clearInterval(progressUpdateInterval);
96
+ progressUpdateInterval = null;
97
+ }
98
+ globalProgress = null;
99
+ }
100
+ /**
101
+ * RLSポリシーを取得
102
+ */
103
+ async function fetchRlsPolicies(client, spinner, progress, schemas = ['public']) {
104
+ const policies = [];
105
+ try {
106
+ const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
107
+ const result = await client.query(`
108
+ SELECT
109
+ schemaname,
110
+ tablename,
111
+ policyname,
112
+ permissive,
113
+ roles,
114
+ cmd,
115
+ qual,
116
+ with_check
117
+ FROM pg_policies
118
+ WHERE schemaname IN (${schemaPlaceholders})
119
+ ORDER BY schemaname, tablename, policyname
120
+ `, schemas);
121
+ const groupedPolicies = {};
122
+ for (const row of result.rows) {
123
+ const tableKey = `${row.schemaname}.${row.tablename}`;
124
+ if (!groupedPolicies[tableKey]) {
125
+ groupedPolicies[tableKey] = [];
126
+ }
127
+ groupedPolicies[tableKey].push(row);
128
+ }
129
+ const tableKeys = Object.keys(groupedPolicies);
130
+ // 進行状況の初期化
131
+ if (progress) {
132
+ progress.rls.total = tableKeys.length;
133
+ }
134
+ for (let i = 0; i < tableKeys.length; i++) {
135
+ const tableKey = tableKeys[i];
136
+ const tablePolicies = groupedPolicies[tableKey];
137
+ const firstPolicy = tablePolicies[0];
138
+ const schemaName = firstPolicy.schemaname;
139
+ const tableName = firstPolicy.tablename;
140
+ // 進行状況を更新
141
+ if (progress && spinner) {
142
+ progress.rls.current = i + 1;
143
+ displayProgress(progress, spinner);
144
+ }
145
+ // RLSポリシー説明を先頭に追加
146
+ let ddl = `-- RLS Policies for ${schemaName}.${tableName}\n`;
147
+ ddl += `-- Row Level Security policies to control data access at the row level\n\n`;
148
+ ddl += `ALTER TABLE ${schemaName}.${tableName} ENABLE ROW LEVEL SECURITY;\n\n`;
149
+ for (const policy of tablePolicies) {
150
+ ddl += `CREATE POLICY ${policy.policyname}\n`;
151
+ ddl += ` ON ${schemaName}.${tableName}\n`;
152
+ ddl += ` AS ${policy.permissive || 'PERMISSIVE'}\n`;
153
+ ddl += ` FOR ${policy.cmd || 'ALL'}\n`;
154
+ if (policy.roles) {
155
+ // rolesが配列の場合と文字列の場合を処理
156
+ let roles;
157
+ if (Array.isArray(policy.roles)) {
158
+ roles = policy.roles.join(', ');
159
+ }
160
+ else if (typeof policy.roles === 'string') {
161
+ roles = policy.roles;
162
+ }
163
+ else {
164
+ // PostgreSQLの配列リテラル形式 "{role1,role2}" の場合
165
+ roles = String(policy.roles)
166
+ .replace(/[{}]/g, '') // 中括弧を除去
167
+ .replace(/"/g, ''); // ダブルクォートを除去
168
+ }
169
+ if (roles && roles.trim() !== '') {
170
+ ddl += ` TO ${roles}\n`;
171
+ }
172
+ }
173
+ if (policy.qual) {
174
+ ddl += ` USING (${policy.qual})\n`;
175
+ }
176
+ if (policy.with_check) {
177
+ ddl += ` WITH CHECK (${policy.with_check})\n`;
178
+ }
179
+ ddl += ';\n\n';
180
+ }
181
+ policies.push({
182
+ name: `${schemaName}_${tableName}_policies`,
183
+ type: 'rls',
184
+ category: `${schemaName}.${tableName}`,
185
+ ddl,
186
+ timestamp: Math.floor(Date.now() / 1000)
187
+ });
188
+ }
189
+ return policies;
190
+ }
191
+ catch (error) {
192
+ console.warn('Skipping RLS policies extraction:', error instanceof Error ? error.message : String(error));
193
+ return [];
194
+ }
195
+ }
196
+ /**
197
+ * 関数を取得
198
+ */
199
+ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
200
+ const functions = [];
201
+ const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
202
+ const result = await client.query(`
203
+ SELECT
204
+ p.proname as name,
205
+ pg_get_functiondef(p.oid) as definition,
206
+ n.nspname as schema_name,
207
+ obj_description(p.oid) as comment,
208
+ pg_get_function_identity_arguments(p.oid) as identity_args,
209
+ pg_get_function_arguments(p.oid) as full_args
210
+ FROM pg_proc p
211
+ JOIN pg_namespace n ON p.pronamespace = n.oid
212
+ WHERE n.nspname IN (${schemaPlaceholders})
213
+ AND p.prokind IN ('f', 'p') -- functions and procedures
214
+ ORDER BY n.nspname, p.proname
215
+ `, schemas);
216
+ // 進行状況の初期化
217
+ if (progress) {
218
+ progress.functions.total = result.rows.length;
219
+ }
220
+ for (let i = 0; i < result.rows.length; i++) {
221
+ const row = result.rows[i];
222
+ // 進行状況を更新
223
+ if (progress && spinner) {
224
+ progress.functions.current = i + 1;
225
+ displayProgress(progress, spinner);
226
+ }
227
+ // 正確な関数シグネチャを構築(スキーマ名と引数の型を含む)
228
+ const functionSignature = `${row.schema_name}.${row.name}(${row.identity_args || ''})`;
229
+ // 関数コメントを先頭に追加
230
+ let ddl = '';
231
+ if (!row.comment) {
232
+ ddl += `-- Function: ${functionSignature}\n`;
233
+ ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
234
+ }
235
+ else {
236
+ ddl += `-- ${row.comment}\n`;
237
+ ddl += `COMMENT ON FUNCTION ${functionSignature} IS '${row.comment}';\n\n`;
238
+ }
239
+ // 関数定義を追加
240
+ ddl += row.definition;
241
+ functions.push({
242
+ name: row.name,
243
+ type: 'function',
244
+ ddl,
245
+ comment: row.comment,
246
+ timestamp: Math.floor(Date.now() / 1000)
247
+ });
248
+ }
249
+ return functions;
250
+ }
251
+ /**
252
+ * トリガーを取得
253
+ */
254
+ async function fetchTriggers(client, spinner, progress, schemas = ['public']) {
255
+ const triggers = [];
256
+ const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
257
+ const result = await client.query(`
258
+ SELECT
259
+ t.tgname as trigger_name,
260
+ c.relname as table_name,
261
+ n.nspname as schema_name,
262
+ pg_get_triggerdef(t.oid) as definition
263
+ FROM pg_trigger t
264
+ JOIN pg_class c ON t.tgrelid = c.oid
265
+ JOIN pg_namespace n ON c.relnamespace = n.oid
266
+ WHERE n.nspname IN (${schemaPlaceholders})
267
+ AND NOT t.tgisinternal
268
+ ORDER BY n.nspname, c.relname, t.tgname
269
+ `, schemas);
270
+ // 進行状況の初期化
271
+ if (progress) {
272
+ progress.triggers.total = result.rows.length;
273
+ }
274
+ for (let i = 0; i < result.rows.length; i++) {
275
+ const row = result.rows[i];
276
+ // 進行状況を更新
277
+ if (progress && spinner) {
278
+ progress.triggers.current = i + 1;
279
+ displayProgress(progress, spinner);
280
+ }
281
+ // トリガー説明を先頭に追加
282
+ let ddl = `-- Trigger: ${row.trigger_name} on ${row.schema_name}.${row.table_name}\n`;
283
+ ddl += `-- Database trigger that automatically executes in response to certain events\n\n`;
284
+ ddl += row.definition + ';';
285
+ triggers.push({
286
+ name: `${row.schema_name}_${row.table_name}_${row.trigger_name}`,
287
+ type: 'trigger',
288
+ category: `${row.schema_name}.${row.table_name}`,
289
+ ddl,
290
+ timestamp: Math.floor(Date.now() / 1000)
291
+ });
292
+ }
293
+ return triggers;
294
+ }
295
+ /**
296
+ * Cronジョブを取得(pg_cron拡張)
297
+ */
298
+ async function fetchCronJobs(client, spinner, progress) {
299
+ const cronJobs = [];
300
+ try {
301
+ const result = await client.query(`
302
+ SELECT
303
+ jobid,
304
+ schedule,
305
+ command,
306
+ jobname,
307
+ active
308
+ FROM cron.job
309
+ ORDER BY jobid
310
+ `);
311
+ // 進行状況の初期化
312
+ if (progress) {
313
+ progress.cronJobs.total = result.rows.length;
314
+ }
315
+ for (let i = 0; i < result.rows.length; i++) {
316
+ const row = result.rows[i];
317
+ // 進行状況を更新
318
+ if (progress && spinner) {
319
+ progress.cronJobs.current = i + 1;
320
+ displayProgress(progress, spinner);
321
+ }
322
+ // Cronジョブ説明を先頭に追加
323
+ let ddl = `-- Cron Job: ${row.jobname || `job_${row.jobid}`}\n`;
324
+ ddl += `-- Scheduled job that runs automatically at specified intervals\n`;
325
+ ddl += `-- Schedule: ${row.schedule}\n`;
326
+ ddl += `-- Command: ${row.command}\n\n`;
327
+ ddl += `SELECT cron.schedule('${row.jobname || `job_${row.jobid}`}', '${row.schedule}', '${row.command}');`;
328
+ cronJobs.push({
329
+ name: row.jobname || `job_${row.jobid}`,
330
+ type: 'cron',
331
+ ddl,
332
+ timestamp: Math.floor(Date.now() / 1000)
333
+ });
334
+ }
335
+ }
336
+ catch (error) {
337
+ // pg_cron拡張がない場合はスキップ
338
+ }
339
+ return cronJobs;
340
+ }
341
+ /**
342
+ * カスタム型を取得
343
+ */
344
+ async function fetchCustomTypes(client, spinner, progress, schemas = ['public']) {
345
+ const types = [];
346
+ const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
347
+ const result = await client.query(`
348
+ SELECT
349
+ t.typname as type_name,
350
+ n.nspname as schema_name,
351
+ pg_catalog.format_type(t.oid, NULL) as type_definition,
352
+ obj_description(t.oid) as comment,
353
+ CASE
354
+ WHEN t.typtype = 'e' THEN 'enum'
355
+ WHEN t.typtype = 'c' THEN 'composite'
356
+ WHEN t.typtype = 'd' THEN 'domain'
357
+ ELSE 'other'
358
+ END as type_category,
359
+ t.typtype
360
+ FROM pg_type t
361
+ JOIN pg_namespace n ON t.typnamespace = n.oid
362
+ WHERE n.nspname IN (${schemaPlaceholders})
363
+ AND t.typtype IN ('e', 'c', 'd')
364
+ AND t.typisdefined = true -- 定義済みの型のみ
365
+ AND NOT t.typarray = 0 -- 配列の基底型を除外
366
+ AND NOT EXISTS (
367
+ -- テーブル、ビュー、インデックス、シーケンス、複合型と同名のものを除外
368
+ SELECT 1 FROM pg_class c
369
+ WHERE c.relname = t.typname
370
+ AND c.relnamespace = n.oid
371
+ )
372
+ AND NOT EXISTS (
373
+ -- 関数・プロシージャと同名のものを除外
374
+ SELECT 1 FROM pg_proc p
375
+ WHERE p.proname = t.typname
376
+ AND p.pronamespace = n.oid
377
+ )
378
+ AND t.typname NOT LIKE 'pg_%' -- PostgreSQL内部型を除外
379
+ AND t.typname NOT LIKE '_%' -- 配列型(アンダースコアで始まる)を除外
380
+ AND t.typname NOT LIKE '%_old' -- 削除予定の型を除外
381
+ AND t.typname NOT LIKE '%_bak' -- バックアップ型を除外
382
+ AND t.typname NOT LIKE 'tmp_%' -- 一時的な型を除外
383
+ ORDER BY n.nspname, t.typname
384
+ `, schemas);
385
+ // 進行状況の初期化
386
+ if (progress) {
387
+ progress.customTypes.total = result.rows.length;
388
+ }
389
+ for (let i = 0; i < result.rows.length; i++) {
390
+ const row = result.rows[i];
391
+ // 進行状況を更新
392
+ if (progress && spinner) {
393
+ progress.customTypes.current = i + 1;
394
+ displayProgress(progress, spinner);
395
+ }
396
+ let ddl = '';
397
+ if (row.type_category === 'enum') {
398
+ // ENUM型の詳細を取得
399
+ const enumResult = await client.query(`
400
+ SELECT enumlabel
401
+ FROM pg_enum
402
+ WHERE enumtypid = (
403
+ SELECT oid FROM pg_type
404
+ WHERE typname = $1 AND typnamespace = (
405
+ SELECT oid FROM pg_namespace WHERE nspname = $2
406
+ )
407
+ )
408
+ ORDER BY enumsortorder
409
+ `, [row.type_name, row.schema_name]);
410
+ // ENUM値が存在する場合のみDDLを生成
411
+ if (enumResult.rows.length > 0) {
412
+ const labels = enumResult.rows.map(r => `'${r.enumlabel}'`).join(', ');
413
+ ddl = `CREATE TYPE ${row.type_name} AS ENUM (${labels});`;
414
+ }
415
+ }
416
+ else if (row.type_category === 'composite') {
417
+ // COMPOSITE型の詳細を取得
418
+ const compositeResult = await client.query(`
419
+ SELECT
420
+ a.attname as column_name,
421
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as column_type
422
+ FROM pg_attribute a
423
+ WHERE a.attrelid = (
424
+ SELECT typrelid FROM pg_type
425
+ WHERE typname = $1 AND typnamespace = (
426
+ SELECT oid FROM pg_namespace WHERE nspname = $2
427
+ )
428
+ )
429
+ AND a.attnum > 0
430
+ AND NOT a.attisdropped -- 削除されたカラムを除外
431
+ ORDER BY a.attnum
432
+ `, [row.type_name, row.schema_name]);
433
+ // コンポジット型の属性が存在する場合のみDDLを生成
434
+ if (compositeResult.rows.length > 0) {
435
+ const columns = compositeResult.rows
436
+ .map(r => ` ${r.column_name} ${r.column_type}`)
437
+ .join(',\n');
438
+ ddl = `CREATE TYPE ${row.type_name} AS (\n${columns}\n);`;
439
+ }
440
+ }
441
+ else if (row.type_category === 'domain') {
442
+ // DOMAIN型の詳細を取得
443
+ const domainResult = await client.query(`
444
+ SELECT
445
+ pg_catalog.format_type(t.typbasetype, t.typtypmod) as base_type,
446
+ t.typnotnull,
447
+ t.typdefault,
448
+ (SELECT string_agg(pg_get_constraintdef(c.oid), ' AND ')
449
+ FROM pg_constraint c
450
+ WHERE c.contypid = t.oid) as constraints
451
+ FROM pg_type t
452
+ WHERE t.typname = $1
453
+ AND t.typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = $2)
454
+ AND t.typtype = 'd'
455
+ `, [row.type_name, row.schema_name]);
456
+ if (domainResult.rows.length > 0) {
457
+ const domain = domainResult.rows[0];
458
+ ddl = `CREATE DOMAIN ${row.type_name} AS ${domain.base_type}`;
459
+ if (domain.typdefault) {
460
+ ddl += ` DEFAULT ${domain.typdefault}`;
461
+ }
462
+ if (domain.typnotnull) {
463
+ ddl += ` NOT NULL`;
464
+ }
465
+ if (domain.constraints) {
466
+ ddl += ` CHECK (${domain.constraints})`;
467
+ }
468
+ ddl += ';';
469
+ }
470
+ }
471
+ if (ddl) {
472
+ // 型コメントを先頭に追加(スキーマ名を含む)
473
+ let finalDdl = '';
474
+ if (!row.comment) {
475
+ finalDdl += `-- Type: ${row.type_name}\n`;
476
+ finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
477
+ }
478
+ else {
479
+ finalDdl += `-- ${row.comment}\n`;
480
+ finalDdl += `COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '${row.comment}';\n\n`;
481
+ }
482
+ finalDdl += ddl;
483
+ types.push({
484
+ name: `${row.schema_name}_${row.type_name}`,
485
+ type: 'type',
486
+ ddl: finalDdl,
487
+ comment: row.comment,
488
+ timestamp: Math.floor(Date.now() / 1000)
489
+ });
490
+ }
491
+ }
492
+ return types;
493
+ }
494
+ /**
495
+ * データベースからテーブル定義を取得
496
+ */
497
+ async function fetchTableDefinitions(client, spinner, progress, schemas = ['public']) {
498
+ const definitions = [];
499
+ const schemaPlaceholders = schemas.map((_, index) => `$${index + 1}`).join(', ');
500
+ // テーブル一覧を取得
501
+ const tablesResult = await client.query(`
502
+ SELECT tablename, schemaname, 'table' as type
503
+ FROM pg_tables
504
+ WHERE schemaname IN (${schemaPlaceholders})
505
+ ORDER BY schemaname, tablename
506
+ `, schemas);
507
+ // ビュー一覧を取得
508
+ const viewsResult = await client.query(`
509
+ SELECT viewname as tablename, schemaname, 'view' as type
510
+ FROM pg_views
511
+ WHERE schemaname IN (${schemaPlaceholders})
512
+ ORDER BY schemaname, viewname
513
+ `, schemas);
514
+ const allObjects = [...tablesResult.rows, ...viewsResult.rows];
515
+ const tableCount = tablesResult.rows.length;
516
+ const viewCount = viewsResult.rows.length;
517
+ // 進行状況の初期化
518
+ if (progress) {
519
+ progress.tables.total = tableCount;
520
+ progress.views.total = viewCount;
521
+ }
522
+ // 制限付き並行処理でテーブル/ビューを処理(接続数を制限)
523
+ // 環境変数で最大値を設定可能(デフォルト20、最大50)
524
+ const envValue = process.env.SUPATOOL_MAX_CONCURRENT || '20';
525
+ const MAX_CONCURRENT = Math.min(50, parseInt(envValue));
526
+ // 環境変数で設定された値を使用(最小5でキャップ)
527
+ const CONCURRENT_LIMIT = Math.max(5, MAX_CONCURRENT);
528
+ // デバッグログ(開発時のみ)
529
+ if (process.env.NODE_ENV === 'development' || process.env.SUPATOOL_DEBUG) {
530
+ console.log(`Processing ${allObjects.length} objects with ${CONCURRENT_LIMIT} concurrent operations`);
531
+ }
532
+ // テーブル/ビュー処理のPromise生成関数
533
+ const processObject = async (obj, index) => {
534
+ const isTable = obj.type === 'table';
535
+ const name = obj.tablename;
536
+ const schemaName = obj.schemaname;
537
+ const type = obj.type;
538
+ let ddl = '';
539
+ let comment = '';
540
+ let timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
541
+ if (type === 'table') {
542
+ // テーブルの場合
543
+ try {
544
+ // テーブルの最終更新時刻を取得
545
+ const tableStatsResult = await client.query(`
546
+ SELECT
547
+ EXTRACT(EPOCH FROM GREATEST(
548
+ COALESCE(last_vacuum, '1970-01-01'::timestamp),
549
+ COALESCE(last_autovacuum, '1970-01-01'::timestamp),
550
+ COALESCE(last_analyze, '1970-01-01'::timestamp),
551
+ COALESCE(last_autoanalyze, '1970-01-01'::timestamp)
552
+ ))::bigint as last_updated
553
+ FROM pg_stat_user_tables
554
+ WHERE relname = $1 AND schemaname = $2
555
+ `, [name, schemaName]);
556
+ if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
557
+ timestamp = tableStatsResult.rows[0].last_updated;
558
+ }
559
+ }
560
+ catch (error) {
561
+ // エラーの場合はデフォルトタイムスタンプを使用
562
+ }
563
+ // CREATE TABLE文を生成
564
+ ddl = await generateCreateTableDDL(client, name, schemaName);
565
+ // テーブルコメントを取得
566
+ try {
567
+ const tableCommentResult = await client.query(`
568
+ SELECT obj_description(c.oid) as table_comment
569
+ FROM pg_class c
570
+ JOIN pg_namespace n ON c.relnamespace = n.oid
571
+ WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
572
+ `, [name, schemaName]);
573
+ if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
574
+ comment = tableCommentResult.rows[0].table_comment;
575
+ }
576
+ }
577
+ catch (error) {
578
+ // エラーの場合はコメントなし
579
+ }
580
+ }
581
+ else {
582
+ // ビューの場合
583
+ try {
584
+ // ビューの定義とsecurity_invoker設定を取得
585
+ const viewResult = await client.query(`
586
+ SELECT
587
+ pv.definition,
588
+ c.relname,
589
+ c.reloptions
590
+ FROM pg_views pv
591
+ JOIN pg_class c ON c.relname = pv.viewname
592
+ JOIN pg_namespace n ON c.relnamespace = n.oid
593
+ WHERE pv.schemaname = $1
594
+ AND pv.viewname = $2
595
+ AND n.nspname = $1
596
+ AND c.relkind = 'v'
597
+ `, [schemaName, name]);
598
+ if (viewResult.rows.length > 0) {
599
+ const view = viewResult.rows[0];
600
+ // ビューのコメントを取得
601
+ const viewCommentResult = await client.query(`
602
+ SELECT obj_description(c.oid) as view_comment
603
+ FROM pg_class c
604
+ JOIN pg_namespace n ON c.relnamespace = n.oid
605
+ WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
606
+ `, [name, schemaName]);
607
+ // ビューコメントを先頭に追加
608
+ if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
609
+ comment = viewCommentResult.rows[0].view_comment;
610
+ ddl = `-- ${comment}\n`;
611
+ ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
612
+ }
613
+ else {
614
+ ddl = `-- View: ${name}\n`;
615
+ ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
616
+ }
617
+ let ddlStart = `CREATE OR REPLACE VIEW ${name}`;
618
+ // security_invoker設定をチェック
619
+ if (view.reloptions) {
620
+ for (const option of view.reloptions) {
621
+ if (option.startsWith('security_invoker=')) {
622
+ const value = option.split('=')[1];
623
+ if (value === 'on' || value === 'true') {
624
+ ddlStart += ' WITH (security_invoker = on)';
625
+ }
626
+ else if (value === 'off' || value === 'false') {
627
+ ddlStart += ' WITH (security_invoker = off)';
628
+ }
629
+ break;
630
+ }
631
+ }
632
+ }
633
+ ddl += `${ddlStart} AS\n${view.definition}`;
634
+ // ビューの作成時刻を取得(可能であれば)
635
+ try {
636
+ const viewStatsResult = await client.query(`
637
+ SELECT EXTRACT(EPOCH FROM GREATEST(
638
+ COALESCE(pg_stat_get_last_vacuum_time(c.oid), '1970-01-01'::timestamp),
639
+ COALESCE(pg_stat_get_last_analyze_time(c.oid), '1970-01-01'::timestamp)
640
+ ))::bigint as last_updated
641
+ FROM pg_class c
642
+ JOIN pg_namespace n ON c.relnamespace = n.oid
643
+ WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'v'
644
+ `, [name, schemaName]);
645
+ if (viewStatsResult.rows.length > 0 && viewStatsResult.rows[0].last_updated > 0) {
646
+ timestamp = viewStatsResult.rows[0].last_updated;
647
+ }
648
+ }
649
+ catch (error) {
650
+ // エラーの場合はデフォルトタイムスタンプを使用
651
+ }
652
+ }
653
+ }
654
+ catch (error) {
655
+ console.error(`Failed to fetch view definition: ${name}`, error);
656
+ return null;
657
+ }
658
+ }
659
+ return {
660
+ name: schemaName === 'public' ? name : `${schemaName}_${name}`,
661
+ type,
662
+ ddl,
663
+ timestamp,
664
+ comment: comment || undefined,
665
+ isTable
666
+ };
667
+ };
668
+ // シンプルなバッチ並行処理(確実な進行状況更新)
669
+ const processedResults = [];
670
+ for (let i = 0; i < allObjects.length; i += CONCURRENT_LIMIT) {
671
+ const batch = allObjects.slice(i, i + CONCURRENT_LIMIT);
672
+ // バッチを並行処理
673
+ const batchPromises = batch.map(async (obj, batchIndex) => {
674
+ try {
675
+ const globalIndex = i + batchIndex;
676
+ // デバッグ: 処理開始
677
+ if (process.env.SUPATOOL_DEBUG) {
678
+ console.log(`Starting ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
679
+ }
680
+ const result = await processObject(obj, globalIndex);
681
+ // デバッグ: 処理完了
682
+ if (process.env.SUPATOOL_DEBUG) {
683
+ console.log(`Completed ${obj.type} ${obj.tablename} (${globalIndex + 1}/${allObjects.length})`);
684
+ }
685
+ // 個別完了時に即座に進行状況を更新
686
+ if (result && progress && spinner) {
687
+ if (result.isTable) {
688
+ progress.tables.current = Math.min(progress.tables.current + 1, progress.tables.total);
689
+ }
690
+ else {
691
+ progress.views.current = Math.min(progress.views.current + 1, progress.views.total);
692
+ }
693
+ displayProgress(progress, spinner);
694
+ }
695
+ return result;
696
+ }
697
+ catch (error) {
698
+ console.error(`Error processing ${obj.type} ${obj.tablename}:`, error);
699
+ return null;
700
+ }
701
+ });
702
+ // バッチの完了を待機
703
+ const batchResults = await Promise.all(batchPromises);
704
+ processedResults.push(...batchResults);
705
+ }
706
+ // null値を除外してdefinitionsに追加
707
+ for (const result of processedResults) {
708
+ if (result) {
709
+ const { isTable, ...definition } = result;
710
+ definitions.push(definition);
711
+ }
712
+ }
713
+ return definitions;
714
+ }
715
+ /**
716
+ * CREATE TABLE DDLを生成(並行処理版)
717
+ */
718
+ async function generateCreateTableDDL(client, tableName, schemaName = 'public') {
719
+ // 全てのクエリを並行実行
720
+ const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult] = await Promise.all([
721
+ // カラム情報を取得
722
+ client.query(`
723
+ SELECT
724
+ column_name,
725
+ data_type,
726
+ character_maximum_length,
727
+ is_nullable,
728
+ column_default
729
+ FROM information_schema.columns
730
+ WHERE table_schema = $1
731
+ AND table_name = $2
732
+ ORDER BY ordinal_position
733
+ `, [schemaName, tableName]),
734
+ // 主キー情報を取得
735
+ client.query(`
736
+ SELECT column_name
737
+ FROM information_schema.table_constraints tc
738
+ JOIN information_schema.key_column_usage kcu
739
+ ON tc.constraint_name = kcu.constraint_name
740
+ WHERE tc.table_schema = $1
741
+ AND tc.table_name = $2
742
+ AND tc.constraint_type = 'PRIMARY KEY'
743
+ ORDER BY kcu.ordinal_position
744
+ `, [schemaName, tableName]),
745
+ // テーブルコメントを取得
746
+ client.query(`
747
+ SELECT obj_description(c.oid) as table_comment
748
+ FROM pg_class c
749
+ JOIN pg_namespace n ON c.relnamespace = n.oid
750
+ WHERE c.relname = $1 AND n.nspname = $2 AND c.relkind = 'r'
751
+ `, [tableName, schemaName]),
752
+ // カラムコメントを取得
753
+ client.query(`
754
+ SELECT
755
+ c.column_name,
756
+ pgd.description as column_comment
757
+ FROM information_schema.columns c
758
+ LEFT JOIN pg_class pgc ON pgc.relname = c.table_name
759
+ LEFT JOIN pg_namespace pgn ON pgn.oid = pgc.relnamespace
760
+ LEFT JOIN pg_attribute pga ON pga.attrelid = pgc.oid AND pga.attname = c.column_name
761
+ LEFT JOIN pg_description pgd ON pgd.objoid = pgc.oid AND pgd.objsubid = pga.attnum
762
+ WHERE c.table_schema = $1 AND c.table_name = $2
763
+ AND pgn.nspname = $1
764
+ ORDER BY c.ordinal_position
765
+ `, [schemaName, tableName]),
766
+ // UNIQUE制約を取得
767
+ client.query(`
768
+ SELECT
769
+ tc.constraint_name,
770
+ string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns
771
+ FROM information_schema.table_constraints tc
772
+ JOIN information_schema.key_column_usage kcu
773
+ ON tc.constraint_name = kcu.constraint_name
774
+ WHERE tc.table_schema = $1
775
+ AND tc.table_name = $2
776
+ AND tc.constraint_type = 'UNIQUE'
777
+ GROUP BY tc.constraint_name
778
+ ORDER BY tc.constraint_name
779
+ `, [schemaName, tableName])
780
+ ]);
781
+ const columnComments = new Map();
782
+ columnCommentsResult.rows.forEach(row => {
783
+ if (row.column_comment) {
784
+ columnComments.set(row.column_name, row.column_comment);
785
+ }
786
+ });
787
+ // テーブルコメントを先頭に追加(スキーマ名を含む)
788
+ let ddl = '';
789
+ if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
790
+ ddl += `-- ${tableCommentResult.rows[0].table_comment}\n`;
791
+ ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
792
+ }
793
+ else {
794
+ ddl += `-- Table: ${tableName}\n`;
795
+ ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
796
+ }
797
+ // CREATE TABLE文を生成
798
+ ddl += `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
799
+ const columnDefs = [];
800
+ for (const col of columnsResult.rows) {
801
+ let colDef = ` ${col.column_name} ${col.data_type.toUpperCase()}`;
802
+ // 長さ指定
803
+ if (col.character_maximum_length) {
804
+ colDef += `(${col.character_maximum_length})`;
805
+ }
806
+ // NOT NULL制約
807
+ if (col.is_nullable === 'NO') {
808
+ colDef += ' NOT NULL';
809
+ }
810
+ // デフォルト値
811
+ if (col.column_default) {
812
+ colDef += ` DEFAULT ${col.column_default}`;
813
+ }
814
+ columnDefs.push(colDef);
815
+ }
816
+ ddl += columnDefs.join(',\n');
817
+ // 主キー制約
818
+ if (primaryKeyResult.rows.length > 0) {
819
+ const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
820
+ ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
821
+ }
822
+ // UNIQUE制約をCREATE TABLE内に追加
823
+ for (const unique of uniqueConstraintResult.rows) {
824
+ ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
825
+ }
826
+ ddl += '\n);\n';
827
+ ddl += '\n';
828
+ // カラムコメントを追加(スキーマ名を含む)
829
+ if (columnComments.size > 0) {
830
+ ddl += '\n-- カラムコメント\n';
831
+ for (const [columnName, comment] of columnComments) {
832
+ ddl += `COMMENT ON COLUMN ${schemaName}.${tableName}.${columnName} IS '${comment}';\n`;
833
+ }
834
+ }
835
+ return ddl;
836
+ }
837
+ /**
838
+ * 定義をファイルに保存
839
+ */
840
+ async function saveDefinitionsByType(definitions, outputDir, separateDirectories = true) {
841
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
842
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
843
+ // 出力ディレクトリを作成
844
+ if (!fs.existsSync(outputDir)) {
845
+ fs.mkdirSync(outputDir, { recursive: true });
846
+ }
847
+ // 各タイプのディレクトリマッピング
848
+ const typeDirectories = separateDirectories ? {
849
+ table: path.join(outputDir, 'tables'), // テーブルもtables/フォルダに
850
+ view: path.join(outputDir, 'views'),
851
+ rls: path.join(outputDir, 'rls'),
852
+ function: path.join(outputDir, 'rpc'),
853
+ trigger: path.join(outputDir, 'rpc'), // トリガーもrpcディレクトリに
854
+ cron: path.join(outputDir, 'cron'),
855
+ type: path.join(outputDir, 'types')
856
+ } : {
857
+ // --no-separate の場合は全てルートに
858
+ table: outputDir,
859
+ view: outputDir,
860
+ rls: outputDir,
861
+ function: outputDir,
862
+ trigger: outputDir,
863
+ cron: outputDir,
864
+ type: outputDir
865
+ };
866
+ // 必要なディレクトリを事前作成
867
+ const requiredDirs = new Set(Object.values(typeDirectories));
868
+ for (const dir of requiredDirs) {
869
+ if (!fs.existsSync(dir)) {
870
+ fs.mkdirSync(dir, { recursive: true });
871
+ }
872
+ }
873
+ // 並行ファイル書き込み
874
+ const writePromises = definitions.map(async (def) => {
875
+ const targetDir = typeDirectories[def.type];
876
+ // ファイル名を決定(TypeとTriggerを区別しやすくする)
877
+ let fileName;
878
+ if (def.type === 'function') {
879
+ fileName = `fn_${def.name}.sql`;
880
+ }
881
+ else if (def.type === 'trigger') {
882
+ fileName = `trg_${def.name}.sql`;
883
+ }
884
+ else {
885
+ fileName = `${def.name}.sql`;
886
+ }
887
+ const filePath = path.join(targetDir, fileName);
888
+ // 最後に改行を追加
889
+ const ddlWithNewline = def.ddl.endsWith('\n') ? def.ddl : def.ddl + '\n';
890
+ // 非同期でファイル書き込み
891
+ const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
892
+ await fsPromises.writeFile(filePath, ddlWithNewline);
893
+ });
894
+ // 全ファイル書き込みの完了を待機
895
+ await Promise.all(writePromises);
896
+ // インデックスファイルを生成
897
+ await generateIndexFile(definitions, outputDir, separateDirectories);
898
+ }
899
+ /**
900
+ * データベースオブジェクトのインデックスファイルを生成
901
+ * AIが構造を理解しやすいように1行ずつリストアップ
902
+ */
903
+ async function generateIndexFile(definitions, outputDir, separateDirectories = true) {
904
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
905
+ const path = await Promise.resolve().then(() => __importStar(require('path')));
906
+ // タイプ別にグループ化
907
+ const groupedDefs = {
908
+ table: definitions.filter(def => def.type === 'table'),
909
+ view: definitions.filter(def => def.type === 'view'),
910
+ rls: definitions.filter(def => def.type === 'rls'),
911
+ function: definitions.filter(def => def.type === 'function'),
912
+ trigger: definitions.filter(def => def.type === 'trigger'),
913
+ cron: definitions.filter(def => def.type === 'cron'),
914
+ type: definitions.filter(def => def.type === 'type')
915
+ };
916
+ const typeLabels = {
917
+ table: 'Tables',
918
+ view: 'Views',
919
+ rls: 'RLS Policies',
920
+ function: 'Functions',
921
+ trigger: 'Triggers',
922
+ cron: 'Cron Jobs',
923
+ type: 'Custom Types'
924
+ };
925
+ // === 人間向け index.md ===
926
+ let indexContent = '# Database Schema Index\n\n';
927
+ // 統計サマリー
928
+ indexContent += '## Summary\n\n';
929
+ Object.entries(groupedDefs).forEach(([type, defs]) => {
930
+ if (defs.length > 0) {
931
+ indexContent += `- ${typeLabels[type]}: ${defs.length} objects\n`;
932
+ }
933
+ });
934
+ indexContent += '\n';
935
+ // ファイル一覧(md形式)
936
+ Object.entries(groupedDefs).forEach(([type, defs]) => {
937
+ if (defs.length === 0)
938
+ return;
939
+ const label = typeLabels[type];
940
+ indexContent += `## ${label}\n\n`;
941
+ defs.forEach(def => {
942
+ const folderPath = separateDirectories
943
+ ? (type === 'trigger' ? 'rpc' : type === 'table' ? 'tables' : type)
944
+ : '.';
945
+ // ファイル名を決定(Functions/Triggersに接頭辞を付ける)
946
+ let fileName;
947
+ if (def.type === 'function') {
948
+ fileName = `fn_${def.name}.sql`;
949
+ }
950
+ else if (def.type === 'trigger') {
951
+ fileName = `trg_${def.name}.sql`;
952
+ }
953
+ else {
954
+ fileName = `${def.name}.sql`;
955
+ }
956
+ const filePath = separateDirectories ? `${folderPath}/${fileName}` : fileName;
957
+ const commentText = def.comment ? ` - ${def.comment}` : '';
958
+ indexContent += `- [${def.name}](${filePath})${commentText}\n`;
959
+ });
960
+ indexContent += '\n';
961
+ });
962
+ // ディレクトリ構造(フォルダのみ)
963
+ indexContent += '## Directory Structure\n\n';
964
+ indexContent += '```\n';
965
+ indexContent += 'schemas/\n';
966
+ indexContent += '├── index.md\n';
967
+ indexContent += '├── llms.txt\n';
968
+ if (separateDirectories) {
969
+ Object.entries(groupedDefs).forEach(([type, defs]) => {
970
+ if (defs.length === 0)
971
+ return;
972
+ const folderName = type === 'trigger' ? 'rpc' : type === 'table' ? 'tables' : type;
973
+ indexContent += `└── ${folderName}/\n`;
974
+ });
975
+ }
976
+ indexContent += '```\n';
977
+ // === AI向け llms.txt ===
978
+ let llmsContent = 'Database Schema - Complete Objects Catalog\n\n';
979
+ // Summary section
980
+ llmsContent += 'SUMMARY\n';
981
+ Object.entries(groupedDefs).forEach(([type, defs]) => {
982
+ if (defs.length > 0) {
983
+ llmsContent += `${typeLabels[type]}: ${defs.length}\n`;
984
+ }
985
+ });
986
+ llmsContent += '\n';
987
+ // Flat list for AI processing (single format)
988
+ llmsContent += 'OBJECTS\n';
989
+ definitions.forEach(def => {
990
+ const folderPath = separateDirectories
991
+ ? (def.type === 'trigger' ? 'rpc' : def.type === 'table' ? 'tables' : def.type)
992
+ : '.';
993
+ // ファイル名を決定(Functions/Triggersに接頭辞を付ける)
994
+ let fileName;
995
+ if (def.type === 'function') {
996
+ fileName = `fn_${def.name}.sql`;
997
+ }
998
+ else if (def.type === 'trigger') {
999
+ fileName = `trg_${def.name}.sql`;
1000
+ }
1001
+ else {
1002
+ fileName = `${def.name}.sql`;
1003
+ }
1004
+ const filePath = separateDirectories ? `${folderPath}/${fileName}` : fileName;
1005
+ const commentText = def.comment ? `:${def.comment}` : '';
1006
+ llmsContent += `${def.type}:${def.name}:${filePath}${commentText}\n`;
1007
+ });
1008
+ // ファイル保存
1009
+ const indexPath = path.join(outputDir, 'index.md');
1010
+ const llmsPath = path.join(outputDir, 'llms.txt');
1011
+ fs.writeFileSync(indexPath, indexContent);
1012
+ fs.writeFileSync(llmsPath, llmsContent);
1013
+ }
1014
+ /**
1015
+ * 定義を分類して出力
1016
+ */
1017
+ async function extractDefinitions(options) {
1018
+ const { connectionString, outputDir, separateDirectories = true, tablesOnly = false, viewsOnly = false, all = false, tablePattern = '*', force = false, schemas = ['public'] } = options;
1019
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
1020
+ const readline = await Promise.resolve().then(() => __importStar(require('readline')));
1021
+ // 上書き確認
1022
+ if (!force && fs.existsSync(outputDir)) {
1023
+ const files = fs.readdirSync(outputDir);
1024
+ if (files.length > 0) {
1025
+ const rl = readline.createInterface({
1026
+ input: process.stdin,
1027
+ output: process.stdout
1028
+ });
1029
+ const answer = await new Promise((resolve) => {
1030
+ rl.question(`Directory "${outputDir}" already exists and contains files. Overwrite? (y/N): `, resolve);
1031
+ });
1032
+ rl.close();
1033
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
1034
+ console.log('Operation cancelled.');
1035
+ return;
1036
+ }
1037
+ }
1038
+ }
1039
+ // スピナーを動的インポート
1040
+ const { default: ora } = await Promise.resolve().then(() => __importStar(require('ora')));
1041
+ const spinner = ora('Connecting to database...').start();
1042
+ const client = new pg_1.Client({ connectionString });
1043
+ try {
1044
+ await client.connect();
1045
+ spinner.text = 'Connected to database';
1046
+ let allDefinitions = [];
1047
+ // 進行状況トラッカーを初期化
1048
+ const progress = {
1049
+ tables: { current: 0, total: 0 },
1050
+ views: { current: 0, total: 0 },
1051
+ rls: { current: 0, total: 0 },
1052
+ functions: { current: 0, total: 0 },
1053
+ triggers: { current: 0, total: 0 },
1054
+ cronJobs: { current: 0, total: 0 },
1055
+ customTypes: { current: 0, total: 0 }
1056
+ };
1057
+ if (all) {
1058
+ // 事前に各オブジェクトの総数を取得
1059
+ spinner.text = 'Counting database objects...';
1060
+ // テーブル・ビューの総数を取得
1061
+ const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
1062
+ const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
1063
+ progress.tables.total = parseInt(tablesCountResult.rows[0].count);
1064
+ progress.views.total = parseInt(viewsCountResult.rows[0].count);
1065
+ // RLS ポリシーの総数を取得(テーブル単位)
1066
+ try {
1067
+ const rlsCountResult = await client.query(`
1068
+ SELECT COUNT(DISTINCT tablename) as count
1069
+ FROM pg_policies
1070
+ WHERE schemaname = 'public'
1071
+ `);
1072
+ progress.rls.total = parseInt(rlsCountResult.rows[0].count);
1073
+ }
1074
+ catch (error) {
1075
+ progress.rls.total = 0;
1076
+ }
1077
+ // 関数の総数を取得
1078
+ const functionsCountResult = await client.query(`
1079
+ SELECT COUNT(*) as count
1080
+ FROM pg_proc p
1081
+ JOIN pg_namespace n ON p.pronamespace = n.oid
1082
+ WHERE n.nspname = 'public' AND p.prokind IN ('f', 'p')
1083
+ `);
1084
+ progress.functions.total = parseInt(functionsCountResult.rows[0].count);
1085
+ // トリガーの総数を取得
1086
+ const triggersCountResult = await client.query(`
1087
+ SELECT COUNT(*) as count
1088
+ FROM pg_trigger t
1089
+ JOIN pg_class c ON t.tgrelid = c.oid
1090
+ JOIN pg_namespace n ON c.relnamespace = n.oid
1091
+ WHERE n.nspname = 'public' AND NOT t.tgisinternal
1092
+ `);
1093
+ progress.triggers.total = parseInt(triggersCountResult.rows[0].count);
1094
+ // Cronジョブの総数を取得
1095
+ try {
1096
+ const cronCountResult = await client.query('SELECT COUNT(*) as count FROM cron.job');
1097
+ progress.cronJobs.total = parseInt(cronCountResult.rows[0].count);
1098
+ }
1099
+ catch (error) {
1100
+ progress.cronJobs.total = 0;
1101
+ }
1102
+ // カスタム型の総数を取得
1103
+ const typesCountResult = await client.query(`
1104
+ SELECT COUNT(*) as count
1105
+ FROM pg_type t
1106
+ JOIN pg_namespace n ON t.typnamespace = n.oid
1107
+ WHERE n.nspname = 'public'
1108
+ AND t.typtype IN ('e', 'c', 'd')
1109
+ AND NOT EXISTS (
1110
+ SELECT 1 FROM pg_class c
1111
+ WHERE c.relname = t.typname
1112
+ AND c.relnamespace = n.oid
1113
+ AND c.relkind IN ('r', 'v', 'i', 'S', 'c')
1114
+ )
1115
+ AND t.typname NOT LIKE 'pg_%'
1116
+ AND t.typname NOT LIKE '_%'
1117
+ `);
1118
+ progress.customTypes.total = parseInt(typesCountResult.rows[0].count);
1119
+ // --all フラグが指定された場合は全てのオブジェクトを取得(順次処理)
1120
+ const tables = await fetchTableDefinitions(client, spinner, progress, schemas);
1121
+ const rlsPolicies = await fetchRlsPolicies(client, spinner, progress, schemas);
1122
+ const functions = await fetchFunctions(client, spinner, progress, schemas);
1123
+ const triggers = await fetchTriggers(client, spinner, progress, schemas);
1124
+ const cronJobs = await fetchCronJobs(client, spinner, progress);
1125
+ const customTypes = await fetchCustomTypes(client, spinner, progress, schemas);
1126
+ allDefinitions = [
1127
+ ...tables,
1128
+ ...rlsPolicies,
1129
+ ...functions,
1130
+ ...triggers,
1131
+ ...cronJobs,
1132
+ ...customTypes
1133
+ ];
1134
+ }
1135
+ else {
1136
+ // 従来の処理(テーブル・ビューのみ)
1137
+ // テーブル・ビューの総数を取得
1138
+ const tablesCountResult = await client.query('SELECT COUNT(*) as count FROM pg_tables WHERE schemaname = \'public\'');
1139
+ const viewsCountResult = await client.query('SELECT COUNT(*) as count FROM pg_views WHERE schemaname = \'public\'');
1140
+ progress.tables.total = parseInt(tablesCountResult.rows[0].count);
1141
+ progress.views.total = parseInt(viewsCountResult.rows[0].count);
1142
+ const definitions = await fetchTableDefinitions(client, spinner, progress, schemas);
1143
+ if (tablesOnly) {
1144
+ allDefinitions = definitions.filter(def => def.type === 'table');
1145
+ }
1146
+ else if (viewsOnly) {
1147
+ allDefinitions = definitions.filter(def => def.type === 'view');
1148
+ }
1149
+ else {
1150
+ allDefinitions = definitions;
1151
+ }
1152
+ }
1153
+ // パターンマッチング
1154
+ if (tablePattern !== '*') {
1155
+ const regex = new RegExp(tablePattern.replace(/\*/g, '.*'));
1156
+ allDefinitions = allDefinitions.filter(def => regex.test(def.name));
1157
+ }
1158
+ // 定義を保存
1159
+ spinner.text = 'Saving definitions to files...';
1160
+ await saveDefinitionsByType(allDefinitions, outputDir, separateDirectories);
1161
+ // 統計を表示
1162
+ const counts = {
1163
+ table: allDefinitions.filter(def => def.type === 'table').length,
1164
+ view: allDefinitions.filter(def => def.type === 'view').length,
1165
+ rls: allDefinitions.filter(def => def.type === 'rls').length,
1166
+ function: allDefinitions.filter(def => def.type === 'function').length,
1167
+ trigger: allDefinitions.filter(def => def.type === 'trigger').length,
1168
+ cron: allDefinitions.filter(def => def.type === 'cron').length,
1169
+ type: allDefinitions.filter(def => def.type === 'type').length
1170
+ };
1171
+ // 進捗表示を停止
1172
+ stopProgressDisplay();
1173
+ spinner.succeed(`Extraction completed: ${outputDir}`);
1174
+ if (counts.table > 0)
1175
+ console.log(` Tables: ${counts.table}`);
1176
+ if (counts.view > 0)
1177
+ console.log(` Views: ${counts.view}`);
1178
+ if (counts.rls > 0)
1179
+ console.log(` RLS Policies: ${counts.rls}`);
1180
+ if (counts.function > 0)
1181
+ console.log(` Functions: ${counts.function}`);
1182
+ if (counts.trigger > 0)
1183
+ console.log(` Triggers: ${counts.trigger}`);
1184
+ if (counts.cron > 0)
1185
+ console.log(` Cron Jobs: ${counts.cron}`);
1186
+ if (counts.type > 0)
1187
+ console.log(` Custom Types: ${counts.type}`);
1188
+ console.log('');
1189
+ }
1190
+ catch (error) {
1191
+ // 進捗表示を停止(エラー時)
1192
+ stopProgressDisplay();
1193
+ spinner.fail('Extraction failed');
1194
+ console.error('Error:', error);
1195
+ throw error;
1196
+ }
1197
+ finally {
1198
+ try {
1199
+ await client.end();
1200
+ }
1201
+ catch (closeError) {
1202
+ // データベース接続の終了エラーは無視(既に切断されている場合など)
1203
+ }
1204
+ }
1205
+ }