supatool 0.3.6 → 0.3.7

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.
@@ -65,26 +65,54 @@ function generateSqlFromModel(model, outPath) {
65
65
  }
66
66
  }
67
67
  let def = ` ${actualColName} ${toSqlType(c.type, actualColName)}`;
68
- if (c.primary)
69
- def += ' PRIMARY KEY';
70
- if (c.unique)
71
- def += ' UNIQUE';
72
68
  if (c.notNull)
73
69
  def += ' NOT NULL';
74
70
  if (c.default)
75
71
  def += ` DEFAULT ${c.default}`;
76
- // 外部キー制約はテーブル末尾に付与するため保留
77
72
  colDefs.push(def);
78
- // 外部キー制約をテーブル末尾に格納
79
- if (c.ref) {
80
- const refTable = c.ref.split('.')[0];
81
- tableConstraints.push(` FOREIGN KEY (${actualColName}) REFERENCES ${refTable}(id)`);
73
+ }
74
+ // 主キー制約(constraint名付き)
75
+ const pkCols = Object.entries(t.fields || {})
76
+ .filter(([_, col]) => col.primary)
77
+ .map(([colName, _]) => colName);
78
+ if (pkCols.length > 0) {
79
+ tableConstraints.push(` CONSTRAINT ${tableName}_pkey PRIMARY KEY (${pkCols.join(', ')})`);
80
+ }
81
+ // ユニーク制約(constraint名付き)
82
+ if (Array.isArray(t.uniques)) {
83
+ for (const unique of t.uniques) {
84
+ if (Array.isArray(unique.columns) && unique.name) {
85
+ tableConstraints.push(` CONSTRAINT ${unique.name} UNIQUE (${unique.columns.join(', ')})`);
86
+ }
87
+ }
88
+ }
89
+ // 外部キー制約(constraint名付き)
90
+ if (Array.isArray(t.foreignKeys)) {
91
+ for (const fk of t.foreignKeys) {
92
+ if (fk.name && fk.columns && fk.refTable && fk.refColumns) {
93
+ let fkStr = ` CONSTRAINT ${fk.name} FOREIGN KEY (${fk.columns.join(', ')}) REFERENCES ${fk.refTable} (${fk.refColumns.join(', ')})`;
94
+ if (fk.onDelete)
95
+ fkStr += ` ON DELETE ${fk.onDelete}`;
96
+ if (fk.onUpdate)
97
+ fkStr += ` ON UPDATE ${fk.onUpdate}`;
98
+ tableConstraints.push(fkStr);
99
+ }
100
+ }
101
+ }
102
+ // チェック制約(constraint名付き)
103
+ if (Array.isArray(t.checkConstraints)) {
104
+ for (const check of t.checkConstraints) {
105
+ if (check.name) {
106
+ tableConstraints.push(` CONSTRAINT ${check.name} CHECK (${check.expression})`);
107
+ }
108
+ else {
109
+ tableConstraints.push(` CHECK (${check.expression})`);
110
+ }
82
111
  }
83
112
  }
84
113
  // 列定義 + テーブルレベル制約を結合
85
114
  const defs = [...colDefs, ...tableConstraints];
86
115
  sql += defs.join(',\n') + '\n);\n\n';
87
- // ALTER TABLEによる外部キー制約は出力しない
88
116
  sql += '\n';
89
117
  }
90
118
  fs_1.default.writeFileSync(outPath, sql);
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.extractDefinitions = extractDefinitions;
37
+ exports.generateCreateTableDDL = generateCreateTableDDL;
37
38
  const pg_1 = require("pg");
38
39
  /**
39
40
  * 進行状況を表示
@@ -869,6 +870,22 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
869
870
  for (const fk of foreignKeyResult.rows) {
870
871
  ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
871
872
  }
873
+ // CHECK制約をCREATE TABLE内に追加(必ず最後)
874
+ const checkConstraintResult = await client.query(`
875
+ SELECT
876
+ con.conname as constraint_name,
877
+ pg_get_constraintdef(con.oid) as check_clause
878
+ FROM pg_constraint con
879
+ JOIN pg_class rel ON rel.oid = con.conrelid
880
+ JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
881
+ WHERE lower(rel.relname) = lower($1)
882
+ AND nsp.nspname = $2
883
+ AND con.contype = 'c'
884
+ ORDER BY con.conname
885
+ `, [tableName, schemaName]);
886
+ for (const check of checkConstraintResult.rows) {
887
+ ddl += `,\n CONSTRAINT ${check.constraint_name} ${check.check_clause}`;
888
+ }
872
889
  ddl += '\n);\n\n';
873
890
  // COMMENT ON文を追加
874
891
  if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
@@ -37,6 +37,7 @@ exports.fetchRemoteSchemas = fetchRemoteSchemas;
37
37
  const pg_1 = require("pg");
38
38
  const dns = __importStar(require("dns"));
39
39
  const util_1 = require("util");
40
+ const definitionExtractor_1 = require("./definitionExtractor");
40
41
  const dnsLookup = (0, util_1.promisify)(dns.lookup);
41
42
  /**
42
43
  * DDL文字列を正規化(空白・改行・タブを統一)
@@ -163,162 +164,32 @@ async function fetchRemoteSchemas(connectionString) {
163
164
  // 各テーブルのスキーマ情報を取得
164
165
  for (const row of tablesResult.rows) {
165
166
  const tableName = row.tablename;
166
- // テーブルの最終更新時刻を取得(複数の方法を試行)
167
- let timestamp = Math.floor(Date.now() / 1000); // デフォルト値
168
- // pg_stat_user_tablesから取得
167
+ // テーブルの最終更新時刻を取得(省略: 既存ロジック流用)
168
+ let timestamp = Math.floor(Date.now() / 1000);
169
169
  try {
170
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]);
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
181
  if (tableStatsResult.rows.length > 0 && tableStatsResult.rows[0].last_updated > 0) {
182
182
  timestamp = tableStatsResult.rows[0].last_updated;
183
183
  }
184
184
  else {
185
- // デフォルトタイムスタンプ(十分に古い固定値)
186
185
  timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
187
186
  }
188
187
  }
189
- catch (statsError) {
190
- // デフォルトタイムスタンプ(十分に古い固定値)
188
+ catch {
191
189
  timestamp = Math.floor(new Date('2020-01-01').getTime() / 1000);
192
190
  }
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を元の形式で保存
191
+ // DDL生成をdefinitionExtractorの関数で統一
192
+ const ddl = await (0, definitionExtractor_1.generateCreateTableDDL)(client, tableName, 'public');
322
193
  schemas[tableName] = {
323
194
  ddl,
324
195
  timestamp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supatool",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "A CLI tool for Supabase schema extraction and TypeScript CRUD generation with declarative database schema support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",