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 +43 -33
- package/dist/generator/sqlGenerator.js +19 -5
- package/dist/sync/definitionExtractor.js +77 -26
- package/package.json +1 -1
package/README.md
CHANGED
@@ -285,41 +285,17 @@ supatool extract --all -c "postgresql://..." -o supabase/schemas
|
|
285
285
|
|
286
286
|
## Changelog
|
287
287
|
|
288
|
-
|
288
|
+
### v0.3.3
|
289
289
|
|
290
|
-
|
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
|
-
###
|
294
|
+
### v0.3.2
|
293
295
|
|
294
|
-
|
295
|
-
|
296
|
-
|
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
|
-
//
|
76
|
+
// 外部キー制約はテーブル末尾に付与するため保留
|
77
|
+
colDefs.push(def);
|
78
|
+
// 外部キー制約をテーブル末尾に格納
|
74
79
|
if (c.ref) {
|
75
80
|
const refTable = c.ref.split('.')[0];
|
76
|
-
|
81
|
+
tableConstraints.push(` FOREIGN KEY (${actualColName}) REFERENCES ${refTable}(id)`);
|
77
82
|
}
|
78
|
-
colDefs.push(def);
|
79
83
|
}
|
80
|
-
|
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
|
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 +=
|
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
|
-
|
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
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
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
|
-
|
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
|
-
|
827
|
-
|
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