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.
- package/README.md +228 -22
- package/dist/bin/helptext.js +106 -20
- package/dist/bin/supatool.js +172 -21
- package/dist/generator/client.js +9 -0
- package/dist/generator/crudGenerator.js +57 -0
- package/dist/generator/docGenerator.js +64 -0
- package/dist/generator/rlsGenerator.js +111 -0
- package/dist/generator/schemaCrudGenerator.js +560 -0
- package/dist/generator/sqlGenerator.js +100 -0
- package/dist/generator/typeGenerator.js +55 -0
- package/dist/generator/types.js +2 -0
- package/dist/parser/modelParser.js +18 -0
- package/dist/sync/config.js +98 -0
- package/dist/sync/definitionExtractor.js +1205 -0
- package/dist/sync/fetchRemoteSchemas.js +369 -0
- package/dist/sync/generateMigration.js +276 -0
- package/dist/sync/index.js +22 -0
- package/dist/sync/migrationRunner.js +271 -0
- package/dist/sync/parseLocalSchemas.js +97 -0
- package/dist/sync/sync.js +208 -0
- package/dist/sync/utils.js +52 -0
- package/dist/sync/writeSchema.js +161 -0
- package/package.json +18 -3
@@ -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
|
+
}
|