relq 1.0.27 → 1.0.28
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/export.cjs +1 -0
- package/dist/cjs/cli/commands/import.cjs +58 -5
- package/dist/cjs/cli/commands/pull.cjs +133 -18
- package/dist/cjs/cli/commands/sync.cjs +4 -1
- package/dist/cjs/cli/utils/ast-codegen.cjs +165 -9
- package/dist/cjs/cli/utils/ast-transformer.cjs +16 -2
- package/dist/cjs/cli/utils/fast-introspect.cjs +14 -0
- package/dist/cjs/cli/utils/schema-comparator.cjs +24 -0
- package/dist/cjs/cli/utils/schema-introspect.cjs +25 -4
- package/dist/cjs/cli/utils/sql-parser.cjs +4 -1
- package/dist/cjs/config/config.cjs +3 -0
- package/dist/cjs/schema-definition/pg-function.cjs +4 -0
- package/dist/cjs/schema-definition/pg-trigger.cjs +9 -5
- package/dist/esm/cli/commands/export.js +1 -0
- package/dist/esm/cli/commands/import.js +59 -6
- package/dist/esm/cli/commands/pull.js +134 -19
- package/dist/esm/cli/commands/sync.js +5 -2
- package/dist/esm/cli/utils/ast-codegen.js +163 -9
- package/dist/esm/cli/utils/ast-transformer.js +16 -2
- package/dist/esm/cli/utils/fast-introspect.js +14 -0
- package/dist/esm/cli/utils/schema-comparator.js +24 -0
- package/dist/esm/cli/utils/schema-introspect.js +25 -4
- package/dist/esm/cli/utils/sql-parser.js +4 -1
- package/dist/esm/config/config.js +3 -0
- package/dist/esm/schema-definition/pg-function.js +4 -0
- package/dist/esm/schema-definition/pg-trigger.js +9 -5
- package/dist/schema-builder.d.ts +25 -19
- package/package.json +1 -1
|
@@ -242,6 +242,31 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
spinner.start('Generating TypeScript schema');
|
|
245
|
+
const outputPath = options.output || (0, config_loader_1.getSchemaPath)(config);
|
|
246
|
+
const absoluteOutputPath = path.resolve(projectRoot, outputPath);
|
|
247
|
+
const schemaExists = fs.existsSync(absoluteOutputPath);
|
|
248
|
+
if (schemaExists && (includeFunctions || includeTriggers)) {
|
|
249
|
+
const existingContent = fs.readFileSync(absoluteOutputPath, 'utf-8');
|
|
250
|
+
const hasPgFunction = /\bpgFunction\s*\(/.test(existingContent);
|
|
251
|
+
const hasPgTrigger = /\bpgTrigger\s*\(/.test(existingContent);
|
|
252
|
+
if (hasPgFunction || hasPgTrigger) {
|
|
253
|
+
spinner.stop();
|
|
254
|
+
const items = [];
|
|
255
|
+
if (hasPgFunction)
|
|
256
|
+
items.push('pgFunction');
|
|
257
|
+
if (hasPgTrigger)
|
|
258
|
+
items.push('pgTrigger');
|
|
259
|
+
const schemaBaseName = path.basename(absoluteOutputPath, '.ts');
|
|
260
|
+
(0, git_utils_1.fatal)(`Existing schema contains ${items.join(' and ')} definitions`, `Functions and triggers should be in separate files:\n` +
|
|
261
|
+
` ${git_utils_1.colors.cyan(`${schemaBaseName}.functions.ts`)} - for functions\n` +
|
|
262
|
+
` ${git_utils_1.colors.cyan(`${schemaBaseName}.triggers.ts`)} - for triggers\n\n` +
|
|
263
|
+
`To migrate:\n` +
|
|
264
|
+
` 1. Move pgFunction definitions to ${schemaBaseName}.functions.ts\n` +
|
|
265
|
+
` 2. Move pgTrigger definitions to ${schemaBaseName}.triggers.ts\n` +
|
|
266
|
+
` 3. Run ${git_utils_1.colors.cyan('relq import')} again\n\n` +
|
|
267
|
+
`Or use ${git_utils_1.colors.cyan('relq import --force')} to overwrite and regenerate all files.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
245
270
|
spinner.succeed('Generated TypeScript schema');
|
|
246
271
|
const incomingSchema = convertToNormalizedSchema(filteredSchema, filteredFunctions, triggers);
|
|
247
272
|
const existingSnapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
@@ -389,10 +414,11 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
389
414
|
when: t.when,
|
|
390
415
|
})) : [],
|
|
391
416
|
});
|
|
417
|
+
(0, ast_codegen_1.assignTrackingIds)(astSchema);
|
|
392
418
|
const finalTypescriptContent = (0, ast_codegen_1.generateTypeScriptFromAST)(astSchema, {
|
|
393
419
|
camelCase: true,
|
|
394
|
-
includeFunctions,
|
|
395
|
-
includeTriggers,
|
|
420
|
+
includeFunctions: false,
|
|
421
|
+
includeTriggers: false,
|
|
396
422
|
});
|
|
397
423
|
if (dryRun) {
|
|
398
424
|
console.log('');
|
|
@@ -409,14 +435,39 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
409
435
|
console.log('');
|
|
410
436
|
return;
|
|
411
437
|
}
|
|
412
|
-
const outputPath = options.output || (0, config_loader_1.getSchemaPath)(config);
|
|
413
|
-
const absoluteOutputPath = path.resolve(projectRoot, outputPath);
|
|
414
438
|
const outputDir = path.dirname(absoluteOutputPath);
|
|
415
439
|
if (!fs.existsSync(outputDir)) {
|
|
416
440
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
417
441
|
}
|
|
418
442
|
fs.writeFileSync(absoluteOutputPath, finalTypescriptContent, 'utf-8');
|
|
419
443
|
console.log(`Written ${git_utils_1.colors.cyan(absoluteOutputPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(finalTypescriptContent.length)})`)}`);
|
|
444
|
+
if (includeFunctions && astSchema.functions.length > 0) {
|
|
445
|
+
const schemaBaseName = path.basename(absoluteOutputPath, '.ts');
|
|
446
|
+
const functionsPath = path.join(outputDir, `${schemaBaseName}.functions.ts`);
|
|
447
|
+
const functionsCode = (0, ast_codegen_1.generateFunctionsFile)(astSchema, {
|
|
448
|
+
camelCase: true,
|
|
449
|
+
importPath: 'relq/schema-builder',
|
|
450
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
451
|
+
});
|
|
452
|
+
if (functionsCode) {
|
|
453
|
+
fs.writeFileSync(functionsPath, functionsCode, 'utf-8');
|
|
454
|
+
console.log(`Written ${git_utils_1.colors.cyan(functionsPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(functionsCode.length)})`)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (includeTriggers && astSchema.triggers.length > 0) {
|
|
458
|
+
const schemaBaseName = path.basename(absoluteOutputPath, '.ts');
|
|
459
|
+
const triggersPath = path.join(outputDir, `${schemaBaseName}.triggers.ts`);
|
|
460
|
+
const triggersCode = (0, ast_codegen_1.generateTriggersFile)(astSchema, {
|
|
461
|
+
camelCase: true,
|
|
462
|
+
importPath: 'relq/schema-builder',
|
|
463
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
464
|
+
functionsImportPath: `./${schemaBaseName}.functions`,
|
|
465
|
+
});
|
|
466
|
+
if (triggersCode) {
|
|
467
|
+
fs.writeFileSync(triggersPath, triggersCode, 'utf-8');
|
|
468
|
+
console.log(`Written ${git_utils_1.colors.cyan(triggersPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(triggersCode.length)})`)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
420
471
|
applyTrackingIdsToSnapshot(finalTypescriptContent, mergedSchema);
|
|
421
472
|
(0, repo_manager_1.saveSnapshot)(mergedSchema, projectRoot);
|
|
422
473
|
if (changes.length > 0) {
|
|
@@ -660,7 +711,7 @@ function convertToNormalizedSchema(parsed, functions = [], triggers = []) {
|
|
|
660
711
|
table: t.tableName,
|
|
661
712
|
events: [t.event],
|
|
662
713
|
timing: t.timing,
|
|
663
|
-
forEach: '
|
|
714
|
+
forEach: (t.forEach || 'ROW'),
|
|
664
715
|
functionName: t.functionName || '',
|
|
665
716
|
})),
|
|
666
717
|
views: regularViews.map(v => ({
|
|
@@ -742,6 +793,7 @@ function snapshotToDbSchema(snapshot) {
|
|
|
742
793
|
tableName: t.table,
|
|
743
794
|
timing: t.timing,
|
|
744
795
|
event: t.events?.[0] || '',
|
|
796
|
+
forEach: (t.forEach || 'ROW'),
|
|
745
797
|
functionName: t.functionName || '',
|
|
746
798
|
definition: '',
|
|
747
799
|
isEnabled: t.isEnabled ?? true,
|
|
@@ -845,6 +897,7 @@ function snapshotToDbSchemaForGeneration(snapshot) {
|
|
|
845
897
|
tableName: t.table,
|
|
846
898
|
timing: t.timing,
|
|
847
899
|
event: t.events?.[0] || '',
|
|
900
|
+
forEach: (t.forEach || 'ROW'),
|
|
848
901
|
functionName: t.functionName || '',
|
|
849
902
|
definition: '',
|
|
850
903
|
isEnabled: t.isEnabled ?? true,
|
|
@@ -456,11 +456,73 @@ async function pullCommand(context) {
|
|
|
456
456
|
triggers: filteredTriggers || [],
|
|
457
457
|
};
|
|
458
458
|
const allChanges = (0, schema_comparator_1.compareSchemas)(localForCompare, remoteForCompare);
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
459
|
+
const changeDisplays = [];
|
|
460
|
+
for (const change of allChanges) {
|
|
461
|
+
const objType = change.objectType;
|
|
462
|
+
const changeType = change.type;
|
|
463
|
+
let action;
|
|
464
|
+
if (changeType === 'CREATE')
|
|
465
|
+
action = 'added';
|
|
466
|
+
else if (changeType === 'DROP')
|
|
467
|
+
action = 'removed';
|
|
468
|
+
else
|
|
469
|
+
action = 'modified';
|
|
470
|
+
let type;
|
|
471
|
+
let name;
|
|
472
|
+
if (objType === 'TABLE') {
|
|
473
|
+
type = 'table';
|
|
474
|
+
name = change.objectName;
|
|
475
|
+
}
|
|
476
|
+
else if (objType === 'COLUMN') {
|
|
477
|
+
type = 'column';
|
|
478
|
+
name = change.parentName ? `${change.parentName}.${change.objectName}` : change.objectName;
|
|
479
|
+
}
|
|
480
|
+
else if (objType === 'INDEX') {
|
|
481
|
+
type = 'index';
|
|
482
|
+
name = change.parentName ? `${change.parentName}:${change.objectName}` : change.objectName;
|
|
483
|
+
}
|
|
484
|
+
else if (objType === 'COLUMN_COMMENT') {
|
|
485
|
+
type = 'column comment';
|
|
486
|
+
const colName = change.after?.columnName || change.before?.columnName || change.objectName;
|
|
487
|
+
const tblName = change.after?.tableName || change.before?.tableName || change.parentName;
|
|
488
|
+
name = tblName ? `${tblName}.${colName}` : colName;
|
|
489
|
+
}
|
|
490
|
+
else if (objType === 'TABLE_COMMENT') {
|
|
491
|
+
type = 'table comment';
|
|
492
|
+
name = change.after?.tableName || change.before?.tableName || change.objectName;
|
|
493
|
+
}
|
|
494
|
+
else if (objType === 'INDEX_COMMENT') {
|
|
495
|
+
type = 'index comment';
|
|
496
|
+
const idxName = change.after?.indexName || change.before?.indexName || change.objectName;
|
|
497
|
+
const tblName = change.after?.tableName || change.before?.tableName || change.parentName;
|
|
498
|
+
name = tblName ? `${tblName}:${idxName}` : idxName;
|
|
499
|
+
}
|
|
500
|
+
else if (objType === 'CONSTRAINT' || objType === 'FOREIGN_KEY' || objType === 'PRIMARY_KEY' || objType === 'CHECK') {
|
|
501
|
+
type = objType.toLowerCase().replace(/_/g, ' ');
|
|
502
|
+
name = change.parentName ? `${change.parentName}::${change.objectName}` : change.objectName;
|
|
503
|
+
}
|
|
504
|
+
else if (objType === 'ENUM') {
|
|
505
|
+
type = 'enum';
|
|
506
|
+
name = change.objectName;
|
|
507
|
+
}
|
|
508
|
+
else if (objType === 'DOMAIN') {
|
|
509
|
+
type = 'domain';
|
|
510
|
+
name = change.objectName;
|
|
511
|
+
}
|
|
512
|
+
else if (objType === 'FUNCTION') {
|
|
513
|
+
type = 'function';
|
|
514
|
+
name = change.objectName;
|
|
515
|
+
}
|
|
516
|
+
else if (objType === 'TRIGGER') {
|
|
517
|
+
type = 'trigger';
|
|
518
|
+
name = change.objectName;
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
type = objType.toLowerCase().replace(/_/g, ' ');
|
|
522
|
+
name = change.objectName;
|
|
523
|
+
}
|
|
524
|
+
changeDisplays.push({ action, type, name });
|
|
525
|
+
}
|
|
464
526
|
const conflicts = detectObjectConflicts(localSnapshot, currentSchema);
|
|
465
527
|
if (conflicts.length > 0 && !force) {
|
|
466
528
|
const mergeState = {
|
|
@@ -488,20 +550,21 @@ async function pullCommand(context) {
|
|
|
488
550
|
return;
|
|
489
551
|
}
|
|
490
552
|
console.log(`${cli_utils_1.colors.yellow('Remote has changes:')}`);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
553
|
+
for (const chg of changeDisplays.slice(0, 15)) {
|
|
554
|
+
let colorFn = cli_utils_1.colors.cyan;
|
|
555
|
+
let prefix = '~';
|
|
556
|
+
if (chg.action === 'added') {
|
|
557
|
+
colorFn = cli_utils_1.colors.green;
|
|
558
|
+
prefix = '+';
|
|
559
|
+
}
|
|
560
|
+
else if (chg.action === 'removed') {
|
|
561
|
+
colorFn = cli_utils_1.colors.red;
|
|
562
|
+
prefix = '-';
|
|
563
|
+
}
|
|
564
|
+
console.log(` ${colorFn(prefix)} ${chg.type}: ${cli_utils_1.colors.bold(chg.name)}`);
|
|
502
565
|
}
|
|
503
|
-
if (
|
|
504
|
-
console.log(` ${cli_utils_1.colors.
|
|
566
|
+
if (changeDisplays.length > 15) {
|
|
567
|
+
console.log(` ${cli_utils_1.colors.muted(`... and ${changeDisplays.length - 15} more`)}`);
|
|
505
568
|
}
|
|
506
569
|
console.log('');
|
|
507
570
|
const noAutoMerge = flags['no-auto-merge'] === true;
|
|
@@ -575,12 +638,35 @@ async function pullCommand(context) {
|
|
|
575
638
|
console.log('');
|
|
576
639
|
return;
|
|
577
640
|
}
|
|
641
|
+
if (schemaExists && (includeFunctions || includeTriggers)) {
|
|
642
|
+
const existingContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
643
|
+
const hasPgFunction = /\bpgFunction\s*\(/.test(existingContent);
|
|
644
|
+
const hasPgTrigger = /\bpgTrigger\s*\(/.test(existingContent);
|
|
645
|
+
if (hasPgFunction || hasPgTrigger) {
|
|
646
|
+
const items = [];
|
|
647
|
+
if (hasPgFunction)
|
|
648
|
+
items.push('pgFunction');
|
|
649
|
+
if (hasPgTrigger)
|
|
650
|
+
items.push('pgTrigger');
|
|
651
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
652
|
+
(0, cli_utils_1.fatal)(`Existing schema contains ${items.join(' and ')} definitions`, `Functions and triggers should be in separate files:\n` +
|
|
653
|
+
` ${cli_utils_1.colors.cyan(`${schemaBaseName}.functions.ts`)} - for functions\n` +
|
|
654
|
+
` ${cli_utils_1.colors.cyan(`${schemaBaseName}.triggers.ts`)} - for triggers\n\n` +
|
|
655
|
+
`To migrate:\n` +
|
|
656
|
+
` 1. Move pgFunction definitions to ${schemaBaseName}.functions.ts\n` +
|
|
657
|
+
` 2. Move pgTrigger definitions to ${schemaBaseName}.triggers.ts\n` +
|
|
658
|
+
` 3. Run ${cli_utils_1.colors.cyan('relq pull')} again\n\n` +
|
|
659
|
+
`Or use ${cli_utils_1.colors.cyan('relq pull --force')} to overwrite and regenerate all files.`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
578
662
|
spinner.start('Generating TypeScript schema...');
|
|
579
663
|
const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
|
|
580
664
|
(0, ast_codegen_1.assignTrackingIds)(parsedSchema);
|
|
581
665
|
const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
|
|
582
666
|
camelCase: config.generate?.camelCase ?? true,
|
|
583
667
|
importPath: 'relq/schema-builder',
|
|
668
|
+
includeFunctions: false,
|
|
669
|
+
includeTriggers: false,
|
|
584
670
|
});
|
|
585
671
|
spinner.succeed('Generated TypeScript schema');
|
|
586
672
|
const schemaDir = path.dirname(schemaPath);
|
|
@@ -593,6 +679,35 @@ async function pullCommand(context) {
|
|
|
593
679
|
spinner.succeed(`Written ${cli_utils_1.colors.cyan(schemaPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(fileSize)})`)}`);
|
|
594
680
|
const fileHash = (0, repo_manager_1.hashFileContent)(typescript);
|
|
595
681
|
(0, repo_manager_1.saveFileHash)(fileHash, projectRoot);
|
|
682
|
+
if (includeFunctions && parsedSchema.functions.length > 0) {
|
|
683
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
684
|
+
const functionsPath = path.join(schemaDir, `${schemaBaseName}.functions.ts`);
|
|
685
|
+
const functionsCode = (0, ast_codegen_1.generateFunctionsFile)(parsedSchema, {
|
|
686
|
+
camelCase: config.generate?.camelCase ?? true,
|
|
687
|
+
importPath: 'relq/schema-builder',
|
|
688
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
689
|
+
});
|
|
690
|
+
if (functionsCode) {
|
|
691
|
+
fs.writeFileSync(functionsPath, functionsCode, 'utf-8');
|
|
692
|
+
const funcFileSize = Buffer.byteLength(functionsCode, 'utf8');
|
|
693
|
+
spinner.succeed(`Written ${cli_utils_1.colors.cyan(functionsPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(funcFileSize)})`)}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (includeTriggers && parsedSchema.triggers.length > 0) {
|
|
697
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
698
|
+
const triggersPath = path.join(schemaDir, `${schemaBaseName}.triggers.ts`);
|
|
699
|
+
const triggersCode = (0, ast_codegen_1.generateTriggersFile)(parsedSchema, {
|
|
700
|
+
camelCase: config.generate?.camelCase ?? true,
|
|
701
|
+
importPath: 'relq/schema-builder',
|
|
702
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
703
|
+
functionsImportPath: `./${schemaBaseName}.functions`,
|
|
704
|
+
});
|
|
705
|
+
if (triggersCode) {
|
|
706
|
+
fs.writeFileSync(triggersPath, triggersCode, 'utf-8');
|
|
707
|
+
const trigFileSize = Buffer.byteLength(triggersCode, 'utf8');
|
|
708
|
+
spinner.succeed(`Written ${cli_utils_1.colors.cyan(triggersPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(trigFileSize)})`)}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
596
711
|
const oldSnapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
597
712
|
const beforeSchema = oldSnapshot ? {
|
|
598
713
|
extensions: oldSnapshot.extensions?.map(e => e.name) || [],
|
|
@@ -16,7 +16,10 @@ async function syncCommand(context) {
|
|
|
16
16
|
const { projectRoot } = context;
|
|
17
17
|
console.log('');
|
|
18
18
|
if (!(0, repo_manager_1.isInitialized)(projectRoot)) {
|
|
19
|
-
|
|
19
|
+
console.log(`${cli_utils_1.colors.yellow('Initializing')} .relq repository...`);
|
|
20
|
+
(0, repo_manager_1.initRepository)(projectRoot);
|
|
21
|
+
console.log(`${cli_utils_1.colors.green('✓')} Repository initialized`);
|
|
22
|
+
console.log('');
|
|
20
23
|
}
|
|
21
24
|
const spinner = (0, cli_utils_1.createSpinner)();
|
|
22
25
|
try {
|
|
@@ -4,7 +4,10 @@ exports.resetTrackingIdCounter = resetTrackingIdCounter;
|
|
|
4
4
|
exports.assignTrackingIds = assignTrackingIds;
|
|
5
5
|
exports.copyTrackingIdsToNormalized = copyTrackingIdsToNormalized;
|
|
6
6
|
exports.generateTypeScriptFromAST = generateTypeScriptFromAST;
|
|
7
|
+
exports.generateFunctionsFile = generateFunctionsFile;
|
|
8
|
+
exports.generateTriggersFile = generateTriggersFile;
|
|
7
9
|
const utils_1 = require("./ast/codegen/utils.cjs");
|
|
10
|
+
const sql_formatter_1 = require("sql-formatter");
|
|
8
11
|
const builder_1 = require("./ast/codegen/builder.cjs");
|
|
9
12
|
const type_map_1 = require("./ast/codegen/type-map.cjs");
|
|
10
13
|
const defaults_1 = require("./ast/codegen/defaults.cjs");
|
|
@@ -552,16 +555,17 @@ function generateSequenceCode(seq, useCamelCase) {
|
|
|
552
555
|
const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
|
|
553
556
|
return `export const ${seqName} = pgSequence('${seq.name}'${optsStr})`;
|
|
554
557
|
}
|
|
555
|
-
function generateFunctionCode(func, useCamelCase) {
|
|
556
|
-
const funcName = useCamelCase ? (0, utils_1.toCamelCase)(func.name) : func.name;
|
|
558
|
+
function generateFunctionCode(func, useCamelCase, varNameOverride) {
|
|
559
|
+
const funcName = varNameOverride || (useCamelCase ? (0, utils_1.toCamelCase)(func.name) : func.name);
|
|
557
560
|
const parts = [];
|
|
558
561
|
parts.push(`export const ${funcName} = pgFunction('${func.name}', {`);
|
|
559
562
|
if (func.args.length > 0) {
|
|
560
|
-
const argsStr = func.args.map(arg => {
|
|
561
|
-
const argParts = [
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
563
|
+
const argsStr = func.args.map((arg, index) => {
|
|
564
|
+
const argParts = [];
|
|
565
|
+
const argName = arg.name || `$${index + 1}`;
|
|
566
|
+
argParts.push(`name: '${argName}'`);
|
|
567
|
+
argParts.push(`type: '${arg.type}'`);
|
|
568
|
+
if (arg.mode && arg.mode !== 'IN')
|
|
565
569
|
argParts.push(`mode: '${arg.mode}'`);
|
|
566
570
|
if (arg.default)
|
|
567
571
|
argParts.push(`default: '${(0, utils_1.escapeString)(arg.default)}'`);
|
|
@@ -571,8 +575,26 @@ function generateFunctionCode(func, useCamelCase) {
|
|
|
571
575
|
}
|
|
572
576
|
parts.push(` returns: '${func.returnType}',`);
|
|
573
577
|
parts.push(` language: '${func.language}',`);
|
|
574
|
-
|
|
575
|
-
|
|
578
|
+
let formattedBody = func.body;
|
|
579
|
+
const lang = func.language?.toLowerCase();
|
|
580
|
+
if (func.body && (lang === 'plpgsql' || lang === 'sql')) {
|
|
581
|
+
try {
|
|
582
|
+
formattedBody = (0, sql_formatter_1.format)(func.body, {
|
|
583
|
+
language: 'postgresql',
|
|
584
|
+
tabWidth: 4,
|
|
585
|
+
keywordCase: 'upper',
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
formattedBody = func.body;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const escapedBody = formattedBody.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
593
|
+
const indentedBody = escapedBody
|
|
594
|
+
.split('\n')
|
|
595
|
+
.map(line => ' ' + line)
|
|
596
|
+
.join('\n');
|
|
597
|
+
parts.push(` body: \`\n${indentedBody}\n \`,`);
|
|
576
598
|
if (func.volatility)
|
|
577
599
|
parts.push(` volatility: '${func.volatility}',`);
|
|
578
600
|
if (func.isStrict)
|
|
@@ -580,6 +602,9 @@ function generateFunctionCode(func, useCamelCase) {
|
|
|
580
602
|
if (func.securityDefiner)
|
|
581
603
|
parts.push(` securityDefiner: true,`);
|
|
582
604
|
parts.push(`})`);
|
|
605
|
+
if (func.trackingId) {
|
|
606
|
+
parts[parts.length - 1] = parts[parts.length - 1].replace('})', `}).$id('${func.trackingId}')`);
|
|
607
|
+
}
|
|
583
608
|
return parts.join('\n');
|
|
584
609
|
}
|
|
585
610
|
function generateViewCode(view, useCamelCase) {
|
|
@@ -1039,3 +1064,134 @@ function generateFKSQLComment(fk, camelCase) {
|
|
|
1039
1064
|
}
|
|
1040
1065
|
return parts.join(' | ');
|
|
1041
1066
|
}
|
|
1067
|
+
function resolveFunctionVarName(func, usedNames, useCamelCase) {
|
|
1068
|
+
const baseName = useCamelCase ? (0, utils_1.toCamelCase)(func.name) : func.name;
|
|
1069
|
+
const count = usedNames.get(baseName) || 0;
|
|
1070
|
+
usedNames.set(baseName, count + 1);
|
|
1071
|
+
if (count === 0) {
|
|
1072
|
+
return baseName;
|
|
1073
|
+
}
|
|
1074
|
+
return `${baseName}_${count + 1}`;
|
|
1075
|
+
}
|
|
1076
|
+
function generateFunctionsFile(schema, options = {}) {
|
|
1077
|
+
const { camelCase = true, importPath = 'relq/schema-builder', schemaImportPath = './schema', } = options;
|
|
1078
|
+
if (schema.functions.length === 0) {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
const parts = [];
|
|
1082
|
+
parts.push('/**');
|
|
1083
|
+
parts.push(' * Auto-generated by Relq CLI');
|
|
1084
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
1085
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
1086
|
+
parts.push(' */');
|
|
1087
|
+
parts.push('');
|
|
1088
|
+
parts.push(`import { pgFunction } from '${importPath}';`);
|
|
1089
|
+
void schemaImportPath;
|
|
1090
|
+
parts.push('');
|
|
1091
|
+
const usedNames = new Map();
|
|
1092
|
+
const functionVarNames = [];
|
|
1093
|
+
parts.push('// =============================================================================');
|
|
1094
|
+
parts.push('// FUNCTIONS');
|
|
1095
|
+
parts.push('// =============================================================================');
|
|
1096
|
+
parts.push('');
|
|
1097
|
+
for (const func of schema.functions) {
|
|
1098
|
+
const varName = resolveFunctionVarName(func, usedNames, camelCase);
|
|
1099
|
+
functionVarNames.push(varName);
|
|
1100
|
+
parts.push(generateFunctionCode(func, camelCase, varName));
|
|
1101
|
+
parts.push('');
|
|
1102
|
+
}
|
|
1103
|
+
parts.push('// =============================================================================');
|
|
1104
|
+
parts.push('// EXPORTS');
|
|
1105
|
+
parts.push('// =============================================================================');
|
|
1106
|
+
parts.push('');
|
|
1107
|
+
parts.push('export const functions = {');
|
|
1108
|
+
for (const name of functionVarNames) {
|
|
1109
|
+
parts.push(` ${name},`);
|
|
1110
|
+
}
|
|
1111
|
+
parts.push('} as const;');
|
|
1112
|
+
parts.push('');
|
|
1113
|
+
return parts.join('\n');
|
|
1114
|
+
}
|
|
1115
|
+
function generateTriggersFile(schema, options = {}) {
|
|
1116
|
+
const { camelCase = true, importPath = 'relq/schema-builder', schemaImportPath = './schema', functionsImportPath = './schema.functions', } = options;
|
|
1117
|
+
if (schema.triggers.length === 0) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
const parts = [];
|
|
1121
|
+
parts.push('/**');
|
|
1122
|
+
parts.push(' * Auto-generated by Relq CLI');
|
|
1123
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
1124
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
1125
|
+
parts.push(' */');
|
|
1126
|
+
parts.push('');
|
|
1127
|
+
parts.push(`import { pgTrigger } from '${importPath}';`);
|
|
1128
|
+
parts.push(`import { schema } from '${schemaImportPath}';`);
|
|
1129
|
+
if (schema.functions.length > 0) {
|
|
1130
|
+
parts.push(`import { functions } from '${functionsImportPath}';`);
|
|
1131
|
+
}
|
|
1132
|
+
parts.push('');
|
|
1133
|
+
const functionNames = new Set();
|
|
1134
|
+
const usedFuncNames = new Map();
|
|
1135
|
+
for (const func of schema.functions) {
|
|
1136
|
+
const varName = resolveFunctionVarName(func, usedFuncNames, camelCase);
|
|
1137
|
+
functionNames.add(varName);
|
|
1138
|
+
}
|
|
1139
|
+
parts.push('// =============================================================================');
|
|
1140
|
+
parts.push('// TRIGGERS');
|
|
1141
|
+
parts.push('// =============================================================================');
|
|
1142
|
+
parts.push('');
|
|
1143
|
+
const triggerVarNames = [];
|
|
1144
|
+
for (const trigger of schema.triggers) {
|
|
1145
|
+
const triggerName = camelCase ? (0, utils_1.toCamelCase)(trigger.name) : trigger.name;
|
|
1146
|
+
const tableName = camelCase ? (0, utils_1.toCamelCase)(trigger.table) : trigger.table;
|
|
1147
|
+
triggerVarNames.push(triggerName);
|
|
1148
|
+
parts.push(`export const ${triggerName} = pgTrigger('${trigger.name}', {`);
|
|
1149
|
+
parts.push(` on: schema.${tableName},`);
|
|
1150
|
+
const events = trigger.events.length === 1
|
|
1151
|
+
? `'${trigger.events[0]}'`
|
|
1152
|
+
: `[${trigger.events.map(e => `'${e}'`).join(', ')}]`;
|
|
1153
|
+
if (trigger.timing === 'BEFORE') {
|
|
1154
|
+
parts.push(` before: ${events},`);
|
|
1155
|
+
}
|
|
1156
|
+
else if (trigger.timing === 'AFTER') {
|
|
1157
|
+
parts.push(` after: ${events},`);
|
|
1158
|
+
}
|
|
1159
|
+
else if (trigger.timing === 'INSTEAD OF') {
|
|
1160
|
+
parts.push(` insteadOf: ${events},`);
|
|
1161
|
+
}
|
|
1162
|
+
parts.push(` forEach: '${trigger.forEach || 'ROW'}',`);
|
|
1163
|
+
const funcVarName = camelCase ? (0, utils_1.toCamelCase)(trigger.functionName) : trigger.functionName;
|
|
1164
|
+
if (functionNames.has(funcVarName)) {
|
|
1165
|
+
parts.push(` execute: functions.${funcVarName},`);
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
parts.push(` execute: '${trigger.functionName}',`);
|
|
1169
|
+
}
|
|
1170
|
+
if (trigger.whenClause)
|
|
1171
|
+
parts.push(` when: '${(0, utils_1.escapeString)(trigger.whenClause)}',`);
|
|
1172
|
+
if (trigger.isConstraint)
|
|
1173
|
+
parts.push(` constraint: true,`);
|
|
1174
|
+
if (trigger.deferrable)
|
|
1175
|
+
parts.push(` deferrable: true,`);
|
|
1176
|
+
if (trigger.initiallyDeferred)
|
|
1177
|
+
parts.push(` initially: 'DEFERRED',`);
|
|
1178
|
+
if (trigger.trackingId) {
|
|
1179
|
+
parts.push(`}).$id('${trigger.trackingId}');`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
parts.push(`});`);
|
|
1183
|
+
}
|
|
1184
|
+
parts.push('');
|
|
1185
|
+
}
|
|
1186
|
+
parts.push('// =============================================================================');
|
|
1187
|
+
parts.push('// EXPORTS');
|
|
1188
|
+
parts.push('// =============================================================================');
|
|
1189
|
+
parts.push('');
|
|
1190
|
+
parts.push('export const triggers = {');
|
|
1191
|
+
for (const name of triggerVarNames) {
|
|
1192
|
+
parts.push(` ${name},`);
|
|
1193
|
+
}
|
|
1194
|
+
parts.push('} as const;');
|
|
1195
|
+
parts.push('');
|
|
1196
|
+
return parts.join('\n');
|
|
1197
|
+
}
|
|
@@ -708,7 +708,8 @@ async function introspectedToParsedSchema(schema) {
|
|
|
708
708
|
args: parseArgTypes(f.argTypes),
|
|
709
709
|
returnType: f.returnType,
|
|
710
710
|
language: f.language,
|
|
711
|
-
body: f.definition || '',
|
|
711
|
+
body: extractFunctionBody(f.definition || ''),
|
|
712
|
+
volatility: f.volatility,
|
|
712
713
|
isStrict: false,
|
|
713
714
|
securityDefiner: false,
|
|
714
715
|
});
|
|
@@ -719,7 +720,7 @@ async function introspectedToParsedSchema(schema) {
|
|
|
719
720
|
table: t.tableName,
|
|
720
721
|
timing: t.timing,
|
|
721
722
|
events: [t.event],
|
|
722
|
-
forEach: '
|
|
723
|
+
forEach: t.forEach || 'ROW',
|
|
723
724
|
functionName: t.functionName || '',
|
|
724
725
|
isConstraint: false,
|
|
725
726
|
});
|
|
@@ -809,6 +810,19 @@ function parseArgTypes(argTypes) {
|
|
|
809
810
|
type: typeof arg === 'string' ? arg.trim() : String(arg),
|
|
810
811
|
}));
|
|
811
812
|
}
|
|
813
|
+
function extractFunctionBody(definition) {
|
|
814
|
+
if (!definition)
|
|
815
|
+
return '';
|
|
816
|
+
const bodyMatch = definition.match(/AS\s+\$([a-zA-Z_]*)\$\s*([\s\S]*?)\s*\$\1\$/i);
|
|
817
|
+
if (bodyMatch) {
|
|
818
|
+
return bodyMatch[2].trim();
|
|
819
|
+
}
|
|
820
|
+
const singleQuoteMatch = definition.match(/AS\s+'([\s\S]*?)'\s*$/i);
|
|
821
|
+
if (singleQuoteMatch) {
|
|
822
|
+
return singleQuoteMatch[1].replace(/''/g, "'");
|
|
823
|
+
}
|
|
824
|
+
return '';
|
|
825
|
+
}
|
|
812
826
|
function normalizedToParsedSchema(schema) {
|
|
813
827
|
return {
|
|
814
828
|
extensions: (schema.extensions || []).map(e => typeof e === 'string' ? e : e.name),
|
|
@@ -453,6 +453,10 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
453
453
|
JOIN pg_language l ON p.prolang = l.oid
|
|
454
454
|
WHERE n.nspname = 'public'
|
|
455
455
|
AND p.prokind IN ('f', 'a')
|
|
456
|
+
-- Exclude C language functions (typically extension functions like pgcrypto)
|
|
457
|
+
AND l.lanname != 'c'
|
|
458
|
+
-- Exclude internal language functions
|
|
459
|
+
AND l.lanname != 'internal'
|
|
456
460
|
ORDER BY p.proname;
|
|
457
461
|
`);
|
|
458
462
|
functions = functionsResult.rows.map(f => ({
|
|
@@ -486,6 +490,10 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
486
490
|
WHEN t.tgtype & 16 > 0 THEN 'UPDATE'
|
|
487
491
|
ELSE 'UNKNOWN'
|
|
488
492
|
END as event,
|
|
493
|
+
CASE
|
|
494
|
+
WHEN t.tgtype & 1 > 0 THEN 'ROW'
|
|
495
|
+
ELSE 'STATEMENT'
|
|
496
|
+
END as for_each,
|
|
489
497
|
p.proname as function_name,
|
|
490
498
|
pg_get_triggerdef(t.oid) as definition,
|
|
491
499
|
t.tgenabled != 'D' as is_enabled
|
|
@@ -495,6 +503,11 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
495
503
|
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
496
504
|
WHERE n.nspname = 'public'
|
|
497
505
|
AND NOT t.tgisinternal
|
|
506
|
+
-- Exclude triggers on partition child tables
|
|
507
|
+
AND NOT EXISTS (
|
|
508
|
+
SELECT 1 FROM pg_inherits i
|
|
509
|
+
WHERE i.inhrelid = c.oid
|
|
510
|
+
)
|
|
498
511
|
ORDER BY c.relname, t.tgname;
|
|
499
512
|
`);
|
|
500
513
|
triggers = triggersResult.rows.map(t => ({
|
|
@@ -502,6 +515,7 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
502
515
|
tableName: t.table_name,
|
|
503
516
|
timing: t.timing,
|
|
504
517
|
event: t.event,
|
|
518
|
+
forEach: t.for_each,
|
|
505
519
|
functionName: t.function_name,
|
|
506
520
|
definition: t.definition || '',
|
|
507
521
|
isEnabled: t.is_enabled,
|
|
@@ -91,6 +91,30 @@ function compareDomains(before, after) {
|
|
|
91
91
|
changes.push((0, change_tracker_1.createChange)('DROP', 'DOMAIN', name, domain, null));
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
for (const [name, afterDomain] of afterMap) {
|
|
95
|
+
const beforeDomain = beforeMap.get(name);
|
|
96
|
+
if (!beforeDomain)
|
|
97
|
+
continue;
|
|
98
|
+
const baseTypeChanged = beforeDomain.baseType !== afterDomain.baseType;
|
|
99
|
+
const notNullChanged = (beforeDomain.isNotNull || false) !== (afterDomain.isNotNull || false);
|
|
100
|
+
const defaultChanged = (beforeDomain.defaultValue || null) !== (afterDomain.defaultValue || null);
|
|
101
|
+
const checkChanged = (beforeDomain.checkExpression || null) !== (afterDomain.checkExpression || null);
|
|
102
|
+
if (baseTypeChanged || notNullChanged || defaultChanged || checkChanged) {
|
|
103
|
+
changes.push((0, change_tracker_1.createChange)('ALTER', 'DOMAIN', name, {
|
|
104
|
+
name: beforeDomain.name,
|
|
105
|
+
baseType: beforeDomain.baseType,
|
|
106
|
+
notNull: beforeDomain.isNotNull,
|
|
107
|
+
default: beforeDomain.defaultValue,
|
|
108
|
+
check: beforeDomain.checkExpression,
|
|
109
|
+
}, {
|
|
110
|
+
name: afterDomain.name,
|
|
111
|
+
baseType: afterDomain.baseType,
|
|
112
|
+
notNull: afterDomain.isNotNull,
|
|
113
|
+
default: afterDomain.defaultValue,
|
|
114
|
+
check: afterDomain.checkExpression,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
return changes;
|
|
95
119
|
}
|
|
96
120
|
function compareCompositeTypes(before, after) {
|