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,369 @@
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.fetchRemoteSchemas = fetchRemoteSchemas;
37
+ const pg_1 = require("pg");
38
+ const dns = __importStar(require("dns"));
39
+ const util_1 = require("util");
40
+ const dnsLookup = (0, util_1.promisify)(dns.lookup);
41
+ /**
42
+ * DDL文字列を正規化(空白・改行・タブを統一)
43
+ */
44
+ function normalizeDDL(ddl) {
45
+ return ddl
46
+ .replace(/\s+/g, ' ') // 連続する空白文字を1つのスペースに
47
+ .replace(/;\s+/g, ';\n') // セミコロン後に改行
48
+ .trim(); // 前後の空白を削除
49
+ }
50
+ /**
51
+ * リモートSupabaseからスキーマを取得
52
+ */
53
+ async function fetchRemoteSchemas(connectionString) {
54
+ const schemas = {};
55
+ // console.log('データベースに接続中...');
56
+ try {
57
+ // 接続文字列の基本チェック
58
+ if (!connectionString || !connectionString.startsWith('postgresql://')) {
59
+ throw new Error('無効な接続文字列です。postgresql://で始まる形式で指定してください。');
60
+ }
61
+ // URL解析して接続先を表示
62
+ const url = new URL(connectionString);
63
+ console.log(`接続先: ${url.hostname}:${url.port}`);
64
+ console.log(`ユーザー: ${url.username}`);
65
+ // パスワードに特殊文字が含まれている場合の対処
66
+ const decodedPassword = decodeURIComponent(url.password || '');
67
+ // 接続文字列を再構築(パスワードを適切にエンコード)
68
+ const encodedPassword = encodeURIComponent(decodedPassword);
69
+ // Session poolerのモードを明示的に指定
70
+ let reconstructedConnectionString = `postgresql://${url.username}:${encodedPassword}@${url.hostname}:${url.port}${url.pathname}`;
71
+ // Session poolingモードのパラメータを追加
72
+ const searchParams = new URLSearchParams(url.search);
73
+ searchParams.set('sslmode', 'require');
74
+ searchParams.set('application_name', 'supatool');
75
+ reconstructedConnectionString += `?${searchParams.toString()}`;
76
+ // IPv4を優先に変更
77
+ dns.setDefaultResultOrder('ipv4first');
78
+ // DNS解決テスト
79
+ try {
80
+ const addresses = await dns.promises.lookup(url.hostname, { all: true });
81
+ }
82
+ catch (dnsError) {
83
+ console.error('❌ DNS解決に失敗しました');
84
+ throw dnsError;
85
+ }
86
+ // Session pooler用の設定
87
+ const clientConfig = {
88
+ connectionString: reconstructedConnectionString,
89
+ ssl: {
90
+ rejectUnauthorized: false
91
+ },
92
+ // Session pooler用の追加設定
93
+ statement_timeout: 30000,
94
+ query_timeout: 30000,
95
+ connectionTimeoutMillis: 10000,
96
+ idleTimeoutMillis: 10000
97
+ };
98
+ console.log('Session pooler経由で接続中...');
99
+ let client;
100
+ try {
101
+ client = new pg_1.Client(clientConfig);
102
+ await client.connect();
103
+ }
104
+ catch (sslError) {
105
+ if (sslError instanceof Error && (sslError.message.includes('SASL') ||
106
+ sslError.message.includes('SCRAM') ||
107
+ sslError.message.includes('certificate') ||
108
+ sslError.message.includes('SELF_SIGNED_CERT'))) {
109
+ console.log('SSL接続不可のため、SSL無効化で再試行中...');
110
+ // SSL無効化バージョンを試行
111
+ const noSslConnectionString = reconstructedConnectionString.replace('sslmode=require', 'sslmode=disable');
112
+ const noSslConfig = {
113
+ ...clientConfig,
114
+ connectionString: noSslConnectionString,
115
+ ssl: false
116
+ };
117
+ try {
118
+ client = new pg_1.Client(noSslConfig);
119
+ await client.connect();
120
+ console.log('SSL無効化での接続に成功');
121
+ }
122
+ catch (noSslError) {
123
+ if (noSslError instanceof Error && (noSslError.message.includes('SASL') || noSslError.message.includes('SCRAM'))) {
124
+ console.log('Session poolerでSASLエラー継続。Direct connectionで再試行中...');
125
+ // Direct connection (ポート5432) で再試行
126
+ const directConnectionString = noSslConnectionString.replace('pooler.supabase.com:5432', 'supabase.co:5432');
127
+ const directConfig = {
128
+ ...noSslConfig,
129
+ connectionString: directConnectionString
130
+ };
131
+ client = new pg_1.Client(directConfig);
132
+ await client.connect();
133
+ console.log('Direct connectionでの接続に成功');
134
+ }
135
+ else {
136
+ throw noSslError;
137
+ }
138
+ }
139
+ }
140
+ else {
141
+ throw sslError;
142
+ }
143
+ }
144
+ // 接続テストクエリ
145
+ const testResult = await client.query('SELECT version()');
146
+ // テーブル一覧を取得
147
+ const tablesResult = await client.query(`
148
+ SELECT tablename
149
+ FROM pg_tables
150
+ WHERE schemaname = 'public'
151
+ ORDER BY tablename
152
+ `);
153
+ console.log(`リモートテーブル:${tablesResult.rows.length}`);
154
+ // ローディングアニメーション用
155
+ const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
156
+ let spinnerIndex = 0;
157
+ const startTime = Date.now();
158
+ const spinnerInterval = setInterval(() => {
159
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
160
+ process.stdout.write(`\r${spinner[spinnerIndex]} スキーマ取得中... ${elapsed}s`);
161
+ spinnerIndex = (spinnerIndex + 1) % spinner.length;
162
+ }, 100);
163
+ // 各テーブルのスキーマ情報を取得
164
+ for (const row of tablesResult.rows) {
165
+ const tableName = row.tablename;
166
+ // テーブルの最終更新時刻を取得(複数の方法を試行)
167
+ let timestamp = Math.floor(Date.now() / 1000); // デフォルト値
168
+ // pg_stat_user_tablesから取得
169
+ try {
170
+ const tableStatsResult = await client.query(`
171
+ SELECT
172
+ EXTRACT(EPOCH FROM GREATEST(
173
+ COALESCE(last_vacuum, '1970-01-01'::timestamp),
174
+ COALESCE(last_autovacuum, '1970-01-01'::timestamp),
175
+ COALESCE(last_analyze, '1970-01-01'::timestamp),
176
+ COALESCE(last_autoanalyze, '1970-01-01'::timestamp)
177
+ ))::bigint as last_updated
178
+ FROM pg_stat_user_tables
179
+ WHERE relname = $1 AND schemaname = 'public'
180
+ `, [tableName]);
181
+ if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
182
+ timestamp = tableStatsResult.rows[0].last_updated;
183
+ }
184
+ else {
185
+ // デフォルトタイムスタンプ(十分に古い固定値)
186
+ timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
187
+ }
188
+ }
189
+ catch (statsError) {
190
+ // デフォルトタイムスタンプ(十分に古い固定値)
191
+ timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
192
+ }
193
+ // カラム情報を取得
194
+ const columnsResult = await client.query(`
195
+ SELECT
196
+ column_name,
197
+ data_type,
198
+ character_maximum_length,
199
+ is_nullable,
200
+ column_default
201
+ FROM information_schema.columns
202
+ WHERE table_schema = 'public'
203
+ AND table_name = $1
204
+ ORDER BY ordinal_position
205
+ `, [tableName]);
206
+ // 主キー情報を取得
207
+ const primaryKeyResult = await client.query(`
208
+ SELECT column_name
209
+ FROM information_schema.table_constraints tc
210
+ JOIN information_schema.key_column_usage kcu
211
+ ON tc.constraint_name = kcu.constraint_name
212
+ WHERE tc.table_schema = 'public'
213
+ AND tc.table_name = $1
214
+ AND tc.constraint_type = 'PRIMARY KEY'
215
+ ORDER BY kcu.ordinal_position
216
+ `, [tableName]);
217
+ // CREATE TABLE文を生成
218
+ let ddl = `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
219
+ const columnDefs = [];
220
+ for (const col of columnsResult.rows) {
221
+ let colDef = ` ${col.column_name} ${col.data_type.toUpperCase()}`;
222
+ // 長さ指定
223
+ if (col.character_maximum_length) {
224
+ colDef += `(${col.character_maximum_length})`;
225
+ }
226
+ // NOT NULL制約
227
+ if (col.is_nullable === 'NO') {
228
+ colDef += ' NOT NULL';
229
+ }
230
+ // デフォルト値
231
+ if (col.column_default) {
232
+ colDef += ` DEFAULT ${col.column_default}`;
233
+ }
234
+ columnDefs.push(colDef);
235
+ }
236
+ ddl += columnDefs.join(',\n');
237
+ // 主キー制約
238
+ if (primaryKeyResult.rows.length > 0) {
239
+ const pkColumns = primaryKeyResult.rows.map(row => row.column_name);
240
+ ddl += `,\n PRIMARY KEY (${pkColumns.join(', ')})`;
241
+ }
242
+ // UNIQUE制約を取得してCREATE TABLE内に含める
243
+ const uniqueConstraintResult = await client.query(`
244
+ SELECT
245
+ tc.constraint_name,
246
+ string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns
247
+ FROM information_schema.table_constraints tc
248
+ JOIN information_schema.key_column_usage kcu
249
+ ON tc.constraint_name = kcu.constraint_name
250
+ WHERE tc.table_schema = 'public'
251
+ AND tc.table_name = $1
252
+ AND tc.constraint_type = 'UNIQUE'
253
+ GROUP BY tc.constraint_name
254
+ ORDER BY tc.constraint_name
255
+ `, [tableName]);
256
+ // UNIQUE制約をCREATE TABLE内に追加
257
+ for (const unique of uniqueConstraintResult.rows) {
258
+ ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
259
+ }
260
+ // CHECK制約は一時的に無効化(NOT NULL制約と重複するため)
261
+ // const checkConstraintResult = await client.query(`...`);
262
+ // CHECK制約をCREATE TABLE内に追加は一時的に無効化
263
+ // for (const check of checkConstraintResult.rows) {
264
+ // ddl += `,\n CONSTRAINT ${check.constraint_name} CHECK ${check.check_clause}`;
265
+ // }
266
+ ddl += '\n);';
267
+ // 外部キー制約のみを別途生成
268
+ const foreignKeyResult = await client.query(`
269
+ SELECT
270
+ tc.constraint_name,
271
+ kcu.column_name,
272
+ ccu.table_name AS foreign_table_name,
273
+ ccu.column_name AS foreign_column_name,
274
+ rc.delete_rule,
275
+ rc.update_rule
276
+ FROM information_schema.table_constraints tc
277
+ JOIN information_schema.key_column_usage kcu
278
+ ON tc.constraint_name = kcu.constraint_name
279
+ JOIN information_schema.constraint_column_usage ccu
280
+ ON ccu.constraint_name = tc.constraint_name
281
+ JOIN information_schema.referential_constraints rc
282
+ ON tc.constraint_name = rc.constraint_name
283
+ WHERE tc.table_schema = 'public'
284
+ AND tc.table_name = $1
285
+ AND tc.constraint_type = 'FOREIGN KEY'
286
+ ORDER BY tc.constraint_name
287
+ `, [tableName]);
288
+ // 外部キー制約のみをALTER TABLE文として追加
289
+ for (const fk of foreignKeyResult.rows) {
290
+ ddl += `\n\nALTER TABLE ${tableName} ADD CONSTRAINT ${fk.constraint_name}`;
291
+ ddl += ` FOREIGN KEY (${fk.column_name})`;
292
+ ddl += ` REFERENCES ${fk.foreign_table_name} (${fk.foreign_column_name})`;
293
+ if (fk.delete_rule && fk.delete_rule !== 'NO ACTION') {
294
+ ddl += ` ON DELETE ${fk.delete_rule}`;
295
+ }
296
+ if (fk.update_rule && fk.update_rule !== 'NO ACTION') {
297
+ ddl += ` ON UPDATE ${fk.update_rule}`;
298
+ }
299
+ ddl += ';';
300
+ }
301
+ // インデックス情報を取得
302
+ const indexResult = await client.query(`
303
+ SELECT
304
+ indexname,
305
+ indexdef
306
+ FROM pg_indexes
307
+ WHERE schemaname = 'public'
308
+ AND tablename = $1
309
+ AND indexname NOT LIKE '%_pkey'
310
+ AND indexname NOT IN (
311
+ SELECT tc.constraint_name
312
+ FROM information_schema.table_constraints tc
313
+ WHERE tc.table_name = $1 AND tc.constraint_type = 'UNIQUE'
314
+ )
315
+ ORDER BY indexname
316
+ `, [tableName]);
317
+ // インデックスを追加
318
+ for (const idx of indexResult.rows) {
319
+ ddl += `\n\n${idx.indexdef};`;
320
+ }
321
+ // DDLを元の形式で保存
322
+ schemas[tableName] = {
323
+ ddl,
324
+ timestamp
325
+ };
326
+ }
327
+ // ローディングアニメーション停止
328
+ clearInterval(spinnerInterval);
329
+ const totalTime = Math.floor((Date.now() - startTime) / 1000);
330
+ process.stdout.write(`\rSchema fetch completed (${totalTime}s) \n`);
331
+ await client.end();
332
+ }
333
+ catch (error) {
334
+ console.error('❌ Remote schema fetch error:');
335
+ if (error instanceof Error) {
336
+ if (error.message.includes('ENOTFOUND')) {
337
+ console.error('🌐 DNS resolution error: Host not found');
338
+ console.error('💡 Check:');
339
+ console.error(' - Is your internet connection working?');
340
+ console.error(' - Is your Supabase project connection string correct?');
341
+ }
342
+ else if (error.message.includes('authentication failed')) {
343
+ console.error('🔐 Authentication error: Incorrect username or password');
344
+ }
345
+ else if (error.message.includes('SASL') || error.message.includes('SCRAM')) {
346
+ console.error('🔐 SASL authentication error: Password or connection settings issue');
347
+ console.error('💡 Session pooler connection checklist:');
348
+ console.error(' - Reset database password in Supabase dashboard');
349
+ console.error(' - Update connection string with new password');
350
+ console.error(' - URL encode special characters in password if needed');
351
+ console.error(' - Ensure project is not paused');
352
+ console.error(' - Supabase Dashboard → Settings → Database → Reset database password');
353
+ }
354
+ else if (error.message.includes('connect ETIMEDOUT')) {
355
+ console.error('⏰ Connection timeout: Cannot reach database server');
356
+ }
357
+ else {
358
+ console.error(`🐛 ${error.message}`);
359
+ }
360
+ }
361
+ console.error('\n📖 Session pooler connection setup:');
362
+ console.error('1. Supabase Dashboard → Settings → Database');
363
+ console.error('2. Select "Session pooler" tab');
364
+ console.error('3. Reset password (Reset database password)');
365
+ console.error('4. Set new connection string in .env.local');
366
+ throw error;
367
+ }
368
+ return schemas;
369
+ }
@@ -0,0 +1,276 @@
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.generateMigrationFile = generateMigrationFile;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const diff_1 = require("diff");
40
+ /**
41
+ * DDL差分からALTER TABLE文を生成
42
+ */
43
+ function generateAlterStatements(tableName, fromDdl, toDdl) {
44
+ const statements = [];
45
+ const diff = (0, diff_1.diffLines)(fromDdl, toDdl);
46
+ // デバッグ情報削除 - シンプルな表示
47
+ // カラム名変更の検出用
48
+ const removedColumns = [];
49
+ const addedColumns = [];
50
+ for (const part of diff) {
51
+ if (part.added) {
52
+ // 追加された行をALTER TABLE文に変換
53
+ const lines = part.value.split('\n').filter(line => line.trim());
54
+ for (const line of lines) {
55
+ const trimmed = line.trim();
56
+ // カラム定義の追加を検出
57
+ if (trimmed.includes(' ') && !trimmed.startsWith('CREATE') && !trimmed.startsWith('PRIMARY') && !trimmed.startsWith('CONSTRAINT')) {
58
+ const columnDef = trimmed.replace(/,$/, '').trim();
59
+ const columnMatch = columnDef.match(/^(\w+)\s+(.+)$/);
60
+ if (columnMatch) {
61
+ const columnName = columnMatch[1];
62
+ const columnType = columnMatch[2];
63
+ addedColumns.push({ name: columnName, definition: columnType });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ else if (part.removed) {
69
+ // 削除された行をALTER TABLE文に変換
70
+ const lines = part.value.split('\n').filter(line => line.trim());
71
+ for (const line of lines) {
72
+ const trimmed = line.trim();
73
+ // カラム定義の削除を検出
74
+ if (trimmed.includes(' ') && !trimmed.startsWith('CREATE') && !trimmed.startsWith('PRIMARY') && !trimmed.startsWith('CONSTRAINT')) {
75
+ const columnDef = trimmed.replace(/,$/, '').trim();
76
+ const columnMatch = columnDef.match(/^(\w+)\s+(.+)$/);
77
+ if (columnMatch) {
78
+ const columnName = columnMatch[1];
79
+ const columnType = columnMatch[2];
80
+ removedColumns.push({ name: columnName, definition: columnType });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ // カラム名変更の検出(型定義が同じで名前が違う場合)
87
+ const renames = [];
88
+ for (const removed of removedColumns) {
89
+ const matching = addedColumns.find(added => added.definition === removed.definition);
90
+ if (matching) {
91
+ renames.push({ from: removed.name, to: matching.name });
92
+ // 処理済みとしてマーク
93
+ removedColumns.splice(removedColumns.indexOf(removed), 1);
94
+ addedColumns.splice(addedColumns.indexOf(matching), 1);
95
+ }
96
+ }
97
+ // デバッグログ削除 - シンプルな表示
98
+ // RENAME COLUMN文を生成
99
+ for (const rename of renames) {
100
+ statements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${rename.from} TO ${rename.to};`);
101
+ }
102
+ // 残りの削除されたカラム
103
+ for (const removed of removedColumns) {
104
+ statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${removed.name};`);
105
+ }
106
+ // 残りの追加されたカラム
107
+ for (const added of addedColumns) {
108
+ statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${added.name} ${added.definition};`);
109
+ }
110
+ return statements;
111
+ }
112
+ /**
113
+ * マイグレーションファイルを生成
114
+ */
115
+ async function generateMigrationFile(tableName, fromDdl, toDdl, projectDir = '.') {
116
+ const migrationDir = path.join(projectDir, 'supabase', 'migrations');
117
+ // migrations ディレクトリを作成
118
+ if (!fs.existsSync(migrationDir)) {
119
+ fs.mkdirSync(migrationDir, { recursive: true });
120
+ }
121
+ // ALTER TABLE文を生成
122
+ const alterStatements = generateAlterStatements(tableName, fromDdl, toDdl);
123
+ if (alterStatements.length === 0) {
124
+ return await generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir);
125
+ }
126
+ // タイムスタンプでファイル名を生成
127
+ const now = new Date();
128
+ const timestamp = now.toISOString()
129
+ .replace(/[-:]/g, '')
130
+ .replace(/\..+/, '')
131
+ .replace('T', '');
132
+ const filename = `${timestamp}_update_${tableName}.sql`;
133
+ const filepath = path.join(migrationDir, filename);
134
+ // マイグレーションファイルの内容を生成
135
+ const content = `-- Migration generated by supatool
136
+ -- Table: ${tableName}
137
+ -- Generated at: ${now.toISOString()}
138
+
139
+ ${alterStatements.join('\n')}
140
+ `;
141
+ // ファイルを書き込み
142
+ fs.writeFileSync(filepath, content, 'utf-8');
143
+ console.log(`マイグレーションファイル生成: ${filename}`);
144
+ return filepath;
145
+ }
146
+ /**
147
+ * DDLからカラム定義を抽出
148
+ */
149
+ function extractColumns(ddl) {
150
+ const columns = [];
151
+ // CREATE TABLE部分を抽出
152
+ const createTableMatch = ddl.match(/CREATE TABLE[^(]*\((.*)\);?/is);
153
+ if (!createTableMatch) {
154
+ return columns;
155
+ }
156
+ const tableContent = createTableMatch[1];
157
+ // カラム定義とCONSTRAINTを分離
158
+ const parts = tableContent.split(',').map(part => part.trim());
159
+ for (const part of parts) {
160
+ // CONSTRAINTやPRIMARY KEYは除外
161
+ if (part.match(/^(PRIMARY|CONSTRAINT|UNIQUE|FOREIGN|CHECK)/i)) {
162
+ continue;
163
+ }
164
+ // カラム名とデータ型を分離
165
+ const columnMatch = part.trim().match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s+(.+)$/);
166
+ if (columnMatch) {
167
+ const columnName = columnMatch[1];
168
+ const columnType = columnMatch[2];
169
+ columns.push({ name: columnName, definition: columnType });
170
+ }
171
+ }
172
+ return columns;
173
+ }
174
+ /**
175
+ * 差分からカラム変更を解析
176
+ */
177
+ function analyzeDiffForTemplate(fromDdl, toDdl) {
178
+ // 各DDLからカラムを抽出
179
+ const fromColumns = extractColumns(fromDdl);
180
+ const toColumns = extractColumns(toDdl);
181
+ // 削除されたカラム(FROMにあってTOにない)
182
+ const removedColumns = fromColumns.filter(fromCol => !toColumns.some(toCol => toCol.name === fromCol.name));
183
+ // 追加されたカラム(TOにあってFROMにない)
184
+ const addedColumns = toColumns.filter(toCol => !fromColumns.some(fromCol => fromCol.name === toCol.name));
185
+ return { removedColumns, addedColumns };
186
+ }
187
+ /**
188
+ * 手動マイグレーションテンプレートを生成
189
+ */
190
+ async function generateManualMigrationTemplate(tableName, fromDdl, toDdl, projectDir) {
191
+ const migrationDir = path.join(projectDir, 'supabase', 'migrations');
192
+ if (!fs.existsSync(migrationDir)) {
193
+ fs.mkdirSync(migrationDir, { recursive: true });
194
+ }
195
+ // 差分を解析
196
+ const { removedColumns, addedColumns } = analyzeDiffForTemplate(fromDdl, toDdl);
197
+ // タイムスタンプでファイル名を生成
198
+ const now = new Date();
199
+ const timestamp = now.toISOString()
200
+ .replace(/[-:]/g, '')
201
+ .replace(/\..+/, '')
202
+ .replace('T', '');
203
+ const filename = `${timestamp}_manual_update_${tableName}.sql`;
204
+ const filepath = path.join(migrationDir, filename);
205
+ // 実際の変更を基にテンプレートを生成
206
+ let migrationStatements = [];
207
+ // カラム名変更の可能性を検出
208
+ const potentialRenames = [];
209
+ for (const removed of removedColumns) {
210
+ const matching = addedColumns.find(added => added.definition === removed.definition);
211
+ if (matching) {
212
+ potentialRenames.push({
213
+ from: removed.name,
214
+ to: matching.name,
215
+ definition: removed.definition
216
+ });
217
+ }
218
+ }
219
+ // 残りの削除・追加
220
+ const remainingRemoved = removedColumns.filter(r => !potentialRenames.some(p => p.from === r.name));
221
+ const remainingAdded = addedColumns.filter(a => !potentialRenames.some(p => p.to === a.name));
222
+ // テンプレート文を生成(DROP → ADD → RENAME の順序)
223
+ if (remainingRemoved.length > 0) {
224
+ migrationStatements.push('-- Column removals:');
225
+ for (const removed of remainingRemoved) {
226
+ migrationStatements.push(`ALTER TABLE ${tableName} DROP COLUMN ${removed.name};`);
227
+ }
228
+ }
229
+ if (remainingAdded.length > 0) {
230
+ if (migrationStatements.length > 0)
231
+ migrationStatements.push('');
232
+ migrationStatements.push('-- Column additions:');
233
+ for (const added of remainingAdded) {
234
+ migrationStatements.push(`ALTER TABLE ${tableName} ADD COLUMN ${added.name} ${added.definition};`);
235
+ }
236
+ }
237
+ if (potentialRenames.length > 0) {
238
+ if (migrationStatements.length > 0)
239
+ migrationStatements.push('');
240
+ migrationStatements.push('-- Possible column renames:');
241
+ for (const rename of potentialRenames) {
242
+ migrationStatements.push(`ALTER TABLE ${tableName} RENAME COLUMN ${rename.from} TO ${rename.to};`);
243
+ }
244
+ }
245
+ // フォールバック: 何も検出されなかった場合
246
+ if (migrationStatements.length === 0) {
247
+ migrationStatements = [
248
+ `-- Manual migration for ${tableName}`,
249
+ `-- No specific changes detected, please edit manually`,
250
+ ``,
251
+ `-- ALTER TABLE ${tableName} RENAME COLUMN old_name TO new_name;`,
252
+ `-- ALTER TABLE ${tableName} ADD COLUMN new_column TYPE;`,
253
+ `-- ALTER TABLE ${tableName} DROP COLUMN old_column;`
254
+ ];
255
+ }
256
+ const content = `-- Manual migration for ${tableName}
257
+ -- Edit this file and apply manually or using supabase CLI
258
+
259
+ ${migrationStatements.join('\n')}
260
+ `;
261
+ fs.writeFileSync(filepath, content, 'utf-8');
262
+ console.log(`手動マイグレーションテンプレート生成: ${filename}`);
263
+ return filepath;
264
+ }
265
+ /**
266
+ * より高度な差分解析(カラム変更を検出)
267
+ */
268
+ function analyzeColumnChanges(tableName, localDdl, remoteDdl) {
269
+ const statements = [];
270
+ // 簡単な例:カラム名の変更を検出
271
+ const localLines = localDdl.split('\n').map(line => line.trim()).filter(line => line);
272
+ const remoteLines = remoteDdl.split('\n').map(line => line.trim()).filter(line => line);
273
+ // カラムの型変更やデフォルト値変更などの高度な検出は今後実装
274
+ // 現在は基本的な追加/削除のみ対応
275
+ return statements;
276
+ }
@@ -0,0 +1,22 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./sync"), exports);
18
+ __exportStar(require("./fetchRemoteSchemas"), exports);
19
+ __exportStar(require("./parseLocalSchemas"), exports);
20
+ __exportStar(require("./writeSchema"), exports);
21
+ __exportStar(require("./utils"), exports);
22
+ __exportStar(require("./config"), exports);