relq 1.0.25 → 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.
Files changed (52) hide show
  1. package/dist/cjs/cli/commands/commit.cjs +80 -0
  2. package/dist/cjs/cli/commands/import.cjs +1 -0
  3. package/dist/cjs/cli/commands/pull.cjs +8 -25
  4. package/dist/cjs/cli/commands/push.cjs +48 -8
  5. package/dist/cjs/cli/commands/rollback.cjs +205 -84
  6. package/dist/cjs/cli/commands/schema-ast.cjs +219 -0
  7. package/dist/cjs/cli/index.cjs +6 -0
  8. package/dist/cjs/cli/utils/ast-codegen.cjs +95 -3
  9. package/dist/cjs/cli/utils/ast-transformer.cjs +12 -0
  10. package/dist/cjs/cli/utils/change-tracker.cjs +135 -0
  11. package/dist/cjs/cli/utils/commit-manager.cjs +54 -0
  12. package/dist/cjs/cli/utils/migration-generator.cjs +319 -0
  13. package/dist/cjs/cli/utils/repo-manager.cjs +99 -3
  14. package/dist/cjs/cli/utils/schema-diff.cjs +390 -0
  15. package/dist/cjs/cli/utils/schema-hash.cjs +4 -0
  16. package/dist/cjs/cli/utils/schema-to-ast.cjs +477 -0
  17. package/dist/cjs/schema-definition/column-types.cjs +50 -4
  18. package/dist/cjs/schema-definition/pg-enum.cjs +10 -0
  19. package/dist/cjs/schema-definition/pg-function.cjs +19 -0
  20. package/dist/cjs/schema-definition/pg-sequence.cjs +22 -1
  21. package/dist/cjs/schema-definition/pg-trigger.cjs +39 -0
  22. package/dist/cjs/schema-definition/pg-view.cjs +17 -0
  23. package/dist/cjs/schema-definition/sql-expressions.cjs +3 -0
  24. package/dist/cjs/schema-definition/table-definition.cjs +4 -0
  25. package/dist/config.d.ts +98 -0
  26. package/dist/esm/cli/commands/commit.js +83 -3
  27. package/dist/esm/cli/commands/import.js +1 -0
  28. package/dist/esm/cli/commands/pull.js +9 -26
  29. package/dist/esm/cli/commands/push.js +49 -9
  30. package/dist/esm/cli/commands/rollback.js +206 -85
  31. package/dist/esm/cli/commands/schema-ast.js +183 -0
  32. package/dist/esm/cli/index.js +6 -0
  33. package/dist/esm/cli/utils/ast-codegen.js +93 -3
  34. package/dist/esm/cli/utils/ast-transformer.js +12 -0
  35. package/dist/esm/cli/utils/change-tracker.js +134 -0
  36. package/dist/esm/cli/utils/commit-manager.js +51 -0
  37. package/dist/esm/cli/utils/migration-generator.js +318 -0
  38. package/dist/esm/cli/utils/repo-manager.js +96 -3
  39. package/dist/esm/cli/utils/schema-diff.js +389 -0
  40. package/dist/esm/cli/utils/schema-hash.js +4 -0
  41. package/dist/esm/cli/utils/schema-to-ast.js +447 -0
  42. package/dist/esm/schema-definition/column-types.js +50 -4
  43. package/dist/esm/schema-definition/pg-enum.js +10 -0
  44. package/dist/esm/schema-definition/pg-function.js +19 -0
  45. package/dist/esm/schema-definition/pg-sequence.js +22 -1
  46. package/dist/esm/schema-definition/pg-trigger.js +39 -0
  47. package/dist/esm/schema-definition/pg-view.js +17 -0
  48. package/dist/esm/schema-definition/sql-expressions.js +3 -0
  49. package/dist/esm/schema-definition/table-definition.js +4 -0
  50. package/dist/index.d.ts +98 -0
  51. package/dist/schema-builder.d.ts +223 -24
  52. 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, confirm, fatal, warning } from "../utils/cli-utils.js";
6
- function parseMigration(content) {
7
- const upMatch = content.match(/--\s*UP\s*\n([\s\S]*?)(?=--\s*DOWN|$)/i);
8
- const downMatch = content.match(/--\s*DOWN\s*\n([\s\S]*?)$/i);
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', `run ${colors.cyan('relq init')} to create a configuration file`);
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 migrationsDir = config.migrations?.directory || './migrations';
23
- const tableName = config.migrations?.tableName || '_relq_migrations';
24
- const count = parseInt(args[0]) || 1;
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
- const { Pool } = await import("../../addon/pg/index.js");
32
- const pool = new Pool({
33
- host: connection.host,
34
- port: connection.port || 5432,
35
- database: connection.database,
36
- user: connection.user,
37
- password: connection.password,
38
- connectionString: connection.url,
39
- });
40
- try {
41
- const result = await pool.query(`
42
- SELECT name FROM "${tableName}"
43
- ORDER BY id DESC
44
- LIMIT $1;
45
- `, [count]);
46
- if (result.rows.length === 0) {
47
- console.log('No migrations to rollback.');
48
- return;
49
- }
50
- const toRollback = result.rows.map(r => r.name);
51
- console.log('Migrations to rollback:');
52
- for (const name of toRollback) {
53
- console.log(` - ${name}`);
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
- if (!force && !dryRun) {
57
- const proceed = await confirm(`${colors.red('This will undo ' + toRollback.length + ' migration(s). Continue?')}`, false);
58
- if (!proceed) {
59
- console.log('Cancelled.');
60
- return;
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
- for (const name of toRollback) {
64
- const filePath = path.join(migrationsDir, name);
65
- if (!fs.existsSync(filePath)) {
66
- warning(`Migration file not found: ${name}`);
67
- continue;
68
- }
69
- const content = fs.readFileSync(filePath, 'utf-8');
70
- const { down } = parseMigration(content);
71
- if (!down) {
72
- warning(`No DOWN section in: ${name}`);
73
- continue;
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 (dryRun) {
76
- console.log(`[dry-run] Would rollback: ${name}`);
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
- else {
81
- console.log(`Rolling back: ${name}...`);
82
- const client = await pool.connect();
83
- try {
84
- await client.query('BEGIN');
85
- await client.query(down);
86
- await client.query(`DELETE FROM "${tableName}" WHERE name = $1`, [name]);
87
- await client.query('COMMIT');
88
- console.log(' Rolled back');
89
- }
90
- catch (error) {
91
- await client.query('ROLLBACK');
92
- throw error;
93
- }
94
- finally {
95
- client.release();
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
- if (!dryRun) {
100
- console.log('');
101
- console.log(`Rolled back ${toRollback.length} migration(s).`);
146
+ finally {
147
+ await client.end();
102
148
  }
103
149
  }
104
- finally {
105
- await pool.end();
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
- catch (error) {
109
- fatal('Rollback failed', error instanceof Error ? error.message : String(error));
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;
@@ -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) {