relq 1.0.26 → 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.
@@ -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,
@@ -375,10 +375,154 @@ async function pullCommand(context) {
375
375
  (0, cli_utils_1.fatal)('You have unresolved merge conflicts', `Use ${cli_utils_1.colors.cyan('relq resolve')} to see and resolve conflicts\nOr use ${cli_utils_1.colors.cyan('relq pull --force')} to overwrite local`);
376
376
  }
377
377
  if (schemaExists && localSnapshot && !force) {
378
- const localTables = new Set(localSnapshot.tables.map(t => t.name));
379
- const remoteTables = new Set(currentSchema.tables.map(t => t.name));
380
- const added = [...remoteTables].filter(t => !localTables.has(t));
381
- const removed = [...localTables].filter(t => !remoteTables.has(t));
378
+ const localForCompare = {
379
+ extensions: localSnapshot.extensions?.map(e => e.name) || [],
380
+ enums: localSnapshot.enums || [],
381
+ domains: localSnapshot.domains?.map(d => ({
382
+ name: d.name,
383
+ baseType: d.baseType,
384
+ isNotNull: d.notNull,
385
+ defaultValue: d.default,
386
+ checkExpression: d.check,
387
+ })) || [],
388
+ compositeTypes: localSnapshot.compositeTypes || [],
389
+ sequences: localSnapshot.sequences || [],
390
+ tables: localSnapshot.tables.map(t => ({
391
+ name: t.name,
392
+ schema: t.schema,
393
+ columns: t.columns.map(c => ({
394
+ name: c.name,
395
+ dataType: c.type,
396
+ isNullable: c.nullable,
397
+ defaultValue: c.default,
398
+ isPrimaryKey: c.primaryKey,
399
+ isUnique: c.unique,
400
+ comment: c.comment,
401
+ })),
402
+ indexes: t.indexes.map(i => ({
403
+ name: i.name,
404
+ columns: i.columns,
405
+ isUnique: i.unique,
406
+ type: i.type,
407
+ comment: i.comment,
408
+ })),
409
+ constraints: t.constraints || [],
410
+ isPartitioned: t.isPartitioned,
411
+ partitionType: t.partitionType,
412
+ partitionKey: t.partitionKey,
413
+ comment: t.comment,
414
+ })),
415
+ functions: localSnapshot.functions || [],
416
+ triggers: localSnapshot.triggers || [],
417
+ };
418
+ const remoteForCompare = {
419
+ extensions: dbSchema.extensions || [],
420
+ enums: filteredEnums || [],
421
+ domains: filteredDomains?.map(d => ({
422
+ name: d.name,
423
+ baseType: d.baseType,
424
+ isNotNull: d.isNotNull,
425
+ defaultValue: d.defaultValue,
426
+ checkExpression: d.checkExpression,
427
+ })) || [],
428
+ compositeTypes: filteredCompositeTypes || [],
429
+ sequences: [],
430
+ tables: filteredTables.map(t => ({
431
+ name: t.name,
432
+ schema: t.schema,
433
+ columns: t.columns.map(c => ({
434
+ name: c.name,
435
+ dataType: c.dataType,
436
+ isNullable: c.isNullable,
437
+ defaultValue: c.defaultValue,
438
+ isPrimaryKey: c.isPrimaryKey,
439
+ isUnique: c.isUnique,
440
+ comment: c.comment,
441
+ })),
442
+ indexes: t.indexes.map(i => ({
443
+ name: i.name,
444
+ columns: i.columns,
445
+ isUnique: i.isUnique,
446
+ type: i.type,
447
+ comment: i.comment,
448
+ })),
449
+ constraints: t.constraints || [],
450
+ isPartitioned: t.isPartitioned,
451
+ partitionType: t.partitionType,
452
+ partitionKey: t.partitionKey,
453
+ comment: t.comment,
454
+ })),
455
+ functions: filteredFunctions || [],
456
+ triggers: filteredTriggers || [],
457
+ };
458
+ const allChanges = (0, schema_comparator_1.compareSchemas)(localForCompare, remoteForCompare);
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
+ }
382
526
  const conflicts = detectObjectConflicts(localSnapshot, currentSchema);
383
527
  if (conflicts.length > 0 && !force) {
384
528
  const mergeState = {
@@ -400,17 +544,27 @@ async function pullCommand(context) {
400
544
  }
401
545
  (0, cli_utils_1.fatal)('Automatic merge failed; fix conflicts and then commit', `${cli_utils_1.colors.cyan('relq resolve --theirs <name>')} Take remote version\n${cli_utils_1.colors.cyan('relq resolve --all-theirs')} Take all remote\n${cli_utils_1.colors.cyan('relq pull --force')} Force overwrite local`);
402
546
  }
403
- if (added.length === 0 && removed.length === 0) {
547
+ if (allChanges.length === 0) {
404
548
  console.log('Already up to date with remote');
405
549
  console.log('');
406
550
  return;
407
551
  }
408
552
  console.log(`${cli_utils_1.colors.yellow('Remote has changes:')}`);
409
- if (added.length > 0) {
410
- console.log(` ${cli_utils_1.colors.green(`+${added.length}`)} tables added`);
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)}`);
411
565
  }
412
- if (removed.length > 0) {
413
- console.log(` ${cli_utils_1.colors.red(`-${removed.length}`)} tables removed`);
566
+ if (changeDisplays.length > 15) {
567
+ console.log(` ${cli_utils_1.colors.muted(`... and ${changeDisplays.length - 15} more`)}`);
414
568
  }
415
569
  console.log('');
416
570
  const noAutoMerge = flags['no-auto-merge'] === true;
@@ -484,12 +638,35 @@ async function pullCommand(context) {
484
638
  console.log('');
485
639
  return;
486
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
+ }
487
662
  spinner.start('Generating TypeScript schema...');
488
663
  const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
489
664
  (0, ast_codegen_1.assignTrackingIds)(parsedSchema);
490
665
  const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
491
666
  camelCase: config.generate?.camelCase ?? true,
492
667
  importPath: 'relq/schema-builder',
668
+ includeFunctions: false,
669
+ includeTriggers: false,
493
670
  });
494
671
  spinner.succeed('Generated TypeScript schema');
495
672
  const schemaDir = path.dirname(schemaPath);
@@ -502,6 +679,35 @@ async function pullCommand(context) {
502
679
  spinner.succeed(`Written ${cli_utils_1.colors.cyan(schemaPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(fileSize)})`)}`);
503
680
  const fileHash = (0, repo_manager_1.hashFileContent)(typescript);
504
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
+ }
505
711
  const oldSnapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
506
712
  const beforeSchema = oldSnapshot ? {
507
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),