relq 1.0.5 → 1.0.7
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 +257 -17
- package/dist/cjs/cli/commands/commit.cjs +13 -2
- package/dist/cjs/cli/commands/export.cjs +25 -19
- package/dist/cjs/cli/commands/import.cjs +219 -100
- package/dist/cjs/cli/commands/init.cjs +86 -14
- package/dist/cjs/cli/commands/pull.cjs +104 -23
- package/dist/cjs/cli/commands/push.cjs +38 -3
- package/dist/cjs/cli/index.cjs +9 -1
- package/dist/cjs/cli/utils/ast/codegen/builder.cjs +297 -0
- package/dist/cjs/cli/utils/ast/codegen/constraints.cjs +185 -0
- package/dist/cjs/cli/utils/ast/codegen/defaults.cjs +311 -0
- package/dist/cjs/cli/utils/ast/codegen/index.cjs +24 -0
- package/dist/cjs/cli/utils/ast/codegen/type-map.cjs +116 -0
- package/dist/cjs/cli/utils/ast/codegen/utils.cjs +69 -0
- package/dist/cjs/cli/utils/ast/index.cjs +19 -0
- package/dist/cjs/cli/utils/ast/transformer/helpers.cjs +154 -0
- package/dist/cjs/cli/utils/ast/transformer/index.cjs +25 -0
- package/dist/cjs/cli/utils/ast/types.cjs +2 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +949 -0
- package/dist/cjs/cli/utils/ast-transformer.cjs +916 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +50 -1
- package/dist/cjs/cli/utils/cli-utils.cjs +151 -0
- package/dist/cjs/cli/utils/fast-introspect.cjs +149 -23
- package/dist/cjs/cli/utils/pg-parser.cjs +1 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +121 -4
- package/dist/cjs/cli/utils/schema-comparator.cjs +98 -14
- package/dist/cjs/cli/utils/schema-introspect.cjs +56 -19
- package/dist/cjs/cli/utils/snapshot-manager.cjs +0 -1
- package/dist/cjs/cli/utils/sql-generator.cjs +353 -64
- package/dist/cjs/cli/utils/type-generator.cjs +114 -15
- package/dist/cjs/config/config.cjs +29 -10
- package/dist/cjs/core/relq-client.cjs +22 -6
- package/dist/cjs/schema-definition/column-types.cjs +149 -13
- package/dist/cjs/schema-definition/defaults.cjs +72 -0
- package/dist/cjs/schema-definition/index.cjs +15 -1
- package/dist/cjs/schema-definition/introspection.cjs +7 -3
- package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
- package/dist/cjs/schema-definition/pg-view.cjs +30 -0
- package/dist/cjs/schema-definition/table-definition.cjs +110 -4
- package/dist/cjs/types/config-types.cjs +13 -4
- package/dist/cjs/utils/aws-dsql.cjs +177 -0
- package/dist/config.d.ts +147 -2
- package/dist/esm/cli/commands/add.js +255 -18
- package/dist/esm/cli/commands/commit.js +13 -2
- package/dist/esm/cli/commands/export.js +25 -19
- package/dist/esm/cli/commands/import.js +221 -102
- package/dist/esm/cli/commands/init.js +86 -14
- package/dist/esm/cli/commands/pull.js +106 -25
- package/dist/esm/cli/commands/push.js +39 -4
- package/dist/esm/cli/index.js +9 -1
- package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
- package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
- package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
- package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
- package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
- package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
- package/dist/esm/cli/utils/ast/index.js +3 -0
- package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
- package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
- package/dist/esm/cli/utils/ast/types.js +1 -0
- package/dist/esm/cli/utils/ast-codegen.js +945 -0
- package/dist/esm/cli/utils/ast-transformer.js +907 -0
- package/dist/esm/cli/utils/change-tracker.js +50 -1
- package/dist/esm/cli/utils/cli-utils.js +147 -0
- package/dist/esm/cli/utils/fast-introspect.js +149 -23
- package/dist/esm/cli/utils/pg-parser.js +1 -0
- package/dist/esm/cli/utils/repo-manager.js +114 -4
- package/dist/esm/cli/utils/schema-comparator.js +98 -14
- package/dist/esm/cli/utils/schema-introspect.js +56 -19
- package/dist/esm/cli/utils/snapshot-manager.js +0 -1
- package/dist/esm/cli/utils/sql-generator.js +353 -64
- package/dist/esm/cli/utils/type-generator.js +114 -15
- package/dist/esm/config/config.js +29 -10
- package/dist/esm/core/relq-client.js +23 -7
- package/dist/esm/schema-definition/column-types.js +146 -12
- package/dist/esm/schema-definition/defaults.js +69 -0
- package/dist/esm/schema-definition/index.js +3 -0
- package/dist/esm/schema-definition/introspection.js +7 -3
- package/dist/esm/schema-definition/pg-relations.js +161 -0
- package/dist/esm/schema-definition/pg-view.js +24 -0
- package/dist/esm/schema-definition/table-definition.js +110 -4
- package/dist/esm/types/config-types.js +12 -4
- package/dist/esm/utils/aws-dsql.js +139 -0
- package/dist/index.d.ts +159 -1
- package/dist/schema-builder.d.ts +1314 -32
- package/package.json +1 -1
|
@@ -2,11 +2,12 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { requireValidConfig } from "../utils/config-loader.js";
|
|
4
4
|
import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
|
|
5
|
-
import {
|
|
5
|
+
import { introspectedToParsedSchema } from "../utils/ast-transformer.js";
|
|
6
|
+
import { generateTypeScriptFromAST } from "../utils/ast-codegen.js";
|
|
6
7
|
import { getConnectionDescription } from "../utils/env-loader.js";
|
|
7
|
-
import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning } from "../utils/cli-utils.js";
|
|
8
|
+
import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning, createMultiProgress } from "../utils/cli-utils.js";
|
|
8
9
|
import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
|
|
9
|
-
import { isInitialized, initRepository, getHead, saveSnapshot, loadSnapshot, createCommit, shortHash, fetchRemoteCommits, ensureRemoteTable, setFetchHead, addUnstagedChanges, getStagedChanges, getUnstagedChanges, clearWorkingState, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
|
|
10
|
+
import { isInitialized, initRepository, getHead, saveSnapshot, loadSnapshot, createCommit, shortHash, fetchRemoteCommits, ensureRemoteTable, setFetchHead, addUnstagedChanges, getStagedChanges, getUnstagedChanges, clearWorkingState, hashFileContent, saveFileHash, markCommitAsPulled, } from "../utils/repo-manager.js";
|
|
10
11
|
import { compareSchemas } from "../utils/schema-comparator.js";
|
|
11
12
|
function toCamelCase(str) {
|
|
12
13
|
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
@@ -145,8 +146,8 @@ export async function pullCommand(context) {
|
|
|
145
146
|
const author = config.author || 'Relq CLI';
|
|
146
147
|
const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
|
|
147
148
|
const schemaPath = path.resolve(projectRoot, schemaPathRaw);
|
|
148
|
-
const includeFunctions = config.
|
|
149
|
-
const includeTriggers = config.
|
|
149
|
+
const includeFunctions = config.includeFunctions ?? true;
|
|
150
|
+
const includeTriggers = config.includeTriggers ?? true;
|
|
150
151
|
const spinner = createSpinner();
|
|
151
152
|
const startTime = Date.now();
|
|
152
153
|
console.log('');
|
|
@@ -202,12 +203,36 @@ export async function pullCommand(context) {
|
|
|
202
203
|
setFetchHead(remoteHead, projectRoot);
|
|
203
204
|
}
|
|
204
205
|
spinner.succeed(`Fetched ${remoteCommits.length} remote commits`);
|
|
205
|
-
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(colors.bold('Introspecting database...'));
|
|
208
|
+
console.log('');
|
|
209
|
+
const progress = createMultiProgress();
|
|
210
|
+
const progressItems = [
|
|
211
|
+
{ id: 'tables', label: 'tables', status: 'pending', count: 0 },
|
|
212
|
+
{ id: 'columns', label: 'columns', status: 'pending', count: 0 },
|
|
213
|
+
{ id: 'constraints', label: 'constraints', status: 'pending', count: 0 },
|
|
214
|
+
{ id: 'indexes', label: 'indexes', status: 'pending', count: 0 },
|
|
215
|
+
{ id: 'checks', label: 'checks', status: 'pending', count: 0 },
|
|
216
|
+
{ id: 'enums', label: 'enums', status: 'pending', count: 0 },
|
|
217
|
+
{ id: 'partitions', label: 'partitions', status: 'pending', count: 0 },
|
|
218
|
+
{ id: 'extensions', label: 'extensions', status: 'pending', count: 0 },
|
|
219
|
+
...(includeFunctions ? [{ id: 'functions', label: 'functions', status: 'pending', count: 0 }] : []),
|
|
220
|
+
...(includeTriggers ? [{ id: 'triggers', label: 'triggers', status: 'pending', count: 0 }] : []),
|
|
221
|
+
{ id: 'collations', label: 'collations', status: 'pending', count: 0 },
|
|
222
|
+
{ id: 'foreign_servers', label: 'foreign servers', status: 'pending', count: 0 },
|
|
223
|
+
{ id: 'foreign_tables', label: 'foreign tables', status: 'pending', count: 0 },
|
|
224
|
+
];
|
|
225
|
+
progress.setItems(progressItems);
|
|
226
|
+
progress.start();
|
|
206
227
|
const dbSchema = await fastIntrospectDatabase(connection, undefined, {
|
|
207
228
|
includeFunctions,
|
|
208
229
|
includeTriggers,
|
|
230
|
+
onDetailedProgress: (update) => {
|
|
231
|
+
progress.updateItem(update.step, { status: update.status, count: update.count });
|
|
232
|
+
},
|
|
209
233
|
});
|
|
210
|
-
|
|
234
|
+
progress.complete();
|
|
235
|
+
console.log('');
|
|
211
236
|
const ignorePatterns = loadRelqignore(projectRoot);
|
|
212
237
|
const filteredTables = dbSchema.tables
|
|
213
238
|
.filter(t => !isTableIgnored(t.name, ignorePatterns).ignored)
|
|
@@ -241,12 +266,21 @@ export async function pullCommand(context) {
|
|
|
241
266
|
default: c.defaultValue,
|
|
242
267
|
primaryKey: c.isPrimaryKey,
|
|
243
268
|
unique: c.isUnique,
|
|
269
|
+
maxLength: c.maxLength ?? null,
|
|
270
|
+
precision: c.precision ?? null,
|
|
271
|
+
scale: c.scale ?? null,
|
|
272
|
+
comment: c.comment || undefined,
|
|
273
|
+
isGenerated: c.isGenerated || false,
|
|
274
|
+
generationExpression: c.generationExpression ?? null,
|
|
244
275
|
})),
|
|
245
276
|
indexes: t.indexes.map(i => ({
|
|
246
277
|
name: i.name,
|
|
247
278
|
columns: i.columns,
|
|
248
279
|
unique: i.isUnique,
|
|
249
280
|
type: i.type,
|
|
281
|
+
definition: i.definition || undefined,
|
|
282
|
+
whereClause: i.whereClause || undefined,
|
|
283
|
+
comment: i.comment || undefined,
|
|
250
284
|
})),
|
|
251
285
|
constraints: t.constraints.map(c => ({
|
|
252
286
|
name: c.name,
|
|
@@ -256,6 +290,12 @@ export async function pullCommand(context) {
|
|
|
256
290
|
isPartitioned: t.isPartitioned,
|
|
257
291
|
partitionType: t.partitionType,
|
|
258
292
|
partitionKey: normalizePartitionKey(t.partitionKey),
|
|
293
|
+
comment: t.comment || undefined,
|
|
294
|
+
partitions: t.childPartitions?.map((p) => ({
|
|
295
|
+
name: p.name,
|
|
296
|
+
bound: p.partitionBound || '',
|
|
297
|
+
boundType: p.partitionType,
|
|
298
|
+
})) || undefined,
|
|
259
299
|
})),
|
|
260
300
|
enums: filteredEnums.map(e => ({
|
|
261
301
|
name: e.name,
|
|
@@ -359,24 +399,43 @@ export async function pullCommand(context) {
|
|
|
359
399
|
}
|
|
360
400
|
}
|
|
361
401
|
else if (!schemaExists) {
|
|
362
|
-
console.log('
|
|
402
|
+
console.log(colors.bold('Creating schema') + colors.dim(' (first pull)'));
|
|
363
403
|
console.log('');
|
|
404
|
+
const tableCount = filteredTables.length;
|
|
405
|
+
const columnCount = filteredTables.reduce((sum, t) => sum + t.columns.length, 0);
|
|
364
406
|
const indexCount = filteredTables.reduce((sum, t) => sum + (t.indexes?.filter(i => !i.isPrimary).length || 0), 0);
|
|
365
|
-
const
|
|
407
|
+
const partitionedCount = filteredTables.filter(t => t.isPartitioned).length;
|
|
408
|
+
const childPartitionCount = filteredTables.reduce((sum, t) => sum + (t.childPartitions?.length || 0), 0);
|
|
366
409
|
const tableCommentCount = filteredTables.filter(t => t.comment).length;
|
|
367
410
|
const columnCommentCount = filteredTables.reduce((sum, t) => sum + t.columns.filter(c => c.comment).length, 0);
|
|
368
|
-
|
|
369
|
-
|
|
411
|
+
const indexCommentCount = filteredTables.reduce((sum, t) => sum + t.indexes.filter(i => i.comment).length, 0);
|
|
412
|
+
const summaryItems = [];
|
|
413
|
+
summaryItems.push({ count: tableCount, label: 'tables' });
|
|
414
|
+
summaryItems.push({ count: columnCount, label: 'columns' });
|
|
370
415
|
if (indexCount > 0)
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
416
|
+
summaryItems.push({ count: indexCount, label: 'indexes' });
|
|
417
|
+
if (partitionedCount > 0) {
|
|
418
|
+
const partLabel = childPartitionCount > 0
|
|
419
|
+
? `partitioned (${childPartitionCount} child partitions)`
|
|
420
|
+
: 'partitioned';
|
|
421
|
+
summaryItems.push({ count: partitionedCount, label: partLabel });
|
|
422
|
+
}
|
|
423
|
+
if (tableCommentCount > 0 || columnCommentCount > 0 || indexCommentCount > 0) {
|
|
424
|
+
const commentParts = [];
|
|
425
|
+
if (tableCommentCount > 0)
|
|
426
|
+
commentParts.push(`${tableCommentCount} table`);
|
|
427
|
+
if (columnCommentCount > 0)
|
|
428
|
+
commentParts.push(`${columnCommentCount} column`);
|
|
429
|
+
if (indexCommentCount > 0)
|
|
430
|
+
commentParts.push(`${indexCommentCount} index`);
|
|
431
|
+
summaryItems.push({ count: tableCommentCount + columnCommentCount + indexCommentCount, label: `comments (${commentParts.join(', ')})` });
|
|
432
|
+
}
|
|
378
433
|
if (dbSchema.extensions.length > 0)
|
|
379
|
-
|
|
434
|
+
summaryItems.push({ count: dbSchema.extensions.length, label: 'extensions' });
|
|
435
|
+
for (const item of summaryItems) {
|
|
436
|
+
const countStr = String(item.count).padStart(4);
|
|
437
|
+
console.log(`${colors.green('[+]')} ${colors.green(countStr)} ${item.label}`);
|
|
438
|
+
}
|
|
380
439
|
console.log('');
|
|
381
440
|
}
|
|
382
441
|
if (dryRun) {
|
|
@@ -385,13 +444,10 @@ export async function pullCommand(context) {
|
|
|
385
444
|
return;
|
|
386
445
|
}
|
|
387
446
|
spinner.start('Generating TypeScript schema...');
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
includeSchema: true,
|
|
391
|
-
includeIndexes: true,
|
|
392
|
-
includeFunctions,
|
|
393
|
-
includeTriggers,
|
|
447
|
+
const parsedSchema = await introspectedToParsedSchema(dbSchema);
|
|
448
|
+
const typescript = generateTypeScriptFromAST(parsedSchema, {
|
|
394
449
|
camelCase: config.generate?.camelCase ?? true,
|
|
450
|
+
importPath: 'relq/schema-builder',
|
|
395
451
|
});
|
|
396
452
|
spinner.succeed('Generated TypeScript schema');
|
|
397
453
|
const schemaDir = path.dirname(schemaPath);
|
|
@@ -494,6 +550,7 @@ export async function pullCommand(context) {
|
|
|
494
550
|
triggers: filteredTriggers || [],
|
|
495
551
|
};
|
|
496
552
|
const schemaChanges = compareSchemas(beforeSchema, afterSchema);
|
|
553
|
+
applyTrackingIdsToSnapshot(typescript, currentSchema);
|
|
497
554
|
saveSnapshot(currentSchema, projectRoot);
|
|
498
555
|
const duration = Date.now() - startTime;
|
|
499
556
|
if (noCommit) {
|
|
@@ -516,6 +573,7 @@ export async function pullCommand(context) {
|
|
|
516
573
|
const connectionDesc = getConnectionDescription(connection);
|
|
517
574
|
const commitMessage = `pull: sync from ${connectionDesc}`;
|
|
518
575
|
const commit = createCommit(currentSchema, author, commitMessage, projectRoot);
|
|
576
|
+
markCommitAsPulled(commit.hash, connection, projectRoot);
|
|
519
577
|
clearWorkingState(projectRoot);
|
|
520
578
|
console.log('');
|
|
521
579
|
console.log(`Pull completed in ${formatDuration(duration)}`);
|
|
@@ -598,3 +656,26 @@ function detectObjectConflicts(local, remote) {
|
|
|
598
656
|
}
|
|
599
657
|
return conflicts;
|
|
600
658
|
}
|
|
659
|
+
function applyTrackingIdsToSnapshot(typescript, snapshot) {
|
|
660
|
+
for (const table of snapshot.tables) {
|
|
661
|
+
const tablePattern = new RegExp(`defineTable\\s*\\(\\s*['"]${table.name}['"]\\s*,\\s*\\{[^}]+\\}\\s*,\\s*\\{[^}]*\\$trackingId:\\s*['"]([^'"]+)['"]`, 's');
|
|
662
|
+
const tableMatch = typescript.match(tablePattern);
|
|
663
|
+
if (tableMatch) {
|
|
664
|
+
table.trackingId = tableMatch[1];
|
|
665
|
+
}
|
|
666
|
+
for (const col of table.columns) {
|
|
667
|
+
const colPattern = new RegExp(`(?:${col.tsName}|${col.name}):\\s*\\w+\\([^)]*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
|
|
668
|
+
const colMatch = typescript.match(colPattern);
|
|
669
|
+
if (colMatch) {
|
|
670
|
+
col.trackingId = colMatch[1];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (const idx of table.indexes) {
|
|
674
|
+
const idxPattern = new RegExp(`index\\s*\\(\\s*['"]${idx.name}['"]\\s*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
|
|
675
|
+
const idxMatch = typescript.match(idxPattern);
|
|
676
|
+
if (idxMatch) {
|
|
677
|
+
idx.trackingId = idxMatch[1];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
@@ -5,7 +5,7 @@ import { getConnectionDescription } from "../utils/env-loader.js";
|
|
|
5
5
|
import { colors, createSpinner, fatal, confirm, warning } from "../utils/cli-utils.js";
|
|
6
6
|
import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
|
|
7
7
|
import { loadRelqignore, isTableIgnored, isColumnIgnored, isEnumIgnored, isDomainIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
|
|
8
|
-
import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, } from "../utils/repo-manager.js";
|
|
8
|
+
import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, isCommitSyncedWith, markCommitAsPushed, getConnectionLabel, } from "../utils/repo-manager.js";
|
|
9
9
|
export async function pushCommand(context) {
|
|
10
10
|
const { config, flags } = context;
|
|
11
11
|
if (!config) {
|
|
@@ -41,7 +41,15 @@ export async function pushCommand(context) {
|
|
|
41
41
|
const localCommits = getAllCommits(projectRoot);
|
|
42
42
|
const remoteHashes = new Set(remoteCommits.map(c => c.hash));
|
|
43
43
|
const localHashes = new Set(localCommits.map(c => c.hash));
|
|
44
|
-
const toPush = localCommits.filter(c =>
|
|
44
|
+
const toPush = localCommits.filter(c => {
|
|
45
|
+
if (remoteHashes.has(c.hash))
|
|
46
|
+
return false;
|
|
47
|
+
const syncStatus = isCommitSyncedWith(c, connection);
|
|
48
|
+
if (syncStatus.synced) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
45
53
|
const remoteMissing = remoteCommits.filter(c => !localHashes.has(c.hash));
|
|
46
54
|
spinner.succeed('Checked remote commits');
|
|
47
55
|
spinner.start('Introspecting remote database...');
|
|
@@ -131,9 +139,10 @@ export async function pushCommand(context) {
|
|
|
131
139
|
const commitsToProcess = [...toPush].reverse();
|
|
132
140
|
spinner.start(`Pushing ${toPush.length} commit(s)...`);
|
|
133
141
|
for (const commit of commitsToProcess) {
|
|
134
|
-
await pushCommit(connection, commit);
|
|
142
|
+
await pushCommit(connection, commit, projectRoot);
|
|
143
|
+
markCommitAsPushed(commit.hash, connection, projectRoot);
|
|
135
144
|
}
|
|
136
|
-
spinner.succeed(`Pushed ${toPush.length} commit(s)`);
|
|
145
|
+
spinner.succeed(`Pushed ${toPush.length} commit(s) to ${getConnectionLabel(connection)}`);
|
|
137
146
|
}
|
|
138
147
|
if (applySQL) {
|
|
139
148
|
spinner.start('Applying SQL changes...');
|
|
@@ -173,6 +182,32 @@ export async function pushCommand(context) {
|
|
|
173
182
|
}
|
|
174
183
|
await client.query('COMMIT');
|
|
175
184
|
spinner.succeed(`Applied ${statementsRun} statement(s) from ${sqlExecuted} commit(s)`);
|
|
185
|
+
let hasRenameOperations = false;
|
|
186
|
+
for (const commit of commitsToProcess) {
|
|
187
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
|
|
188
|
+
if (fs.existsSync(commitPath)) {
|
|
189
|
+
const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
190
|
+
if (enhancedCommit.changes?.some((c) => c.type === 'RENAME')) {
|
|
191
|
+
hasRenameOperations = true;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (hasRenameOperations) {
|
|
197
|
+
spinner.start('Updating snapshot after RENAME...');
|
|
198
|
+
try {
|
|
199
|
+
const updatedSchema = await fastIntrospectDatabase(connection, undefined, {
|
|
200
|
+
includeFunctions,
|
|
201
|
+
includeTriggers,
|
|
202
|
+
});
|
|
203
|
+
const snapshotPath = path.join(projectRoot, '.relq', 'snapshot.json');
|
|
204
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(updatedSchema, null, 2));
|
|
205
|
+
spinner.succeed('Snapshot updated with renamed objects');
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
spinner.warn('Could not update snapshot after RENAME - run "relq pull" manually');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
176
211
|
}
|
|
177
212
|
catch (error) {
|
|
178
213
|
try {
|
package/dist/esm/cli/index.js
CHANGED
|
@@ -62,7 +62,14 @@ function parseArgs(argv) {
|
|
|
62
62
|
flags[arg.slice(2, eqIndex)] = arg.slice(eqIndex + 1);
|
|
63
63
|
}
|
|
64
64
|
else {
|
|
65
|
-
|
|
65
|
+
const nextArg = argv[i + 1];
|
|
66
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
67
|
+
flags[arg.slice(2)] = nextArg;
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
flags[arg.slice(2)] = true;
|
|
72
|
+
}
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
else if (arg.startsWith('-') && arg.length === 2) {
|
|
@@ -175,6 +182,7 @@ async function main() {
|
|
|
175
182
|
printHelp();
|
|
176
183
|
process.exit(1);
|
|
177
184
|
}
|
|
185
|
+
console.log('Command:', command);
|
|
178
186
|
let config = null;
|
|
179
187
|
let resolvedProjectRoot = process.cwd();
|
|
180
188
|
if (requiresConfig(command)) {
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { toCamelCase, escapeString } from "./utils.js";
|
|
2
|
+
export const KNOWN_BUILDER_FUNCTIONS = {
|
|
3
|
+
'setweight': 'setweight',
|
|
4
|
+
'to_tsvector': 'toTsvector',
|
|
5
|
+
'to_tsquery': 'toTsquery',
|
|
6
|
+
'plainto_tsquery': 'plainToTsquery',
|
|
7
|
+
'phraseto_tsquery': 'phraseToTsquery',
|
|
8
|
+
'websearch_to_tsquery': 'websearchToTsquery',
|
|
9
|
+
'ts_headline': 'tsHeadline',
|
|
10
|
+
'ts_rank': 'tsRank',
|
|
11
|
+
'ts_rank_cd': 'tsRankCd',
|
|
12
|
+
'coalesce': 'coalesce',
|
|
13
|
+
'nullif': 'nullif',
|
|
14
|
+
'greatest': 'greatest',
|
|
15
|
+
'least': 'least',
|
|
16
|
+
'concat': 'concat',
|
|
17
|
+
'concat_ws': 'concatWs',
|
|
18
|
+
'lower': 'lower',
|
|
19
|
+
'upper': 'upper',
|
|
20
|
+
'trim': 'trim',
|
|
21
|
+
'ltrim': 'ltrim',
|
|
22
|
+
'rtrim': 'rtrim',
|
|
23
|
+
'length': 'length',
|
|
24
|
+
'substring': 'substring',
|
|
25
|
+
'replace': 'replace',
|
|
26
|
+
'regexp_replace': 'regexpReplace',
|
|
27
|
+
'split_part': 'splitPart',
|
|
28
|
+
'now': 'now',
|
|
29
|
+
'current_timestamp': 'currentTimestamp',
|
|
30
|
+
'current_date': 'currentDate',
|
|
31
|
+
'current_time': 'currentTime',
|
|
32
|
+
'date_trunc': 'dateTrunc',
|
|
33
|
+
'date_part': 'datePart',
|
|
34
|
+
'extract': 'extract',
|
|
35
|
+
'age': 'age',
|
|
36
|
+
'gen_random_uuid': 'genRandomUuid',
|
|
37
|
+
'uuid_generate_v4': 'uuidGenerateV4',
|
|
38
|
+
'jsonb_build_object': 'jsonbBuildObject',
|
|
39
|
+
'jsonb_agg': 'jsonbAgg',
|
|
40
|
+
'json_agg': 'jsonAgg',
|
|
41
|
+
'array_agg': 'arrayAgg',
|
|
42
|
+
'string_agg': 'stringAgg',
|
|
43
|
+
'count': 'count',
|
|
44
|
+
'sum': 'sum',
|
|
45
|
+
'avg': 'avg',
|
|
46
|
+
'min': 'min',
|
|
47
|
+
'max': 'max',
|
|
48
|
+
'abs': 'abs',
|
|
49
|
+
'ceil': 'ceil',
|
|
50
|
+
'floor': 'floor',
|
|
51
|
+
'round': 'round',
|
|
52
|
+
'sqrt': 'sqrt',
|
|
53
|
+
'power': 'power',
|
|
54
|
+
'mod': 'mod',
|
|
55
|
+
'random': 'random',
|
|
56
|
+
};
|
|
57
|
+
export function mapFunctionToBuilder(funcName) {
|
|
58
|
+
return KNOWN_BUILDER_FUNCTIONS[funcName] || null;
|
|
59
|
+
}
|
|
60
|
+
export function astToBuilder(node, prefixOrOptions = 'g') {
|
|
61
|
+
const opts = typeof prefixOrOptions === 'string'
|
|
62
|
+
? { prefix: prefixOrOptions }
|
|
63
|
+
: prefixOrOptions;
|
|
64
|
+
const { prefix = 'g', useCamelCase = false, useTableRef = false } = opts;
|
|
65
|
+
if (!node)
|
|
66
|
+
return "''";
|
|
67
|
+
if (node.FuncCall) {
|
|
68
|
+
const func = node.FuncCall;
|
|
69
|
+
const funcName = func.funcname?.map((n) => n.String?.sval).filter(Boolean).join('.') || '';
|
|
70
|
+
const args = (func.args || []).map((a) => astToBuilder(a, opts));
|
|
71
|
+
const builderMethod = mapFunctionToBuilder(funcName.toLowerCase());
|
|
72
|
+
if (builderMethod) {
|
|
73
|
+
return `${prefix}.${builderMethod}(${args.join(', ')})`;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Unsupported function in generated expression: "${funcName}". Add this function to KNOWN_BUILDER_FUNCTIONS in astToBuilder.`);
|
|
76
|
+
}
|
|
77
|
+
if (node.CoalesceExpr) {
|
|
78
|
+
const args = (node.CoalesceExpr.args || []).map((a) => astToBuilder(a, opts));
|
|
79
|
+
return `${prefix}.coalesce(${args.join(', ')})`;
|
|
80
|
+
}
|
|
81
|
+
if (node.A_Expr) {
|
|
82
|
+
const expr = node.A_Expr;
|
|
83
|
+
const op = expr.name?.[0]?.String?.sval || '';
|
|
84
|
+
const left = astToBuilder(expr.lexpr, opts);
|
|
85
|
+
const right = astToBuilder(expr.rexpr, opts);
|
|
86
|
+
switch (op) {
|
|
87
|
+
case '||':
|
|
88
|
+
return `__CONCAT__[${left}, ${right}]`;
|
|
89
|
+
case '->>':
|
|
90
|
+
return `${prefix}.jsonbExtractText(${left}, ${right})`;
|
|
91
|
+
case '->':
|
|
92
|
+
return `${prefix}.jsonbExtract(${left}, ${right})`;
|
|
93
|
+
case '@@':
|
|
94
|
+
return `${prefix}.tsMatch(${left}, ${right})`;
|
|
95
|
+
case '+':
|
|
96
|
+
return `${prefix}.add(${left}, ${right})`;
|
|
97
|
+
case '-':
|
|
98
|
+
return `${prefix}.subtract(${left}, ${right})`;
|
|
99
|
+
case '*':
|
|
100
|
+
return `${prefix}.multiply(${left}, ${right})`;
|
|
101
|
+
case '/':
|
|
102
|
+
return `${prefix}.divide(${left}, ${right})`;
|
|
103
|
+
case '%':
|
|
104
|
+
return `${prefix}.mod(${left}, ${right})`;
|
|
105
|
+
case '=':
|
|
106
|
+
case '<>':
|
|
107
|
+
case '!=':
|
|
108
|
+
case '<':
|
|
109
|
+
case '>':
|
|
110
|
+
case '<=':
|
|
111
|
+
case '>=':
|
|
112
|
+
if (useTableRef) {
|
|
113
|
+
const methodMap = {
|
|
114
|
+
'=': 'eq', '<>': 'ne', '!=': 'neq',
|
|
115
|
+
'<': 'lt', '>': 'gt', '<=': 'lte', '>=': 'gte'
|
|
116
|
+
};
|
|
117
|
+
return `${left}.${methodMap[op]}(${right})`;
|
|
118
|
+
}
|
|
119
|
+
return `${prefix}.compare(${left}, '${op}', ${right})`;
|
|
120
|
+
case '~':
|
|
121
|
+
case '~*':
|
|
122
|
+
case '!~':
|
|
123
|
+
case '!~*':
|
|
124
|
+
return `${prefix}.regex(${left}, '${op}', ${right})`;
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`Unsupported operator in generated expression: "${op}". Add explicit handling for this operator in astToBuilder.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (node.ColumnRef) {
|
|
130
|
+
const fields = (node.ColumnRef.fields || [])
|
|
131
|
+
.map((f) => f.String?.sval)
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
if (fields.length === 1) {
|
|
134
|
+
const colName = useCamelCase ? toCamelCase(fields[0]) : fields[0];
|
|
135
|
+
return `table.${colName}`;
|
|
136
|
+
}
|
|
137
|
+
else if (fields.length === 2) {
|
|
138
|
+
const colName = useCamelCase ? toCamelCase(fields[1]) : fields[1];
|
|
139
|
+
return `table.${colName}`;
|
|
140
|
+
}
|
|
141
|
+
const colName = useCamelCase ? toCamelCase(fields[fields.length - 1]) : fields[fields.length - 1];
|
|
142
|
+
return `table.${colName}`;
|
|
143
|
+
}
|
|
144
|
+
if (node.A_Const) {
|
|
145
|
+
const val = node.A_Const;
|
|
146
|
+
if (val.sval !== undefined) {
|
|
147
|
+
const s = val.sval?.sval ?? val.sval;
|
|
148
|
+
if (s === '' || s === ' ') {
|
|
149
|
+
return s === '' ? `${prefix}.asText()` : `${prefix}.asText(' ')`;
|
|
150
|
+
}
|
|
151
|
+
return `'${escapeString(String(s))}'`;
|
|
152
|
+
}
|
|
153
|
+
else if (val.ival !== undefined) {
|
|
154
|
+
const i = val.ival?.ival ?? val.ival;
|
|
155
|
+
return String(i);
|
|
156
|
+
}
|
|
157
|
+
else if (val.fval !== undefined) {
|
|
158
|
+
const f = val.fval?.fval ?? val.fval;
|
|
159
|
+
return String(f);
|
|
160
|
+
}
|
|
161
|
+
else if (val.boolval !== undefined) {
|
|
162
|
+
return val.boolval?.boolval ? 'true' : 'false';
|
|
163
|
+
}
|
|
164
|
+
return `${prefix}.asText()`;
|
|
165
|
+
}
|
|
166
|
+
if (node.TypeCast) {
|
|
167
|
+
const arg = astToBuilder(node.TypeCast.arg, opts);
|
|
168
|
+
const typeName = node.TypeCast.typeName?.names
|
|
169
|
+
?.map((n) => n.String?.sval)
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join('.') || '';
|
|
172
|
+
if (typeName === 'regconfig' || typeName === 'pg_catalog.regconfig') {
|
|
173
|
+
return arg;
|
|
174
|
+
}
|
|
175
|
+
if (typeName === 'text' || typeName === 'pg_catalog.text') {
|
|
176
|
+
return `${prefix}.asText(${arg})`;
|
|
177
|
+
}
|
|
178
|
+
if (typeName === 'varchar' || typeName === 'pg_catalog.varchar' || typeName.includes('character varying')) {
|
|
179
|
+
return arg;
|
|
180
|
+
}
|
|
181
|
+
if (typeName === 'char' || typeName === 'bpchar' || typeName === 'pg_catalog.bpchar') {
|
|
182
|
+
return arg;
|
|
183
|
+
}
|
|
184
|
+
if (typeName === 'numeric' || typeName === 'pg_catalog.numeric') {
|
|
185
|
+
return arg;
|
|
186
|
+
}
|
|
187
|
+
if (typeName === 'int4' || typeName === 'integer' || typeName === 'pg_catalog.int4') {
|
|
188
|
+
return arg;
|
|
189
|
+
}
|
|
190
|
+
return `${prefix}.cast(${arg}, '${typeName}')`;
|
|
191
|
+
}
|
|
192
|
+
if (node.NullTest) {
|
|
193
|
+
const arg = astToBuilder(node.NullTest.arg, opts);
|
|
194
|
+
const isNull = node.NullTest.nulltesttype === 'IS_NULL';
|
|
195
|
+
return isNull ? `${prefix}.isNull(${arg})` : `${prefix}.isNotNull(${arg})`;
|
|
196
|
+
}
|
|
197
|
+
if (node.BoolExpr) {
|
|
198
|
+
const boolexpr = node.BoolExpr;
|
|
199
|
+
const args = (boolexpr.args || []).map((a) => astToBuilder(a, opts));
|
|
200
|
+
if (useTableRef && args.length >= 2) {
|
|
201
|
+
switch (boolexpr.boolop) {
|
|
202
|
+
case 'AND_EXPR': return args.reduce((acc, arg) => `${acc}.and(${arg})`);
|
|
203
|
+
case 'OR_EXPR': return args.reduce((acc, arg) => `${acc}.or(${arg})`);
|
|
204
|
+
case 'NOT_EXPR': return `${args[0]}.not()`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
switch (boolexpr.boolop) {
|
|
208
|
+
case 'AND_EXPR': return `${prefix}.and(${args.join(', ')})`;
|
|
209
|
+
case 'OR_EXPR': return `${prefix}.or(${args.join(', ')})`;
|
|
210
|
+
case 'NOT_EXPR': return `${prefix}.not(${args[0]})`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (node.CaseExpr) {
|
|
214
|
+
const caseExpr = node.CaseExpr;
|
|
215
|
+
let chain = `${prefix}.case()`;
|
|
216
|
+
for (const w of (caseExpr.args || [])) {
|
|
217
|
+
if (w.CaseWhen) {
|
|
218
|
+
const condition = astToBuilder(w.CaseWhen.expr, opts);
|
|
219
|
+
const result = astToBuilder(w.CaseWhen.result, opts);
|
|
220
|
+
chain += `.when(${condition}, ${result})`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (caseExpr.defresult) {
|
|
224
|
+
const defResult = astToBuilder(caseExpr.defresult, opts);
|
|
225
|
+
chain += `.else(${defResult})`;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
chain += '.end()';
|
|
229
|
+
}
|
|
230
|
+
return chain;
|
|
231
|
+
}
|
|
232
|
+
const nodeType = Object.keys(node)[0];
|
|
233
|
+
throw new Error(`Unsupported AST node type in generated expression: "${nodeType}". Add explicit handling for this node type in astToBuilder.`);
|
|
234
|
+
}
|
|
235
|
+
function extractConcatItems(expr) {
|
|
236
|
+
if (expr.startsWith('__CONCAT__[')) {
|
|
237
|
+
const inner = expr.slice('__CONCAT__['.length, -1);
|
|
238
|
+
const items = [];
|
|
239
|
+
let depth = 0;
|
|
240
|
+
let current = '';
|
|
241
|
+
let inQuote = false;
|
|
242
|
+
for (let i = 0; i < inner.length; i++) {
|
|
243
|
+
const c = inner[i];
|
|
244
|
+
if (c === "'" && inner[i - 1] !== '\\') {
|
|
245
|
+
inQuote = !inQuote;
|
|
246
|
+
}
|
|
247
|
+
if (!inQuote) {
|
|
248
|
+
if (c === '[' || c === '(')
|
|
249
|
+
depth++;
|
|
250
|
+
if (c === ']' || c === ')')
|
|
251
|
+
depth--;
|
|
252
|
+
if (c === ',' && depth === 0) {
|
|
253
|
+
items.push(current.trim());
|
|
254
|
+
current = '';
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
current += c;
|
|
259
|
+
}
|
|
260
|
+
if (current.trim())
|
|
261
|
+
items.push(current.trim());
|
|
262
|
+
return items.flatMap(item => extractConcatItems(item));
|
|
263
|
+
}
|
|
264
|
+
return [expr];
|
|
265
|
+
}
|
|
266
|
+
function cleanAllConcats(expr) {
|
|
267
|
+
let result = expr;
|
|
268
|
+
let iterations = 0;
|
|
269
|
+
const maxIterations = 50;
|
|
270
|
+
while (result.includes('__CONCAT__[') && iterations < maxIterations) {
|
|
271
|
+
const match = result.match(/__CONCAT__\[([^\[\]]*)\]/);
|
|
272
|
+
if (match) {
|
|
273
|
+
const replacement = `F.concat(${match[1]})`;
|
|
274
|
+
result = result.replace(match[0], replacement);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
iterations++;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
export function formatGeneratedExpression(expr) {
|
|
284
|
+
if (expr.startsWith('__CONCAT__[')) {
|
|
285
|
+
const items = extractConcatItems(expr);
|
|
286
|
+
const cleanedItems = items.map(item => cleanAllConcats(item));
|
|
287
|
+
return { isArray: true, items: cleanedItems };
|
|
288
|
+
}
|
|
289
|
+
const cleaned = cleanAllConcats(expr);
|
|
290
|
+
return { isArray: false, items: [cleaned] };
|
|
291
|
+
}
|