relq 1.0.27 → 1.0.29

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.
@@ -265,6 +265,7 @@ function normalizedToDbSchema(normalized) {
265
265
  tableName: t.table,
266
266
  event: t.events.join(' OR '),
267
267
  timing: t.timing,
268
+ forEach: t.forEach || 'ROW',
268
269
  functionName: t.functionName,
269
270
  definition: '',
270
271
  isEnabled: t.isEnabled !== false,
@@ -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: 'STATEMENT',
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 tablesAdded = allChanges.filter(c => c.type === 'CREATE' && c.objectType === 'TABLE').length;
460
- const tablesRemoved = allChanges.filter(c => c.type === 'DROP' && c.objectType === 'TABLE').length;
461
- const columnsChanged = allChanges.filter(c => c.objectType === 'COLUMN').length;
462
- const indexesChanged = allChanges.filter(c => c.objectType === 'INDEX').length;
463
- const otherChanges = allChanges.filter(c => c.objectType !== 'TABLE' && c.objectType !== 'COLUMN' && c.objectType !== 'INDEX').length;
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
- if (tablesAdded > 0) {
492
- console.log(` ${cli_utils_1.colors.green(`+${tablesAdded}`)} table(s) added`);
493
- }
494
- if (tablesRemoved > 0) {
495
- console.log(` ${cli_utils_1.colors.red(`-${tablesRemoved}`)} table(s) removed`);
496
- }
497
- if (columnsChanged > 0) {
498
- console.log(` ${cli_utils_1.colors.cyan(`~${columnsChanged}`)} column change(s)`);
499
- }
500
- if (indexesChanged > 0) {
501
- console.log(` ${cli_utils_1.colors.cyan(`~${indexesChanged}`)} index change(s)`);
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 (otherChanges > 0) {
504
- console.log(` ${cli_utils_1.colors.cyan(`~${otherChanges}`)} other change(s)`);
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
- (0, cli_utils_1.fatal)('not a relq repository (or any parent directories): .relq', `Run ${cli_utils_1.colors.cyan('relq init')} to initialize.`);
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 = [`type: '${arg.type}'`];
562
- if (arg.name)
563
- argParts.push(`name: '${arg.name}'`);
564
- if (arg.mode)
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
- const escapedBody = func.body.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
575
- parts.push(` body: \`${escapedBody}\`,`);
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: 'STATEMENT',
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) {