relq 1.0.2 → 1.0.4
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/add.cjs +403 -27
- package/dist/cjs/cli/commands/branch.cjs +13 -23
- package/dist/cjs/cli/commands/checkout.cjs +16 -29
- package/dist/cjs/cli/commands/cherry-pick.cjs +3 -4
- package/dist/cjs/cli/commands/commit.cjs +21 -29
- package/dist/cjs/cli/commands/diff.cjs +28 -32
- package/dist/cjs/cli/commands/export.cjs +7 -7
- package/dist/cjs/cli/commands/fetch.cjs +15 -21
- package/dist/cjs/cli/commands/generate.cjs +28 -54
- package/dist/cjs/cli/commands/history.cjs +19 -40
- package/dist/cjs/cli/commands/import.cjs +34 -41
- package/dist/cjs/cli/commands/init.cjs +69 -59
- package/dist/cjs/cli/commands/introspect.cjs +4 -8
- package/dist/cjs/cli/commands/log.cjs +26 -32
- package/dist/cjs/cli/commands/merge.cjs +24 -41
- package/dist/cjs/cli/commands/migrate.cjs +12 -25
- package/dist/cjs/cli/commands/pull.cjs +216 -106
- package/dist/cjs/cli/commands/push.cjs +35 -75
- package/dist/cjs/cli/commands/remote.cjs +2 -1
- package/dist/cjs/cli/commands/reset.cjs +22 -43
- package/dist/cjs/cli/commands/resolve.cjs +12 -14
- package/dist/cjs/cli/commands/rollback.cjs +16 -38
- package/dist/cjs/cli/commands/stash.cjs +5 -7
- package/dist/cjs/cli/commands/status.cjs +5 -10
- package/dist/cjs/cli/commands/sync.cjs +30 -50
- package/dist/cjs/cli/commands/tag.cjs +3 -4
- package/dist/cjs/cli/index.cjs +72 -9
- package/dist/cjs/cli/utils/change-tracker.cjs +107 -3
- package/dist/cjs/cli/utils/cli-utils.cjs +217 -0
- package/dist/cjs/cli/utils/config-loader.cjs +34 -8
- package/dist/cjs/cli/utils/fast-introspect.cjs +109 -3
- package/dist/cjs/cli/utils/git-utils.cjs +42 -161
- package/dist/cjs/cli/utils/pool-manager.cjs +156 -0
- package/dist/cjs/cli/utils/project-root.cjs +56 -5
- package/dist/cjs/cli/utils/relqignore.cjs +1 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +47 -0
- package/dist/cjs/cli/utils/schema-comparator.cjs +301 -11
- package/dist/cjs/cli/utils/schema-diff.cjs +202 -1
- package/dist/cjs/cli/utils/schema-hash.cjs +2 -1
- package/dist/cjs/cli/utils/schema-introspect.cjs +7 -3
- package/dist/cjs/cli/utils/snapshot-manager.cjs +1 -0
- package/dist/cjs/cli/utils/spinner.cjs +14 -106
- package/dist/cjs/cli/utils/sql-generator.cjs +10 -2
- package/dist/cjs/cli/utils/type-generator.cjs +28 -16
- package/dist/config.d.ts +16 -6
- package/dist/esm/cli/commands/add.js +372 -29
- package/dist/esm/cli/commands/branch.js +14 -24
- package/dist/esm/cli/commands/checkout.js +16 -29
- package/dist/esm/cli/commands/cherry-pick.js +3 -4
- package/dist/esm/cli/commands/commit.js +22 -30
- package/dist/esm/cli/commands/diff.js +6 -10
- package/dist/esm/cli/commands/export.js +8 -8
- package/dist/esm/cli/commands/fetch.js +14 -20
- package/dist/esm/cli/commands/generate.js +28 -54
- package/dist/esm/cli/commands/history.js +11 -32
- package/dist/esm/cli/commands/import.js +35 -42
- package/dist/esm/cli/commands/init.js +65 -55
- package/dist/esm/cli/commands/introspect.js +4 -8
- package/dist/esm/cli/commands/log.js +6 -12
- package/dist/esm/cli/commands/merge.js +20 -37
- package/dist/esm/cli/commands/migrate.js +12 -25
- package/dist/esm/cli/commands/pull.js +204 -94
- package/dist/esm/cli/commands/push.js +21 -61
- package/dist/esm/cli/commands/remote.js +2 -1
- package/dist/esm/cli/commands/reset.js +16 -37
- package/dist/esm/cli/commands/resolve.js +13 -15
- package/dist/esm/cli/commands/rollback.js +16 -38
- package/dist/esm/cli/commands/stash.js +6 -8
- package/dist/esm/cli/commands/status.js +6 -11
- package/dist/esm/cli/commands/sync.js +30 -50
- package/dist/esm/cli/commands/tag.js +3 -4
- package/dist/esm/cli/index.js +72 -9
- package/dist/esm/cli/utils/change-tracker.js +107 -3
- package/dist/esm/cli/utils/cli-utils.js +169 -0
- package/dist/esm/cli/utils/config-loader.js +34 -8
- package/dist/esm/cli/utils/fast-introspect.js +109 -3
- package/dist/esm/cli/utils/git-utils.js +2 -124
- package/dist/esm/cli/utils/pool-manager.js +114 -0
- package/dist/esm/cli/utils/project-root.js +55 -5
- package/dist/esm/cli/utils/relqignore.js +1 -0
- package/dist/esm/cli/utils/repo-manager.js +42 -0
- package/dist/esm/cli/utils/schema-comparator.js +301 -11
- package/dist/esm/cli/utils/schema-diff.js +202 -1
- package/dist/esm/cli/utils/schema-hash.js +2 -1
- package/dist/esm/cli/utils/schema-introspect.js +7 -3
- package/dist/esm/cli/utils/snapshot-manager.js +1 -0
- package/dist/esm/cli/utils/spinner.js +1 -101
- package/dist/esm/cli/utils/sql-generator.js +10 -2
- package/dist/esm/cli/utils/type-generator.js +28 -16
- package/dist/index.d.ts +25 -8
- package/dist/schema-builder.d.ts +18 -7
- package/package.json +1 -1
|
@@ -1,76 +1,56 @@
|
|
|
1
1
|
import { requireValidConfig } from "../utils/config-loader.js";
|
|
2
2
|
import { getConnectionDescription } from "../utils/env-loader.js";
|
|
3
|
-
import { colors, createSpinner } from "../utils/
|
|
4
|
-
import { isInitialized, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits,
|
|
3
|
+
import { colors, createSpinner, fatal, success } from "../utils/cli-utils.js";
|
|
4
|
+
import { isInitialized, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, } from "../utils/repo-manager.js";
|
|
5
5
|
import { pullCommand } from "./pull.js";
|
|
6
6
|
export async function syncCommand(context) {
|
|
7
7
|
const { config, flags } = context;
|
|
8
8
|
if (!config) {
|
|
9
|
-
|
|
10
|
-
process.exit(1);
|
|
9
|
+
fatal('No configuration found', `Run ${colors.cyan('relq init')} to create one.`);
|
|
11
10
|
}
|
|
12
|
-
requireValidConfig(config);
|
|
11
|
+
await requireValidConfig(config, { calledFrom: 'sync' });
|
|
13
12
|
const connection = config.connection;
|
|
14
|
-
const projectRoot =
|
|
13
|
+
const { projectRoot } = context;
|
|
15
14
|
console.log('');
|
|
16
15
|
if (!isInitialized(projectRoot)) {
|
|
17
|
-
|
|
18
|
-
console.log('');
|
|
19
|
-
console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
|
|
20
|
-
return;
|
|
16
|
+
fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
|
|
21
17
|
}
|
|
22
18
|
const spinner = createSpinner();
|
|
23
19
|
try {
|
|
24
|
-
|
|
25
|
-
const status = await getRepoStatus(connection, projectRoot);
|
|
26
|
-
spinner.stop();
|
|
27
|
-
console.log(`On database: ${colors.cyan(getConnectionDescription(connection))}`);
|
|
20
|
+
console.log(`Syncing with ${colors.cyan(getConnectionDescription(connection))}...`);
|
|
28
21
|
console.log('');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
22
|
+
await pullCommand(context);
|
|
23
|
+
await ensureRemoteTable(connection);
|
|
24
|
+
const remoteCommits = await fetchRemoteCommits(connection, 100);
|
|
25
|
+
const remoteHashes = new Set(remoteCommits.map(c => c.hash));
|
|
26
|
+
const localCommitsAfter = getAllCommits(projectRoot);
|
|
27
|
+
const toPush = localCommitsAfter.filter(c => !remoteHashes.has(c.hash));
|
|
28
|
+
if (toPush.length === 0) {
|
|
29
|
+
success('Sync complete - up to date');
|
|
37
30
|
console.log('');
|
|
38
31
|
return;
|
|
39
32
|
}
|
|
33
|
+
console.log(`Pushing ${toPush.length} local commit(s)...`);
|
|
40
34
|
console.log('');
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const pullContext = {
|
|
45
|
-
...context,
|
|
46
|
-
flags: { ...flags, force: true },
|
|
47
|
-
};
|
|
48
|
-
await pullCommand(pullContext);
|
|
35
|
+
spinner.start(`Pushing ${toPush.length} commit(s)...`);
|
|
36
|
+
for (const commit of toPush.reverse()) {
|
|
37
|
+
await pushCommit(connection, commit);
|
|
49
38
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
spinner.start(`Pushing ${toPush.length} commit(s)...`);
|
|
59
|
-
for (const commit of toPush.reverse()) {
|
|
60
|
-
await pushCommit(connection, commit);
|
|
61
|
-
}
|
|
62
|
-
spinner.succeed(`Pushed ${toPush.length} commit(s)`);
|
|
39
|
+
spinner.succeed(`Pushed ${toPush.length} commit(s)`);
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log('Pushed commits:');
|
|
42
|
+
for (const commit of toPush.slice(0, 5)) {
|
|
43
|
+
console.log(` ${colors.yellow(shortHash(commit.hash))} ${commit.message}`);
|
|
44
|
+
}
|
|
45
|
+
if (toPush.length > 5) {
|
|
46
|
+
console.log(` ${colors.muted(`... and ${toPush.length - 5} more`)}`);
|
|
63
47
|
}
|
|
64
48
|
console.log('');
|
|
65
|
-
|
|
66
|
-
const pushedText = status.aheadBy > 0 ? `${colors.green(`↑ ${status.aheadBy}`)} pushed` : '';
|
|
67
|
-
const separator = pulledText && pushedText ? ' | ' : '';
|
|
68
|
-
console.log(`${colors.green('✓')} Sync complete: ${pulledText}${separator}${pushedText}`);
|
|
49
|
+
success(`Sync complete - pushed ${toPush.length} commit(s)`);
|
|
69
50
|
}
|
|
70
|
-
catch (
|
|
51
|
+
catch (err) {
|
|
71
52
|
spinner.fail('Sync failed');
|
|
72
|
-
|
|
73
|
-
process.exit(1);
|
|
53
|
+
fatal(err instanceof Error ? err.message : String(err));
|
|
74
54
|
}
|
|
75
55
|
console.log('');
|
|
76
56
|
}
|
|
@@ -14,8 +14,7 @@ function saveTags(tags, projectRoot) {
|
|
|
14
14
|
fs.writeFileSync(tagPath, JSON.stringify(tags, null, 2));
|
|
15
15
|
}
|
|
16
16
|
export async function tagCommand(context) {
|
|
17
|
-
const { args, flags } = context;
|
|
18
|
-
const projectRoot = process.cwd();
|
|
17
|
+
const { args, flags, projectRoot } = context;
|
|
19
18
|
console.log('');
|
|
20
19
|
if (!isInitialized(projectRoot)) {
|
|
21
20
|
console.log(`${colors.red('fatal:')} not a relq repository`);
|
|
@@ -39,7 +38,7 @@ export async function tagCommand(context) {
|
|
|
39
38
|
}
|
|
40
39
|
delete tags[tagName];
|
|
41
40
|
saveTags(tags, projectRoot);
|
|
42
|
-
console.log(
|
|
41
|
+
console.log(`Deleted tag '${tagName}'`);
|
|
43
42
|
console.log('');
|
|
44
43
|
return;
|
|
45
44
|
}
|
|
@@ -87,7 +86,7 @@ export async function tagCommand(context) {
|
|
|
87
86
|
createdAt: new Date().toISOString(),
|
|
88
87
|
};
|
|
89
88
|
saveTags(tags, projectRoot);
|
|
90
|
-
console.log(
|
|
89
|
+
console.log(`Tagged ${colors.yellow(shortHash(hash))} as '${tagName}'`);
|
|
91
90
|
console.log(` ${commit.message}`);
|
|
92
91
|
console.log('');
|
|
93
92
|
return;
|
package/dist/esm/cli/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import { initCommand } from "./commands/init.js";
|
|
3
2
|
import { pullCommand } from "./commands/pull.js";
|
|
4
3
|
import { pushCommand } from "./commands/push.js";
|
|
@@ -22,6 +21,34 @@ import { mergeCommand } from "./commands/merge.js";
|
|
|
22
21
|
import { tagCommand } from "./commands/tag.js";
|
|
23
22
|
import { cherryPickCommand } from "./commands/cherry-pick.js";
|
|
24
23
|
import { remoteCommand } from "./commands/remote.js";
|
|
24
|
+
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
function loadEnvFile() {
|
|
27
|
+
let currentDir = process.cwd();
|
|
28
|
+
const root = path.parse(currentDir).root;
|
|
29
|
+
while (currentDir !== root) {
|
|
30
|
+
const envPath = path.join(currentDir, '.env');
|
|
31
|
+
if (fs.existsSync(envPath)) {
|
|
32
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
33
|
+
for (const line of envContent.split('\n')) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
36
|
+
continue;
|
|
37
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
38
|
+
if (match) {
|
|
39
|
+
const key = match[1].trim();
|
|
40
|
+
const value = match[2].trim().replace(/^["']|["']$/g, '');
|
|
41
|
+
if (!process.env[key]) {
|
|
42
|
+
process.env[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
currentDir = path.dirname(currentDir);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
loadEnvFile();
|
|
25
52
|
const VERSION = '1.1.0';
|
|
26
53
|
function parseArgs(argv) {
|
|
27
54
|
const args = [];
|
|
@@ -149,20 +176,56 @@ async function main() {
|
|
|
149
176
|
process.exit(1);
|
|
150
177
|
}
|
|
151
178
|
let config = null;
|
|
179
|
+
let resolvedProjectRoot = process.cwd();
|
|
152
180
|
if (requiresConfig(command)) {
|
|
153
181
|
const configPath = flags.config;
|
|
154
182
|
try {
|
|
155
183
|
const { loadConfigWithEnv, findConfigFileRecursive } = await import("./utils/config-loader.js");
|
|
156
|
-
const
|
|
184
|
+
const { findProjectRoot } = await import("./utils/project-root.js");
|
|
185
|
+
const configResult = configPath ? { path: configPath, directory: path.dirname(path.resolve(configPath)) } : findConfigFileRecursive();
|
|
186
|
+
if (!configResult) {
|
|
187
|
+
const projectRoot = findProjectRoot();
|
|
188
|
+
if (!projectRoot) {
|
|
189
|
+
const colors = {
|
|
190
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
191
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
192
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
193
|
+
};
|
|
194
|
+
console.error('');
|
|
195
|
+
console.error(colors.red('fatal:') + ' not a relq project (or any of the parent directories)');
|
|
196
|
+
console.error('');
|
|
197
|
+
console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} in your project directory to initialize relq.`);
|
|
198
|
+
console.error('');
|
|
199
|
+
process.exit(128);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
resolvedProjectRoot = configResult?.directory || findProjectRoot() || process.cwd();
|
|
203
|
+
const foundConfig = configResult?.path;
|
|
157
204
|
if (!foundConfig) {
|
|
158
|
-
|
|
159
|
-
|
|
205
|
+
const colors = {
|
|
206
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
207
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
208
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
209
|
+
};
|
|
210
|
+
console.error('');
|
|
211
|
+
console.error(colors.red('error:') + ' relq.config.ts not found');
|
|
212
|
+
console.error('');
|
|
213
|
+
console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} to create one or use ${colors.cyan('--config')} to specify a path.`);
|
|
214
|
+
console.error('');
|
|
160
215
|
process.exit(1);
|
|
161
216
|
}
|
|
162
217
|
config = await loadConfigWithEnv(configPath);
|
|
163
218
|
if (requiresDbConnection(command, flags) && !config.connection?.host && !config.connection?.url) {
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
const colors = {
|
|
220
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
221
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
222
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
223
|
+
};
|
|
224
|
+
console.error('');
|
|
225
|
+
console.error(colors.red('error:') + ' No database connection configured');
|
|
226
|
+
console.error('');
|
|
227
|
+
console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} or set DATABASE_* environment variables.`);
|
|
228
|
+
console.error('');
|
|
166
229
|
process.exit(1);
|
|
167
230
|
}
|
|
168
231
|
}
|
|
@@ -171,7 +234,7 @@ async function main() {
|
|
|
171
234
|
process.exit(1);
|
|
172
235
|
}
|
|
173
236
|
}
|
|
174
|
-
const context = { config, args, flags };
|
|
237
|
+
const context = { config, args, flags, projectRoot: resolvedProjectRoot };
|
|
175
238
|
try {
|
|
176
239
|
switch (command) {
|
|
177
240
|
case 'init':
|
|
@@ -261,7 +324,7 @@ async function main() {
|
|
|
261
324
|
theirs: Boolean(flags.theirs),
|
|
262
325
|
ours: Boolean(flags.ours),
|
|
263
326
|
abort: Boolean(flags.abort),
|
|
264
|
-
});
|
|
327
|
+
}, resolvedProjectRoot);
|
|
265
328
|
break;
|
|
266
329
|
case 'export':
|
|
267
330
|
await exportCommand(context);
|
|
@@ -281,4 +344,4 @@ async function main() {
|
|
|
281
344
|
process.exit(1);
|
|
282
345
|
}
|
|
283
346
|
}
|
|
284
|
-
main
|
|
347
|
+
export { main };
|
|
@@ -31,6 +31,12 @@ export function generateChangeSQL(change) {
|
|
|
31
31
|
return generateConstraintSQL(change);
|
|
32
32
|
case 'PARTITION':
|
|
33
33
|
return generatePartitionSQL(change);
|
|
34
|
+
case 'PARTITION_CHILD':
|
|
35
|
+
return generatePartitionChildSQL(change);
|
|
36
|
+
case 'TABLE_COMMENT':
|
|
37
|
+
return generateTableCommentSQL(change);
|
|
38
|
+
case 'COLUMN_COMMENT':
|
|
39
|
+
return generateColumnCommentSQL(change);
|
|
34
40
|
case 'VIEW':
|
|
35
41
|
return generateViewSQL(change);
|
|
36
42
|
case 'MATERIALIZED_VIEW':
|
|
@@ -44,6 +50,8 @@ export function generateChangeSQL(change) {
|
|
|
44
50
|
return generateForeignServerSQL(change);
|
|
45
51
|
case 'FOREIGN_TABLE':
|
|
46
52
|
return generateForeignTableSQL(change);
|
|
53
|
+
case 'COLLATION':
|
|
54
|
+
return generateCollationSQL(change);
|
|
47
55
|
default:
|
|
48
56
|
return `-- Unsupported object type: ${change.objectType}`;
|
|
49
57
|
}
|
|
@@ -144,8 +152,11 @@ function generateTableSQL(change) {
|
|
|
144
152
|
}
|
|
145
153
|
const allDefs = [...colDefs, ...constraintDefs].join(',\n');
|
|
146
154
|
let sql = `CREATE TABLE "${data.name}" (\n${allDefs}\n)`;
|
|
147
|
-
if (data.isPartitioned && data.partitionType && data.partitionKey
|
|
148
|
-
|
|
155
|
+
if (data.isPartitioned && data.partitionType && data.partitionKey) {
|
|
156
|
+
const keyArr = Array.isArray(data.partitionKey) ? data.partitionKey : [data.partitionKey];
|
|
157
|
+
if (keyArr.length) {
|
|
158
|
+
sql += ` PARTITION BY ${data.partitionType} (${keyArr.join(', ')})`;
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
return sql + ';';
|
|
151
162
|
}
|
|
@@ -226,6 +237,9 @@ function generateConstraintSQL(change) {
|
|
|
226
237
|
return '';
|
|
227
238
|
}
|
|
228
239
|
function generatePartitionSQL(change) {
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
function generatePartitionChildSQL(change) {
|
|
229
243
|
const data = change.after;
|
|
230
244
|
if (change.type === 'CREATE' && data) {
|
|
231
245
|
return `CREATE TABLE "${data.name}" PARTITION OF "${data.parentTable}" ${data.bound};`;
|
|
@@ -235,6 +249,33 @@ function generatePartitionSQL(change) {
|
|
|
235
249
|
}
|
|
236
250
|
return '';
|
|
237
251
|
}
|
|
252
|
+
function generateTableCommentSQL(change) {
|
|
253
|
+
const data = change.after;
|
|
254
|
+
if ((change.type === 'CREATE' || change.type === 'ALTER') && data) {
|
|
255
|
+
const escaped = data.comment.replace(/'/g, "''");
|
|
256
|
+
return `COMMENT ON TABLE "${data.tableName}" IS '${escaped}';`;
|
|
257
|
+
}
|
|
258
|
+
else if (change.type === 'DROP') {
|
|
259
|
+
const beforeData = change.before;
|
|
260
|
+
const tableName = beforeData?.tableName || change.objectName;
|
|
261
|
+
return `COMMENT ON TABLE "${tableName}" IS NULL;`;
|
|
262
|
+
}
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
function generateColumnCommentSQL(change) {
|
|
266
|
+
const data = change.after;
|
|
267
|
+
if ((change.type === 'CREATE' || change.type === 'ALTER') && data) {
|
|
268
|
+
const escaped = data.comment.replace(/'/g, "''");
|
|
269
|
+
return `COMMENT ON COLUMN "${data.tableName}"."${data.columnName}" IS '${escaped}';`;
|
|
270
|
+
}
|
|
271
|
+
else if (change.type === 'DROP') {
|
|
272
|
+
const beforeData = change.before;
|
|
273
|
+
const tableName = beforeData?.tableName || change.parentName || '';
|
|
274
|
+
const columnName = beforeData?.columnName || change.objectName;
|
|
275
|
+
return `COMMENT ON COLUMN "${tableName}"."${columnName}" IS NULL;`;
|
|
276
|
+
}
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
238
279
|
function generateViewSQL(change) {
|
|
239
280
|
const data = change.after;
|
|
240
281
|
if (change.type === 'CREATE' && data) {
|
|
@@ -320,6 +361,28 @@ function generateForeignTableSQL(change) {
|
|
|
320
361
|
}
|
|
321
362
|
return '';
|
|
322
363
|
}
|
|
364
|
+
function generateCollationSQL(change) {
|
|
365
|
+
const data = change.after;
|
|
366
|
+
if (change.type === 'CREATE' && data) {
|
|
367
|
+
const options = [];
|
|
368
|
+
if (data.locale)
|
|
369
|
+
options.push(`LOCALE = '${data.locale}'`);
|
|
370
|
+
if (data.lcCollate)
|
|
371
|
+
options.push(`LC_COLLATE = '${data.lcCollate}'`);
|
|
372
|
+
if (data.lcCtype)
|
|
373
|
+
options.push(`LC_CTYPE = '${data.lcCtype}'`);
|
|
374
|
+
if (data.provider)
|
|
375
|
+
options.push(`PROVIDER = ${data.provider}`);
|
|
376
|
+
if (data.deterministic !== undefined) {
|
|
377
|
+
options.push(`DETERMINISTIC = ${data.deterministic ? 'TRUE' : 'FALSE'}`);
|
|
378
|
+
}
|
|
379
|
+
return `CREATE COLLATION IF NOT EXISTS "${data.name}" (${options.join(', ')});`;
|
|
380
|
+
}
|
|
381
|
+
else if (change.type === 'DROP') {
|
|
382
|
+
return `DROP COLLATION IF EXISTS "${change.objectName}";`;
|
|
383
|
+
}
|
|
384
|
+
return '';
|
|
385
|
+
}
|
|
323
386
|
export function createChange(type, objectType, objectName, before, after, parentName) {
|
|
324
387
|
const id = generateChangeId(type, objectType, objectName, parentName);
|
|
325
388
|
const change = {
|
|
@@ -341,6 +404,42 @@ export function getChangeDisplayName(change) {
|
|
|
341
404
|
change.type === 'DROP' ? '-' :
|
|
342
405
|
change.type === 'ALTER' ? '~' :
|
|
343
406
|
'>';
|
|
407
|
+
if (change.objectType === 'INDEX') {
|
|
408
|
+
const data = change.after;
|
|
409
|
+
const tableName = data?.tableName || change.parentName;
|
|
410
|
+
if (tableName) {
|
|
411
|
+
return `${prefix} ${change.objectType} ${change.objectName} on ${tableName}`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (change.objectType === 'PARTITION') {
|
|
415
|
+
const data = change.after;
|
|
416
|
+
if (data?.tableName && data?.type && data?.key) {
|
|
417
|
+
let keyStr = Array.isArray(data.key) ? data.key.join(', ') : data.key;
|
|
418
|
+
keyStr = keyStr.replace(/[{}]/g, '');
|
|
419
|
+
return `${prefix} PARTITIONED ${data.tableName} by ${data.type.toUpperCase()}(${keyStr})`;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (change.objectType === 'PARTITION_CHILD') {
|
|
423
|
+
const data = change.after;
|
|
424
|
+
if (data?.name && data?.parentTable) {
|
|
425
|
+
const bound = data.bound || '';
|
|
426
|
+
return `${prefix} PARTITION ${data.name} of ${data.parentTable} ${bound}`;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (change.objectType === 'CHECK') {
|
|
430
|
+
const actionWord = change.type === 'CREATE' ? 'ADD' : change.type === 'DROP' ? 'DROP' : 'ALTER';
|
|
431
|
+
if (change.parentName) {
|
|
432
|
+
return `${prefix} ${actionWord} CHECK ${change.objectName} on ${change.parentName}`;
|
|
433
|
+
}
|
|
434
|
+
return `${prefix} ${actionWord} CHECK ${change.objectName}`;
|
|
435
|
+
}
|
|
436
|
+
if (change.objectType === 'PRIMARY_KEY' || change.objectType === 'FOREIGN_KEY' || change.objectType === 'EXCLUSION') {
|
|
437
|
+
const actionWord = change.type === 'CREATE' ? 'ADD' : change.type === 'DROP' ? 'DROP' : 'ALTER';
|
|
438
|
+
if (change.parentName) {
|
|
439
|
+
return `${prefix} ${actionWord} ${change.objectType.replace('_', ' ')} ${change.objectName} on ${change.parentName}`;
|
|
440
|
+
}
|
|
441
|
+
return `${prefix} ${actionWord} ${change.objectType.replace('_', ' ')} ${change.objectName}`;
|
|
442
|
+
}
|
|
344
443
|
if (change.parentName) {
|
|
345
444
|
return `${prefix} ${change.objectType} ${change.parentName}.${change.objectName}`;
|
|
346
445
|
}
|
|
@@ -350,14 +449,18 @@ export function sortChangesByDependency(changes) {
|
|
|
350
449
|
const order = {
|
|
351
450
|
'EXTENSION': 1,
|
|
352
451
|
'SCHEMA': 2,
|
|
452
|
+
'SCHEMA_FILE': 0,
|
|
353
453
|
'ENUM': 3,
|
|
354
454
|
'DOMAIN': 4,
|
|
355
455
|
'COMPOSITE_TYPE': 5,
|
|
356
456
|
'SEQUENCE': 6,
|
|
357
457
|
'FOREIGN_SERVER': 7,
|
|
358
458
|
'TABLE': 10,
|
|
459
|
+
'TABLE_COMMENT': 10,
|
|
359
460
|
'PARTITION': 11,
|
|
360
|
-
'
|
|
461
|
+
'PARTITION_CHILD': 12,
|
|
462
|
+
'COLUMN': 13,
|
|
463
|
+
'COLUMN_COMMENT': 12,
|
|
361
464
|
'INDEX': 13,
|
|
362
465
|
'CONSTRAINT': 14,
|
|
363
466
|
'PRIMARY_KEY': 14,
|
|
@@ -370,6 +473,7 @@ export function sortChangesByDependency(changes) {
|
|
|
370
473
|
'PROCEDURE': 31,
|
|
371
474
|
'TRIGGER': 40,
|
|
372
475
|
'ENUM_VALUE': 3,
|
|
476
|
+
'COLLATION': 4,
|
|
373
477
|
'FOREIGN_TABLE': 8,
|
|
374
478
|
};
|
|
375
479
|
return [...changes].sort((a, b) => {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
3
|
+
export const colors = {
|
|
4
|
+
reset: '\x1b[0m',
|
|
5
|
+
red: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
|
|
6
|
+
green: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
|
|
7
|
+
yellow: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
|
|
8
|
+
blue: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
|
|
9
|
+
magenta: (s) => isColorSupported ? `\x1b[35m${s}\x1b[0m` : s,
|
|
10
|
+
cyan: (s) => isColorSupported ? `\x1b[36m${s}\x1b[0m` : s,
|
|
11
|
+
white: (s) => isColorSupported ? `\x1b[37m${s}\x1b[0m` : s,
|
|
12
|
+
gray: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
|
|
13
|
+
bold: (s) => isColorSupported ? `\x1b[1m${s}\x1b[0m` : s,
|
|
14
|
+
dim: (s) => isColorSupported ? `\x1b[2m${s}\x1b[0m` : s,
|
|
15
|
+
muted: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
|
|
16
|
+
success: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
|
|
17
|
+
error: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
|
|
18
|
+
warning: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
|
|
19
|
+
info: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
|
|
20
|
+
};
|
|
21
|
+
export function createSpinner() {
|
|
22
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
|
+
let frameIndex = 0;
|
|
24
|
+
let interval = null;
|
|
25
|
+
let currentMessage = '';
|
|
26
|
+
let isSpinning = false;
|
|
27
|
+
const isTTY = process.stdout.isTTY;
|
|
28
|
+
const clearLine = () => {
|
|
29
|
+
if (isTTY)
|
|
30
|
+
process.stdout.write('\r\x1b[K');
|
|
31
|
+
};
|
|
32
|
+
const render = () => {
|
|
33
|
+
if (!isSpinning)
|
|
34
|
+
return;
|
|
35
|
+
clearLine();
|
|
36
|
+
process.stdout.write(`${colors.cyan(frames[frameIndex])} ${currentMessage}`);
|
|
37
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
start(message) {
|
|
41
|
+
if (isSpinning)
|
|
42
|
+
this.stop();
|
|
43
|
+
currentMessage = message;
|
|
44
|
+
isSpinning = true;
|
|
45
|
+
frameIndex = 0;
|
|
46
|
+
if (isTTY) {
|
|
47
|
+
interval = setInterval(render, 80);
|
|
48
|
+
render();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`${message}...`);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
update(message) {
|
|
55
|
+
currentMessage = message;
|
|
56
|
+
},
|
|
57
|
+
succeed(message) {
|
|
58
|
+
this.stop();
|
|
59
|
+
console.log(message || currentMessage);
|
|
60
|
+
},
|
|
61
|
+
fail(message) {
|
|
62
|
+
this.stop();
|
|
63
|
+
},
|
|
64
|
+
info(message) {
|
|
65
|
+
this.stop();
|
|
66
|
+
console.log(message || currentMessage);
|
|
67
|
+
},
|
|
68
|
+
warn(message) {
|
|
69
|
+
this.stop();
|
|
70
|
+
warning(message || currentMessage);
|
|
71
|
+
},
|
|
72
|
+
stop() {
|
|
73
|
+
if (interval) {
|
|
74
|
+
clearInterval(interval);
|
|
75
|
+
interval = null;
|
|
76
|
+
}
|
|
77
|
+
if (isSpinning && isTTY) {
|
|
78
|
+
clearLine();
|
|
79
|
+
}
|
|
80
|
+
isSpinning = false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function fatal(message, hintMessage) {
|
|
85
|
+
console.error(`${colors.red('fatal:')} ${message}`);
|
|
86
|
+
if (hintMessage) {
|
|
87
|
+
hint(hintMessage);
|
|
88
|
+
}
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
export function error(message, hintMessage) {
|
|
92
|
+
console.error(`${colors.red('error:')} ${message}`);
|
|
93
|
+
if (hintMessage) {
|
|
94
|
+
hint(hintMessage);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function warning(message) {
|
|
98
|
+
console.error(`${colors.yellow('warning:')} ${message}`);
|
|
99
|
+
}
|
|
100
|
+
export function hint(message) {
|
|
101
|
+
console.error(`${colors.yellow('hint:')} ${message}`);
|
|
102
|
+
}
|
|
103
|
+
export function success(message) {
|
|
104
|
+
console.log(colors.green(message));
|
|
105
|
+
}
|
|
106
|
+
export function confirm(question, defaultYes = true) {
|
|
107
|
+
const rl = readline.createInterface({
|
|
108
|
+
input: process.stdin,
|
|
109
|
+
output: process.stdout,
|
|
110
|
+
});
|
|
111
|
+
const suffix = defaultYes ? '[Y/n]' : '[y/N]';
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
rl.question(`${question} ${suffix} `, (answer) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
const a = answer.trim().toLowerCase();
|
|
116
|
+
if (!a)
|
|
117
|
+
resolve(defaultYes);
|
|
118
|
+
else
|
|
119
|
+
resolve(a === 'y' || a === 'yes');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export function select(question, options) {
|
|
124
|
+
const rl = readline.createInterface({
|
|
125
|
+
input: process.stdin,
|
|
126
|
+
output: process.stdout,
|
|
127
|
+
});
|
|
128
|
+
console.log(question);
|
|
129
|
+
options.forEach((opt, i) => {
|
|
130
|
+
console.log(` ${i + 1}) ${opt}`);
|
|
131
|
+
});
|
|
132
|
+
return new Promise((resolve) => {
|
|
133
|
+
rl.question(`Select [1-${options.length}]: `, (answer) => {
|
|
134
|
+
rl.close();
|
|
135
|
+
const num = parseInt(answer.trim(), 10);
|
|
136
|
+
if (num >= 1 && num <= options.length) {
|
|
137
|
+
resolve(num - 1);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
resolve(0);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export function formatBytes(bytes) {
|
|
146
|
+
if (bytes < 1024)
|
|
147
|
+
return `${bytes} B`;
|
|
148
|
+
if (bytes < 1024 * 1024)
|
|
149
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
150
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
151
|
+
}
|
|
152
|
+
export function formatDuration(ms) {
|
|
153
|
+
if (ms < 1000)
|
|
154
|
+
return `${ms}ms`;
|
|
155
|
+
if (ms < 60000)
|
|
156
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
157
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
158
|
+
}
|
|
159
|
+
export function progressBar(current, total, width = 30) {
|
|
160
|
+
const percentage = Math.min(100, Math.round((current / total) * 100));
|
|
161
|
+
const filled = Math.round((percentage / 100) * width);
|
|
162
|
+
const empty = width - filled;
|
|
163
|
+
return `${'#'.repeat(filled)}${'.'.repeat(empty)} ${percentage}%`;
|
|
164
|
+
}
|
|
165
|
+
export function requireInit(isInitialized, projectRoot) {
|
|
166
|
+
if (!isInitialized) {
|
|
167
|
+
fatal('not a relq repository (or any of the parent directories): .relq', "run 'relq init' to initialize a repository");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -64,7 +64,7 @@ export function validateConfig(config) {
|
|
|
64
64
|
if (!config.connection?.host && !config.connection?.url) {
|
|
65
65
|
errors.push('No database connection configured. Set connection in relq.config.ts or use DATABASE_* env vars.');
|
|
66
66
|
}
|
|
67
|
-
const hasSchemaPath = typeof config.schema === 'string';
|
|
67
|
+
const hasSchemaPath = typeof config.schema === 'string' && config.schema.length > 0;
|
|
68
68
|
const hasSchemaDir = typeof config.schema === 'object' && config.schema?.directory;
|
|
69
69
|
const hasTypeGenOutput = config.typeGeneration?.output;
|
|
70
70
|
const hasGenerateOutDir = config.generate?.outDir;
|
|
@@ -73,14 +73,40 @@ export function validateConfig(config) {
|
|
|
73
73
|
}
|
|
74
74
|
return errors;
|
|
75
75
|
}
|
|
76
|
-
export function requireValidConfig(config) {
|
|
76
|
+
export async function requireValidConfig(config, options) {
|
|
77
77
|
const errors = validateConfig(config);
|
|
78
|
-
if (errors.length
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
if (errors.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
const colors = {
|
|
81
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
82
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
83
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
84
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
85
|
+
};
|
|
86
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
87
|
+
if (options?.autoComplete !== false && isInteractive) {
|
|
88
|
+
try {
|
|
89
|
+
const { initCommand } = await import("../commands/init.js");
|
|
90
|
+
const { findProjectRoot } = await import("./project-root.js");
|
|
91
|
+
const projectRoot = findProjectRoot() || process.cwd();
|
|
92
|
+
const flags = {};
|
|
93
|
+
if (options?.calledFrom) {
|
|
94
|
+
flags['called-from'] = options.calledFrom;
|
|
95
|
+
}
|
|
96
|
+
await initCommand({ args: [], flags, config, projectRoot });
|
|
97
|
+
return;
|
|
82
98
|
}
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
catch (e) {
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
console.error('');
|
|
104
|
+
console.error(colors.red('error:') + ' Configuration errors:');
|
|
105
|
+
for (const error of errors) {
|
|
106
|
+
console.error(` ${colors.yellow('•')} ${error}`);
|
|
85
107
|
}
|
|
108
|
+
console.error('');
|
|
109
|
+
console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} to create a configuration file or check your config settings.`);
|
|
110
|
+
console.error('');
|
|
111
|
+
process.exit(1);
|
|
86
112
|
}
|