relq 1.0.24 → 1.0.26
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/dist/cjs/cli/commands/commit.cjs +80 -0
- package/dist/cjs/cli/commands/import.cjs +1 -0
- package/dist/cjs/cli/commands/pull.cjs +8 -25
- package/dist/cjs/cli/commands/push.cjs +48 -8
- package/dist/cjs/cli/commands/rollback.cjs +205 -84
- package/dist/cjs/cli/commands/schema-ast.cjs +219 -0
- package/dist/cjs/cli/index.cjs +6 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +95 -3
- package/dist/cjs/cli/utils/ast-transformer.cjs +12 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +135 -0
- package/dist/cjs/cli/utils/commit-manager.cjs +54 -0
- package/dist/cjs/cli/utils/migration-generator.cjs +319 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +99 -3
- package/dist/cjs/cli/utils/schema-diff.cjs +390 -0
- package/dist/cjs/cli/utils/schema-hash.cjs +4 -0
- package/dist/cjs/cli/utils/schema-to-ast.cjs +477 -0
- package/dist/cjs/cli/utils/schema-validator.cjs +21 -1
- package/dist/cjs/schema-definition/column-types.cjs +63 -10
- package/dist/cjs/schema-definition/pg-enum.cjs +10 -0
- package/dist/cjs/schema-definition/pg-function.cjs +19 -0
- package/dist/cjs/schema-definition/pg-sequence.cjs +22 -1
- package/dist/cjs/schema-definition/pg-trigger.cjs +39 -0
- package/dist/cjs/schema-definition/pg-view.cjs +17 -0
- package/dist/cjs/schema-definition/sql-expressions.cjs +3 -0
- package/dist/cjs/schema-definition/table-definition.cjs +4 -0
- package/dist/config.d.ts +98 -0
- package/dist/esm/cli/commands/commit.js +83 -3
- package/dist/esm/cli/commands/import.js +1 -0
- package/dist/esm/cli/commands/pull.js +9 -26
- package/dist/esm/cli/commands/push.js +49 -9
- package/dist/esm/cli/commands/rollback.js +206 -85
- package/dist/esm/cli/commands/schema-ast.js +183 -0
- package/dist/esm/cli/index.js +6 -0
- package/dist/esm/cli/utils/ast-codegen.js +93 -3
- package/dist/esm/cli/utils/ast-transformer.js +12 -0
- package/dist/esm/cli/utils/change-tracker.js +134 -0
- package/dist/esm/cli/utils/commit-manager.js +51 -0
- package/dist/esm/cli/utils/migration-generator.js +318 -0
- package/dist/esm/cli/utils/repo-manager.js +96 -3
- package/dist/esm/cli/utils/schema-diff.js +389 -0
- package/dist/esm/cli/utils/schema-hash.js +4 -0
- package/dist/esm/cli/utils/schema-to-ast.js +447 -0
- package/dist/esm/cli/utils/schema-validator.js +21 -1
- package/dist/esm/schema-definition/column-types.js +63 -10
- package/dist/esm/schema-definition/pg-enum.js +10 -0
- package/dist/esm/schema-definition/pg-function.js +19 -0
- package/dist/esm/schema-definition/pg-sequence.js +22 -1
- package/dist/esm/schema-definition/pg-trigger.js +39 -0
- package/dist/esm/schema-definition/pg-view.js +17 -0
- package/dist/esm/schema-definition/sql-expressions.js +3 -0
- package/dist/esm/schema-definition/table-definition.js +4 -0
- package/dist/index.d.ts +98 -0
- package/dist/schema-builder.d.ts +223 -24
- package/package.json +1 -1
|
@@ -2,110 +2,231 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { requireValidConfig } from "../utils/config-loader.js";
|
|
4
4
|
import { getConnectionDescription } from "../utils/env-loader.js";
|
|
5
|
-
import { colors,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
up: upMatch?.[1]?.trim() || '',
|
|
11
|
-
down: downMatch?.[1]?.trim() || '',
|
|
12
|
-
};
|
|
13
|
-
}
|
|
5
|
+
import { colors, createSpinner, fatal, confirm, warning } from "../utils/cli-utils.js";
|
|
6
|
+
import { isInitialized, getHead, getAllCommits, shortHash, ensureRemoteTable, markCommitAsRolledBack, } from "../utils/repo-manager.js";
|
|
7
|
+
import { compareSchemas } from "../utils/schema-diff.js";
|
|
8
|
+
import { generateMigrationFromComparison } from "../utils/migration-generator.js";
|
|
14
9
|
export async function rollbackCommand(context) {
|
|
15
10
|
const { config, args, flags } = context;
|
|
16
11
|
if (!config) {
|
|
17
|
-
fatal('No configuration found', `
|
|
18
|
-
return;
|
|
12
|
+
fatal('No configuration found', `Run ${colors.cyan('relq init')} to create one.`);
|
|
19
13
|
}
|
|
20
14
|
await requireValidConfig(config, { calledFrom: 'rollback' });
|
|
21
15
|
const connection = config.connection;
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const dryRun = flags['dry-run'] === true;
|
|
26
|
-
const force = flags['force'] === true;
|
|
27
|
-
console.log(`Rolling back ${count} migration(s)...`);
|
|
28
|
-
console.log(` Connection: ${getConnectionDescription(connection)}`);
|
|
16
|
+
const { projectRoot } = context;
|
|
17
|
+
const preview = flags['preview'] === true || flags['dry-run'] === true;
|
|
18
|
+
const skipPrompt = flags['yes'] === true || flags['y'] === true;
|
|
29
19
|
console.log('');
|
|
20
|
+
if (!isInitialized(projectRoot)) {
|
|
21
|
+
fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
|
|
22
|
+
}
|
|
23
|
+
const localHead = getHead(projectRoot);
|
|
24
|
+
if (!localHead) {
|
|
25
|
+
fatal('no commits to rollback', 'Repository has no commits.');
|
|
26
|
+
}
|
|
27
|
+
const targetRef = args[0] || 'HEAD~1';
|
|
28
|
+
const target = resolveTarget(projectRoot, targetRef, localHead);
|
|
29
|
+
if (!target) {
|
|
30
|
+
fatal(`invalid rollback target: ${targetRef}`, 'Use a commit hash or HEAD~N notation.');
|
|
31
|
+
}
|
|
32
|
+
if (target.commitsToRollback.length === 0) {
|
|
33
|
+
console.log('Nothing to rollback - already at target commit.');
|
|
34
|
+
console.log('');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const spinner = createSpinner();
|
|
30
38
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
console.log(`Rolling back to ${colors.yellow(shortHash(target.hash))}`);
|
|
40
|
+
console.log(`${colors.muted(target.message)}`);
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(`${colors.red('Commits to rollback:')} ${target.commitsToRollback.length}`);
|
|
43
|
+
for (const commit of target.commitsToRollback.slice(0, 5)) {
|
|
44
|
+
console.log(` ${colors.red('↩')} ${shortHash(commit.hash)} ${commit.message}`);
|
|
45
|
+
}
|
|
46
|
+
if (target.commitsToRollback.length > 5) {
|
|
47
|
+
console.log(` ${colors.muted(`... and ${target.commitsToRollback.length - 5} more`)}`);
|
|
48
|
+
}
|
|
49
|
+
console.log('');
|
|
50
|
+
const currentCommitPath = path.join(projectRoot, '.relq', 'commits', `${localHead}.json`);
|
|
51
|
+
const targetCommitPath = path.join(projectRoot, '.relq', 'commits', `${target.hash}.json`);
|
|
52
|
+
if (!fs.existsSync(currentCommitPath)) {
|
|
53
|
+
fatal(`current commit not found: ${localHead}`);
|
|
54
|
+
}
|
|
55
|
+
if (!fs.existsSync(targetCommitPath)) {
|
|
56
|
+
fatal(`target commit not found: ${target.hash}`);
|
|
57
|
+
}
|
|
58
|
+
const currentCommit = JSON.parse(fs.readFileSync(currentCommitPath, 'utf-8'));
|
|
59
|
+
const targetCommit = JSON.parse(fs.readFileSync(targetCommitPath, 'utf-8'));
|
|
60
|
+
let rollbackSQL = [];
|
|
61
|
+
if (currentCommit.schemaAST && targetCommit.schemaAST) {
|
|
62
|
+
const comparison = compareSchemas(currentCommit.schemaAST, targetCommit.schemaAST);
|
|
63
|
+
const migration = generateMigrationFromComparison(comparison);
|
|
64
|
+
rollbackSQL = migration.down;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
warning('No schema AST found - using commit SQL reversal (may be incomplete)');
|
|
55
68
|
console.log('');
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
for (const commit of target.commitsToRollback) {
|
|
70
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
|
|
71
|
+
if (fs.existsSync(commitPath)) {
|
|
72
|
+
const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
73
|
+
if (enhancedCommit.downSQL) {
|
|
74
|
+
rollbackSQL.push(enhancedCommit.downSQL);
|
|
75
|
+
}
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
}
|
|
79
|
+
if (rollbackSQL.length === 0) {
|
|
80
|
+
warning('No rollback SQL generated - manual intervention may be required');
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
if (preview) {
|
|
84
|
+
console.log(`${colors.yellow('Preview')} - showing rollback SQL`);
|
|
85
|
+
console.log('');
|
|
86
|
+
if (rollbackSQL.length > 0) {
|
|
87
|
+
for (const sql of rollbackSQL.slice(0, 10)) {
|
|
88
|
+
console.log(` ${sql.substring(0, 100)}${sql.length > 100 ? '...' : ''}`);
|
|
74
89
|
}
|
|
75
|
-
if (
|
|
76
|
-
console.log(`
|
|
77
|
-
console.log(down);
|
|
78
|
-
console.log('');
|
|
90
|
+
if (rollbackSQL.length > 10) {
|
|
91
|
+
console.log(` ${colors.muted(`... and ${rollbackSQL.length - 10} more statements`)}`);
|
|
79
92
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(` ${colors.muted('(no SQL statements)')}`);
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(`${colors.muted('Remove')} ${colors.cyan('--preview')} ${colors.muted('to execute rollback.')}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!skipPrompt) {
|
|
103
|
+
warning('This will modify your database!');
|
|
104
|
+
console.log('');
|
|
105
|
+
const confirmed = await confirm(`Rollback ${target.commitsToRollback.length} commit(s)?`, false);
|
|
106
|
+
if (!confirmed) {
|
|
107
|
+
fatal('Rollback cancelled by user');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
spinner.start('Connecting to remote...');
|
|
111
|
+
await ensureRemoteTable(connection);
|
|
112
|
+
spinner.succeed(`Connected to ${colors.cyan(getConnectionDescription(connection))}`);
|
|
113
|
+
if (rollbackSQL.length > 0) {
|
|
114
|
+
spinner.start('Executing rollback...');
|
|
115
|
+
const pg = await import("../../addon/pg/index.js");
|
|
116
|
+
const client = new pg.Client({
|
|
117
|
+
host: connection.host,
|
|
118
|
+
port: connection.port,
|
|
119
|
+
database: connection.database,
|
|
120
|
+
user: connection.user,
|
|
121
|
+
password: connection.password,
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
await client.connect();
|
|
125
|
+
await client.query('BEGIN');
|
|
126
|
+
let statementsRun = 0;
|
|
127
|
+
for (const sql of rollbackSQL) {
|
|
128
|
+
if (sql.trim()) {
|
|
129
|
+
await client.query(sql);
|
|
130
|
+
statementsRun++;
|
|
96
131
|
}
|
|
97
132
|
}
|
|
133
|
+
await client.query('COMMIT');
|
|
134
|
+
spinner.succeed(`Executed ${statementsRun} rollback statement(s)`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
try {
|
|
138
|
+
await client.query('ROLLBACK');
|
|
139
|
+
spinner.fail('Rollback failed - transaction rolled back');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
spinner.fail('Rollback failed');
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
98
145
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
console.log(`Rolled back ${toRollback.length} migration(s).`);
|
|
146
|
+
finally {
|
|
147
|
+
await client.end();
|
|
102
148
|
}
|
|
103
149
|
}
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
spinner.start('Updating commit status...');
|
|
151
|
+
for (const commit of target.commitsToRollback) {
|
|
152
|
+
await markCommitAsRolledBack(connection, commit.hash);
|
|
106
153
|
}
|
|
154
|
+
spinner.succeed('Marked commits as rolled back');
|
|
155
|
+
const headPath = path.join(projectRoot, '.relq', 'HEAD');
|
|
156
|
+
fs.writeFileSync(headPath, target.hash);
|
|
157
|
+
if (targetCommit.schema) {
|
|
158
|
+
const snapshotPath = path.join(projectRoot, '.relq', 'snapshot.json');
|
|
159
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(targetCommit.schema, null, 2));
|
|
160
|
+
}
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(`${colors.green('Rollback complete')}`);
|
|
163
|
+
console.log(` ${shortHash(localHead)} → ${shortHash(target.hash)}`);
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
spinner.fail('Rollback failed');
|
|
168
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
107
169
|
}
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
}
|
|
171
|
+
function resolveTarget(projectRoot, ref, currentHead) {
|
|
172
|
+
const commits = getAllCommits(projectRoot);
|
|
173
|
+
const commitsByHash = new Map(commits.map(c => [c.hash, c]));
|
|
174
|
+
if (ref.startsWith('HEAD~')) {
|
|
175
|
+
const n = parseInt(ref.slice(5), 10);
|
|
176
|
+
if (isNaN(n) || n < 1)
|
|
177
|
+
return null;
|
|
178
|
+
let current = currentHead;
|
|
179
|
+
const commitsToRollback = [];
|
|
180
|
+
for (let i = 0; i < n; i++) {
|
|
181
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${current}.json`);
|
|
182
|
+
if (!fs.existsSync(commitPath))
|
|
183
|
+
return null;
|
|
184
|
+
const commit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
185
|
+
commitsToRollback.push(commit);
|
|
186
|
+
if (!commit.parentHash) {
|
|
187
|
+
if (i < n - 1) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
hash: commit.hash,
|
|
192
|
+
message: '(initial state)',
|
|
193
|
+
commitsToRollback: commitsToRollback.slice(0, -1),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
current = commit.parentHash;
|
|
197
|
+
}
|
|
198
|
+
const targetPath = path.join(projectRoot, '.relq', 'commits', `${current}.json`);
|
|
199
|
+
if (!fs.existsSync(targetPath))
|
|
200
|
+
return null;
|
|
201
|
+
const targetCommit = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
|
|
202
|
+
return {
|
|
203
|
+
hash: current,
|
|
204
|
+
message: targetCommit.message,
|
|
205
|
+
commitsToRollback,
|
|
206
|
+
};
|
|
110
207
|
}
|
|
208
|
+
const normalizedRef = ref.length < 40 ? commits.find(c => c.hash.startsWith(ref))?.hash : ref;
|
|
209
|
+
if (!normalizedRef || !commitsByHash.has(normalizedRef)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const commitsToRollback = [];
|
|
213
|
+
let current = currentHead;
|
|
214
|
+
while (current && current !== normalizedRef) {
|
|
215
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${current}.json`);
|
|
216
|
+
if (!fs.existsSync(commitPath))
|
|
217
|
+
break;
|
|
218
|
+
const commit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
219
|
+
commitsToRollback.push(commit);
|
|
220
|
+
current = commit.parentHash || '';
|
|
221
|
+
}
|
|
222
|
+
if (current !== normalizedRef) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const targetCommit = commitsByHash.get(normalizedRef);
|
|
226
|
+
return {
|
|
227
|
+
hash: normalizedRef,
|
|
228
|
+
message: targetCommit?.message || '',
|
|
229
|
+
commitsToRollback,
|
|
230
|
+
};
|
|
111
231
|
}
|
|
232
|
+
export default rollbackCommand;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { createJiti } from 'jiti';
|
|
4
|
+
import { getSchemaPath } from "../utils/config-loader.js";
|
|
5
|
+
import { schemaToAST } from "../utils/schema-to-ast.js";
|
|
6
|
+
import { colors, createSpinner } from "../utils/spinner.js";
|
|
7
|
+
export async function schemaAstCommand(context) {
|
|
8
|
+
const { config, args, flags, projectRoot } = context;
|
|
9
|
+
const spinner = createSpinner();
|
|
10
|
+
console.log('');
|
|
11
|
+
const options = {
|
|
12
|
+
json: Boolean(flags.json),
|
|
13
|
+
output: flags.output,
|
|
14
|
+
pretty: flags.pretty !== false,
|
|
15
|
+
};
|
|
16
|
+
let schemaPath;
|
|
17
|
+
if (args.length > 0) {
|
|
18
|
+
schemaPath = path.resolve(projectRoot, args[0]);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
schemaPath = path.resolve(projectRoot, getSchemaPath(config ?? undefined));
|
|
22
|
+
}
|
|
23
|
+
const relativePath = path.relative(process.cwd(), schemaPath);
|
|
24
|
+
spinner.start(`Loading schema from ${colors.cyan(relativePath)}`);
|
|
25
|
+
let schemaModule;
|
|
26
|
+
try {
|
|
27
|
+
const jiti = createJiti(path.dirname(schemaPath), { interopDefault: true });
|
|
28
|
+
const module = await jiti.import(schemaPath);
|
|
29
|
+
if (module && module.default && typeof module.default === 'object') {
|
|
30
|
+
schemaModule = module.default;
|
|
31
|
+
}
|
|
32
|
+
else if (module && typeof module === 'object') {
|
|
33
|
+
schemaModule = module;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
throw new Error('Schema file must export an object with table/enum definitions');
|
|
37
|
+
}
|
|
38
|
+
spinner.succeed(`Loaded schema from ${colors.cyan(relativePath)}`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
spinner.fail(`Failed to load schema`);
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(colors.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(colors.yellow('hint:') + ` Make sure ${relativePath} is a valid TypeScript schema file.`);
|
|
46
|
+
console.log('');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
spinner.start('Converting schema to AST');
|
|
50
|
+
let ast;
|
|
51
|
+
try {
|
|
52
|
+
ast = schemaToAST(schemaModule);
|
|
53
|
+
spinner.succeed('Converted schema to AST');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
spinner.fail('Failed to convert schema');
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(colors.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
59
|
+
console.log('');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const indent = options.pretty ? 2 : 0;
|
|
63
|
+
const output = JSON.stringify(ast, null, indent);
|
|
64
|
+
if (options.output) {
|
|
65
|
+
const outputPath = path.resolve(projectRoot, options.output);
|
|
66
|
+
const outputDir = path.dirname(outputPath);
|
|
67
|
+
if (!fs.existsSync(outputDir)) {
|
|
68
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
fs.writeFileSync(outputPath, output, 'utf-8');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(`Written AST to ${colors.cyan(options.output)}`);
|
|
73
|
+
}
|
|
74
|
+
else if (options.json) {
|
|
75
|
+
console.log(output);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.log('');
|
|
79
|
+
printAstSummary(ast);
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(colors.muted(`Use ${colors.cyan('--json')} for full AST output or ${colors.cyan('--output <file>')} to write to file.`));
|
|
82
|
+
}
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
85
|
+
function printAstSummary(ast) {
|
|
86
|
+
console.log(colors.bold('Schema AST Summary'));
|
|
87
|
+
console.log('');
|
|
88
|
+
if (ast.extensions.length > 0) {
|
|
89
|
+
console.log(colors.cyan('Extensions:') + ` ${ast.extensions.length}`);
|
|
90
|
+
for (const ext of ast.extensions) {
|
|
91
|
+
console.log(` ${colors.green('•')} ${ext}`);
|
|
92
|
+
}
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
if (ast.enums.length > 0) {
|
|
96
|
+
console.log(colors.cyan('Enums:') + ` ${ast.enums.length}`);
|
|
97
|
+
for (const e of ast.enums) {
|
|
98
|
+
const tid = e.trackingId ? colors.muted(` [${e.trackingId}]`) : '';
|
|
99
|
+
console.log(` ${colors.green('•')} ${e.name}${tid}: (${e.values.join(', ')})`);
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
|
103
|
+
if (ast.domains.length > 0) {
|
|
104
|
+
console.log(colors.cyan('Domains:') + ` ${ast.domains.length}`);
|
|
105
|
+
for (const d of ast.domains) {
|
|
106
|
+
const tid = d.trackingId ? colors.muted(` [${d.trackingId}]`) : '';
|
|
107
|
+
console.log(` ${colors.green('•')} ${d.name}${tid}: ${d.baseType}`);
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
if (ast.compositeTypes.length > 0) {
|
|
112
|
+
console.log(colors.cyan('Composite Types:') + ` ${ast.compositeTypes.length}`);
|
|
113
|
+
for (const c of ast.compositeTypes) {
|
|
114
|
+
const tid = c.trackingId ? colors.muted(` [${c.trackingId}]`) : '';
|
|
115
|
+
const attrs = c.attributes.map(a => a.name).join(', ');
|
|
116
|
+
console.log(` ${colors.green('•')} ${c.name}${tid}: (${attrs})`);
|
|
117
|
+
}
|
|
118
|
+
console.log('');
|
|
119
|
+
}
|
|
120
|
+
if (ast.sequences.length > 0) {
|
|
121
|
+
console.log(colors.cyan('Sequences:') + ` ${ast.sequences.length}`);
|
|
122
|
+
for (const s of ast.sequences) {
|
|
123
|
+
const tid = s.trackingId ? colors.muted(` [${s.trackingId}]`) : '';
|
|
124
|
+
console.log(` ${colors.green('•')} ${s.name}${tid}`);
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
if (ast.tables.length > 0) {
|
|
129
|
+
console.log(colors.cyan('Tables:') + ` ${ast.tables.length}`);
|
|
130
|
+
for (const t of ast.tables) {
|
|
131
|
+
const tid = t.trackingId ? colors.muted(` [${t.trackingId}]`) : '';
|
|
132
|
+
console.log(` ${colors.green('•')} ${t.name}${tid}`);
|
|
133
|
+
console.log(` Columns: ${t.columns.length}`);
|
|
134
|
+
for (const col of t.columns.slice(0, 5)) {
|
|
135
|
+
const colTid = col.trackingId ? colors.muted(` [${col.trackingId}]`) : '';
|
|
136
|
+
const pk = col.isPrimaryKey ? colors.yellow(' PK') : '';
|
|
137
|
+
const nullable = col.isNullable ? '' : colors.red(' NOT NULL');
|
|
138
|
+
console.log(` - ${col.name}${colTid}: ${col.type}${pk}${nullable}`);
|
|
139
|
+
}
|
|
140
|
+
if (t.columns.length > 5) {
|
|
141
|
+
console.log(` ${colors.muted(`... and ${t.columns.length - 5} more`)}`);
|
|
142
|
+
}
|
|
143
|
+
if (t.indexes.length > 0) {
|
|
144
|
+
console.log(` Indexes: ${t.indexes.length}`);
|
|
145
|
+
}
|
|
146
|
+
if (t.constraints.length > 0) {
|
|
147
|
+
console.log(` Constraints: ${t.constraints.length}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
if (ast.views.length > 0) {
|
|
153
|
+
console.log(colors.cyan('Views:') + ` ${ast.views.length}`);
|
|
154
|
+
for (const v of ast.views) {
|
|
155
|
+
const tid = v.trackingId ? colors.muted(` [${v.trackingId}]`) : '';
|
|
156
|
+
const mat = v.isMaterialized ? colors.yellow(' (materialized)') : '';
|
|
157
|
+
console.log(` ${colors.green('•')} ${v.name}${tid}${mat}`);
|
|
158
|
+
}
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
if (ast.functions.length > 0) {
|
|
162
|
+
console.log(colors.cyan('Functions:') + ` ${ast.functions.length}`);
|
|
163
|
+
for (const f of ast.functions) {
|
|
164
|
+
const tid = f.trackingId ? colors.muted(` [${f.trackingId}]`) : '';
|
|
165
|
+
const args = f.args.map(a => a.type).join(', ');
|
|
166
|
+
console.log(` ${colors.green('•')} ${f.name}${tid}(${args}) -> ${f.returnType}`);
|
|
167
|
+
}
|
|
168
|
+
console.log('');
|
|
169
|
+
}
|
|
170
|
+
if (ast.triggers.length > 0) {
|
|
171
|
+
console.log(colors.cyan('Triggers:') + ` ${ast.triggers.length}`);
|
|
172
|
+
for (const tr of ast.triggers) {
|
|
173
|
+
const tid = tr.trackingId ? colors.muted(` [${tr.trackingId}]`) : '';
|
|
174
|
+
console.log(` ${colors.green('•')} ${tr.name}${tid} on ${tr.table}`);
|
|
175
|
+
}
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
const total = ast.tables.length + ast.enums.length + ast.views.length +
|
|
179
|
+
ast.functions.length + ast.triggers.length + ast.sequences.length +
|
|
180
|
+
ast.domains.length + ast.compositeTypes.length;
|
|
181
|
+
console.log(colors.bold(`Total: ${total} schema objects`));
|
|
182
|
+
}
|
|
183
|
+
export default schemaAstCommand;
|
package/dist/esm/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { tagCommand } from "./commands/tag.js";
|
|
|
22
22
|
import { cherryPickCommand } from "./commands/cherry-pick.js";
|
|
23
23
|
import { remoteCommand } from "./commands/remote.js";
|
|
24
24
|
import { validateCommand } from "./commands/validate.js";
|
|
25
|
+
import { schemaAstCommand } from "./commands/schema-ast.js";
|
|
25
26
|
import * as fs from 'fs';
|
|
26
27
|
import * as path from 'path';
|
|
27
28
|
function loadEnvFile() {
|
|
@@ -132,6 +133,7 @@ Other Commands:
|
|
|
132
133
|
introspect Parse database schema
|
|
133
134
|
import <sql-file> Import SQL file to schema
|
|
134
135
|
export [file] Export schema to SQL file
|
|
136
|
+
schema:ast [file] Convert schema to AST (JSON)
|
|
135
137
|
|
|
136
138
|
Options:
|
|
137
139
|
--help, -h Show this help
|
|
@@ -323,6 +325,10 @@ async function main() {
|
|
|
323
325
|
case 'validate':
|
|
324
326
|
await validateCommand(context);
|
|
325
327
|
break;
|
|
328
|
+
case 'schema:ast':
|
|
329
|
+
case 'ast':
|
|
330
|
+
await schemaAstCommand(context);
|
|
331
|
+
break;
|
|
326
332
|
case 'sync':
|
|
327
333
|
await syncCommand(context);
|
|
328
334
|
break;
|
|
@@ -14,6 +14,96 @@ function generateTrackingId(prefix) {
|
|
|
14
14
|
export function resetTrackingIdCounter() {
|
|
15
15
|
trackingIdCounter = 0;
|
|
16
16
|
}
|
|
17
|
+
export function assignTrackingIds(schema) {
|
|
18
|
+
for (const table of schema.tables) {
|
|
19
|
+
if (!table.trackingId) {
|
|
20
|
+
table.trackingId = generateTrackingId('t');
|
|
21
|
+
}
|
|
22
|
+
for (const col of table.columns) {
|
|
23
|
+
if (!col.trackingId) {
|
|
24
|
+
col.trackingId = generateTrackingId('c');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const idx of table.indexes) {
|
|
28
|
+
if (!idx.trackingId) {
|
|
29
|
+
idx.trackingId = generateTrackingId('i');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const e of schema.enums) {
|
|
34
|
+
if (!e.trackingId) {
|
|
35
|
+
e.trackingId = generateTrackingId('t');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const f of schema.functions) {
|
|
39
|
+
if (!f.trackingId) {
|
|
40
|
+
f.trackingId = generateTrackingId('f');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const s of schema.sequences) {
|
|
44
|
+
if (!s.trackingId) {
|
|
45
|
+
s.trackingId = generateTrackingId('t');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const v of schema.views) {
|
|
49
|
+
if (!v.trackingId) {
|
|
50
|
+
v.trackingId = generateTrackingId('t');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const d of schema.domains) {
|
|
54
|
+
if (!d.trackingId) {
|
|
55
|
+
d.trackingId = generateTrackingId('t');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const tr of schema.triggers) {
|
|
59
|
+
if (!tr.trackingId) {
|
|
60
|
+
tr.trackingId = generateTrackingId('t');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return schema;
|
|
64
|
+
}
|
|
65
|
+
export function copyTrackingIdsToNormalized(parsedSchema, normalizedSchema) {
|
|
66
|
+
const tableMap = new Map();
|
|
67
|
+
for (const table of parsedSchema.tables) {
|
|
68
|
+
const columnMap = new Map();
|
|
69
|
+
for (const col of table.columns) {
|
|
70
|
+
if (col.trackingId) {
|
|
71
|
+
columnMap.set(col.name, col.trackingId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const indexMap = new Map();
|
|
75
|
+
for (const idx of table.indexes) {
|
|
76
|
+
if (idx.trackingId) {
|
|
77
|
+
indexMap.set(idx.name, idx.trackingId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
tableMap.set(table.name, {
|
|
81
|
+
trackingId: table.trackingId,
|
|
82
|
+
columns: columnMap,
|
|
83
|
+
indexes: indexMap,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
for (const table of normalizedSchema.tables) {
|
|
87
|
+
const parsed = tableMap.get(table.name);
|
|
88
|
+
if (!parsed)
|
|
89
|
+
continue;
|
|
90
|
+
if (parsed.trackingId) {
|
|
91
|
+
table.trackingId = parsed.trackingId;
|
|
92
|
+
}
|
|
93
|
+
for (const col of table.columns) {
|
|
94
|
+
const colTrackingId = parsed.columns.get(col.name);
|
|
95
|
+
if (colTrackingId) {
|
|
96
|
+
col.trackingId = colTrackingId;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const idx of table.indexes) {
|
|
100
|
+
const idxTrackingId = parsed.indexes.get(idx.name);
|
|
101
|
+
if (idxTrackingId) {
|
|
102
|
+
idx.trackingId = idxTrackingId;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
17
107
|
function getExplicitFKName(constraintName, tableName, columnName) {
|
|
18
108
|
if (!constraintName)
|
|
19
109
|
return undefined;
|
|
@@ -81,7 +171,7 @@ function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOver
|
|
|
81
171
|
if (!col.isNullable && !col.isPrimaryKey) {
|
|
82
172
|
line += '.notNull()';
|
|
83
173
|
}
|
|
84
|
-
const trackingId = generateTrackingId('c');
|
|
174
|
+
const trackingId = col.trackingId || generateTrackingId('c');
|
|
85
175
|
line += `.$id('${trackingId}')`;
|
|
86
176
|
return line + commentSuffix;
|
|
87
177
|
}
|
|
@@ -127,7 +217,7 @@ function generateIndexCode(index, useCamelCase) {
|
|
|
127
217
|
const includeCols = index.includeColumns.map(c => `table.${useCamelCase ? toCamelCase(c) : c}`).join(', ');
|
|
128
218
|
line += `.include(${includeCols})`;
|
|
129
219
|
}
|
|
130
|
-
const trackingId = generateTrackingId('i');
|
|
220
|
+
const trackingId = index.trackingId || generateTrackingId('i');
|
|
131
221
|
line += `.$id('${trackingId}')`;
|
|
132
222
|
if (index.comment) {
|
|
133
223
|
line += `.comment('${escapeString(index.comment)}')`;
|
|
@@ -396,7 +486,7 @@ function generateTableCode(table, useCamelCase, enumNames, domainNames) {
|
|
|
396
486
|
if (tableComment) {
|
|
397
487
|
optionParts.push(tableComment);
|
|
398
488
|
}
|
|
399
|
-
const tableTrackingId = generateTrackingId('t');
|
|
489
|
+
const tableTrackingId = table.trackingId || generateTrackingId('t');
|
|
400
490
|
optionParts.push(` $trackingId: '${tableTrackingId}'`);
|
|
401
491
|
let tableCode;
|
|
402
492
|
if (optionParts.length > 0) {
|