supatool 0.3.1 → 0.3.3

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 CHANGED
@@ -285,41 +285,17 @@ supatool extract --all -c "postgresql://..." -o supabase/schemas
285
285
 
286
286
  ## Changelog
287
287
 
288
- ## Database Comments
288
+ ### v0.3.3
289
289
 
290
- Supatool automatically extracts and includes PostgreSQL comments in all generated files. Comments enhance documentation and AI understanding of your schema.
290
+ - **ENHANCED**: Improved SQL comment placement (moved to end of each SQL statement)
291
+ - **ENHANCED**: Unified comment format for tables, views, functions, and custom types
292
+ - **FIXED**: Preserved view `security_invoker` settings
291
293
 
292
- ### Adding Comments to Your Database
294
+ ### v0.3.2
293
295
 
294
- ```sql
295
- -- Table comments
296
- COMMENT ON TABLE users IS 'User account information and authentication data';
297
-
298
- -- View comments
299
- COMMENT ON VIEW user_profiles IS 'Combined user data with profile information';
300
-
301
- -- Function comments
302
- COMMENT ON FUNCTION update_timestamp() IS 'Automatically updates the updated_at column';
303
-
304
- -- Custom type comments
305
- COMMENT ON TYPE user_status IS 'Enumeration of possible user account statuses';
306
- ```
307
-
308
- ### Comment Integration
309
-
310
- Comments appear in:
311
- - **index.md**: Human-readable file listings with descriptions (tables/views only)
312
- - **llms.txt**: AI-friendly format (`type:name:path:comment`)
313
- - **Generated SQL**: As `COMMENT ON` statements for full schema recreation
314
-
315
- **Example output:**
316
- ```markdown
317
- ## Tables
318
- - [users](tables/users.sql) - User account information and authentication data
319
- - [posts](tables/posts.sql) - User-generated content and blog posts
320
- ```
321
-
322
- ## Changelog
296
+ - **ENHANCED**: Adjust for extensions(vector, geometry etc.)
297
+ - **FIXED**: USER-DEFINED column types are now rendered with full type definitions (e.g. `vector(1536)`, `geometry(Point,4326)`).
298
+ - **ADDED**: `FOREIGN KEY` constraints are now included as `CONSTRAINT ... FOREIGN KEY ... REFERENCES ...` inside generated `CREATE TABLE` statements.
323
299
 
324
300
  ### v0.3.0
325
301
 
@@ -350,4 +326,38 @@ Comments appear in:
350
326
  ### v0.2.0
351
327
  - Added `gen:` commands for code and schema generation
352
328
  - Enhanced `create` command
353
- - Introduced model schema support (`schemas/supatool-data.schema.ts`)
329
+ - Introduced model schema support (`schemas/supatool-data.schema.ts`)
330
+
331
+ ## Database Comments
332
+
333
+ Supatool automatically extracts and includes PostgreSQL comments in all generated files. Comments enhance documentation and AI understanding of your schema.
334
+
335
+ ### Adding Comments to Your Database
336
+
337
+ ```sql
338
+ -- Table comments
339
+ COMMENT ON TABLE users IS 'User account information and authentication data';
340
+
341
+ -- View comments
342
+ COMMENT ON VIEW user_profiles IS 'Combined user data with profile information';
343
+
344
+ -- Function comments
345
+ COMMENT ON FUNCTION update_timestamp() IS 'Automatically updates the updated_at column';
346
+
347
+ -- Custom type comments
348
+ COMMENT ON TYPE user_status IS 'Enumeration of possible user account statuses';
349
+ ```
350
+
351
+ ### Comment Integration
352
+
353
+ Comments appear in:
354
+ - **index.md**: Human-readable file listings with descriptions (tables/views only)
355
+ - **llms.txt**: AI-friendly format (`type:name:path:comment`)
356
+ - **Generated SQL**: As `COMMENT ON` statements for full schema recreation
357
+
358
+ **Example output:**
359
+ ```markdown
360
+ ## Tables
361
+ - [users](tables/users.sql) - User account information and authentication data
362
+ - [posts](tables/posts.sql) - User-generated content and blog posts
363
+ ```
@@ -49,6 +49,7 @@ function generateSqlFromModel(model, outPath) {
49
49
  let userIdCount = userIdFields.length;
50
50
  sql += `CREATE TABLE ${tableName} (\n`;
51
51
  const colDefs = [];
52
+ const tableConstraints = [];
52
53
  for (const [colName, col] of Object.entries(t.fields || {})) {
53
54
  const c = col;
54
55
  let actualColName = colName;
@@ -66,18 +67,23 @@ function generateSqlFromModel(model, outPath) {
66
67
  let def = ` ${actualColName} ${toSqlType(c.type, actualColName)}`;
67
68
  if (c.primary)
68
69
  def += ' PRIMARY KEY';
70
+ if (c.unique)
71
+ def += ' UNIQUE';
69
72
  if (c.notNull)
70
73
  def += ' NOT NULL';
71
74
  if (c.default)
72
75
  def += ` DEFAULT ${c.default}`;
73
- // 外部キー制約はREFERENCESで付与
76
+ // 外部キー制約はテーブル末尾に付与するため保留
77
+ colDefs.push(def);
78
+ // 外部キー制約をテーブル末尾に格納
74
79
  if (c.ref) {
75
80
  const refTable = c.ref.split('.')[0];
76
- def += ` REFERENCES ${refTable}(id)`;
81
+ tableConstraints.push(` FOREIGN KEY (${actualColName}) REFERENCES ${refTable}(id)`);
77
82
  }
78
- colDefs.push(def);
79
83
  }
80
- sql += colDefs.join(',\n') + '\n);\n\n';
84
+ // 列定義 + テーブルレベル制約を結合
85
+ const defs = [...colDefs, ...tableConstraints];
86
+ sql += defs.join(',\n') + '\n);\n\n';
81
87
  // ALTER TABLEによる外部キー制約は出力しない
82
88
  sql += '\n';
83
89
  }
@@ -89,12 +95,20 @@ function toSqlType(type, colName) {
89
95
  // 時刻列は必ずtimestamptz
90
96
  if (type === 'timestamp' || type === 'timestamptz' || colName.endsWith('_at'))
91
97
  return 'timestamptz';
98
+ // vector型サポート
99
+ if (/^vector(\(\d+\))?$/i.test(type))
100
+ return type;
101
+ // extensions.vector → vector へ変換
102
+ const extVectorMatch = type.match(/^extensions\.vector(\(\d+\))?$/i);
103
+ if (extVectorMatch) {
104
+ return `vector${extVectorMatch[1] || ''}`;
105
+ }
92
106
  switch (type) {
93
107
  case 'uuid': return 'uuid';
94
108
  case 'text': return 'text';
95
109
  case 'int':
96
110
  case 'integer': return 'integer';
97
111
  case 'boolean': return 'boolean';
98
- default: return 'text';
112
+ default: return type; // 指定が未知の場合はそのまま返す
99
113
  }
100
114
  }
@@ -230,14 +230,19 @@ async function fetchFunctions(client, spinner, progress, schemas = ['public']) {
230
230
  let ddl = '';
231
231
  if (!row.comment) {
232
232
  ddl += `-- Function: ${functionSignature}\n`;
233
- ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
234
233
  }
235
234
  else {
236
235
  ddl += `-- ${row.comment}\n`;
237
- ddl += `COMMENT ON FUNCTION ${functionSignature} IS '${row.comment}';\n\n`;
238
236
  }
239
237
  // 関数定義を追加
240
- ddl += row.definition;
238
+ ddl += row.definition + '\n\n';
239
+ // COMMENT ON文を追加
240
+ if (!row.comment) {
241
+ ddl += `-- COMMENT ON FUNCTION ${functionSignature} IS '_your_comment_here_';\n\n`;
242
+ }
243
+ else {
244
+ ddl += `COMMENT ON FUNCTION ${functionSignature} IS '${row.comment}';\n\n`;
245
+ }
241
246
  functions.push({
242
247
  name: row.name,
243
248
  type: 'function',
@@ -469,17 +474,23 @@ async function fetchCustomTypes(client, spinner, progress, schemas = ['public'])
469
474
  }
470
475
  }
471
476
  if (ddl) {
472
- // 型コメントを先頭に追加(スキーマ名を含む)
477
+ // 型コメントを先頭に追加
473
478
  let finalDdl = '';
474
479
  if (!row.comment) {
475
480
  finalDdl += `-- Type: ${row.type_name}\n`;
476
- finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
477
481
  }
478
482
  else {
479
483
  finalDdl += `-- ${row.comment}\n`;
484
+ }
485
+ // 型定義を追加
486
+ finalDdl += ddl + '\n\n';
487
+ // COMMENT ON文を追加
488
+ if (!row.comment) {
489
+ finalDdl += `-- COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '_your_comment_here_';\n\n`;
490
+ }
491
+ else {
480
492
  finalDdl += `COMMENT ON TYPE ${row.schema_name}.${row.type_name} IS '${row.comment}';\n\n`;
481
493
  }
482
- finalDdl += ddl;
483
494
  types.push({
484
495
  name: `${row.schema_name}_${row.type_name}`,
485
496
  type: 'type',
@@ -608,12 +619,11 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
608
619
  if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
609
620
  comment = viewCommentResult.rows[0].view_comment;
610
621
  ddl = `-- ${comment}\n`;
611
- ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
612
622
  }
613
623
  else {
614
624
  ddl = `-- View: ${name}\n`;
615
- ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
616
625
  }
626
+ // ビュー定義を追加
617
627
  let ddlStart = `CREATE OR REPLACE VIEW ${name}`;
618
628
  // security_invoker設定をチェック
619
629
  if (view.reloptions) {
@@ -630,7 +640,14 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
630
640
  }
631
641
  }
632
642
  }
633
- ddl += `${ddlStart} AS\n${view.definition}`;
643
+ ddl += ddlStart + ' AS\n' + view.definition + ';\n\n';
644
+ // COMMENT ON文を追加
645
+ if (viewCommentResult.rows.length > 0 && viewCommentResult.rows[0].view_comment) {
646
+ ddl += `COMMENT ON VIEW ${schemaName}.${name} IS '${comment}';\n\n`;
647
+ }
648
+ else {
649
+ ddl += `-- COMMENT ON VIEW ${schemaName}.${name} IS '_your_comment_here_';\n\n`;
650
+ }
634
651
  // ビューの作成時刻を取得(可能であれば)
635
652
  try {
636
653
  const viewStatsResult = await client.query(`
@@ -652,8 +669,7 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
652
669
  }
653
670
  }
654
671
  catch (error) {
655
- console.error(`Failed to fetch view definition: ${name}`, error);
656
- return null;
672
+ // エラーの場合はコメントなし
657
673
  }
658
674
  }
659
675
  return {
@@ -717,19 +733,24 @@ async function fetchTableDefinitions(client, spinner, progress, schemas = ['publ
717
733
  */
718
734
  async function generateCreateTableDDL(client, tableName, schemaName = 'public') {
719
735
  // 全てのクエリを並行実行
720
- const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult] = await Promise.all([
736
+ const [columnsResult, primaryKeyResult, tableCommentResult, columnCommentsResult, uniqueConstraintResult, foreignKeyResult] = await Promise.all([
721
737
  // カラム情報を取得
722
738
  client.query(`
723
739
  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
740
+ c.column_name,
741
+ c.data_type,
742
+ c.udt_name,
743
+ c.character_maximum_length,
744
+ c.is_nullable,
745
+ c.column_default,
746
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS full_type
747
+ FROM information_schema.columns c
748
+ JOIN pg_class cl ON cl.relname = c.table_name
749
+ JOIN pg_namespace ns ON ns.nspname = c.table_schema AND ns.oid = cl.relnamespace
750
+ JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attname = c.column_name
751
+ WHERE c.table_schema = $1
752
+ AND c.table_name = $2
753
+ ORDER BY c.ordinal_position
733
754
  `, [schemaName, tableName]),
734
755
  // 主キー情報を取得
735
756
  client.query(`
@@ -776,6 +797,25 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
776
797
  AND tc.constraint_type = 'UNIQUE'
777
798
  GROUP BY tc.constraint_name
778
799
  ORDER BY tc.constraint_name
800
+ `, [schemaName, tableName]),
801
+ // FOREIGN KEY制約を取得
802
+ client.query(`
803
+ SELECT
804
+ tc.constraint_name,
805
+ string_agg(kcu.column_name, ', ' ORDER BY kcu.ordinal_position) as columns,
806
+ ccu.table_schema AS foreign_table_schema,
807
+ ccu.table_name AS foreign_table_name,
808
+ string_agg(ccu.column_name, ', ' ORDER BY kcu.ordinal_position) as foreign_columns
809
+ FROM information_schema.table_constraints tc
810
+ JOIN information_schema.key_column_usage kcu
811
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
812
+ JOIN information_schema.constraint_column_usage ccu
813
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
814
+ WHERE tc.table_schema = $1
815
+ AND tc.table_name = $2
816
+ AND tc.constraint_type = 'FOREIGN KEY'
817
+ GROUP BY tc.constraint_name, ccu.table_schema, ccu.table_name
818
+ ORDER BY tc.constraint_name
779
819
  `, [schemaName, tableName])
780
820
  ]);
781
821
  const columnComments = new Map();
@@ -786,19 +826,20 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
786
826
  });
787
827
  // テーブルコメントを先頭に追加(スキーマ名を含む)
788
828
  let ddl = '';
829
+ // テーブルコメントを先頭に追加
789
830
  if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
790
831
  ddl += `-- ${tableCommentResult.rows[0].table_comment}\n`;
791
- ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
792
832
  }
793
833
  else {
794
834
  ddl += `-- Table: ${tableName}\n`;
795
- ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
796
835
  }
797
836
  // CREATE TABLE文を生成
798
837
  ddl += `CREATE TABLE IF NOT EXISTS ${tableName} (\n`;
799
838
  const columnDefs = [];
800
839
  for (const col of columnsResult.rows) {
801
- let colDef = ` ${col.column_name} ${col.data_type.toUpperCase()}`;
840
+ const rawType = col.full_type ||
841
+ ((col.data_type === 'USER-DEFINED' && col.udt_name) ? col.udt_name : col.data_type);
842
+ let colDef = ` ${col.column_name} ${rawType}`;
802
843
  // 長さ指定
803
844
  if (col.character_maximum_length) {
804
845
  colDef += `(${col.character_maximum_length})`;
@@ -823,8 +864,18 @@ async function generateCreateTableDDL(client, tableName, schemaName = 'public')
823
864
  for (const unique of uniqueConstraintResult.rows) {
824
865
  ddl += `,\n CONSTRAINT ${unique.constraint_name} UNIQUE (${unique.columns})`;
825
866
  }
826
- ddl += '\n);\n';
827
- ddl += '\n';
867
+ // FOREIGN KEY制約をCREATE TABLE内に追加
868
+ for (const fk of foreignKeyResult.rows) {
869
+ ddl += `,\n CONSTRAINT ${fk.constraint_name} FOREIGN KEY (${fk.columns}) REFERENCES ${fk.foreign_table_schema}.${fk.foreign_table_name} (${fk.foreign_columns})`;
870
+ }
871
+ ddl += '\n);\n\n';
872
+ // COMMENT ON文を追加
873
+ if (tableCommentResult.rows.length > 0 && tableCommentResult.rows[0].table_comment) {
874
+ ddl += `COMMENT ON TABLE ${schemaName}.${tableName} IS '${tableCommentResult.rows[0].table_comment}';\n\n`;
875
+ }
876
+ else {
877
+ ddl += `-- COMMENT ON TABLE ${schemaName}.${tableName} IS '_your_comment_here_';\n\n`;
878
+ }
828
879
  // カラムコメントを追加(スキーマ名を含む)
829
880
  if (columnComments.size > 0) {
830
881
  ddl += '\n-- カラムコメント\n';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supatool",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",