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.
- 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 +4 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toCamelCase, escapeString, isBalanced, } from "./ast/codegen/utils.js";
|
|
2
|
+
import { format as formatSQL } from 'sql-formatter';
|
|
2
3
|
import { astToBuilder } from "./ast/codegen/builder.js";
|
|
3
4
|
import { getColumnBuilder, getColumnBuilderWithInfo } from "./ast/codegen/type-map.js";
|
|
4
5
|
import { formatDefaultValue, resetDefaultImportFlags, getDefaultImportNeeded, getDefaultSqlImportNeeded, } from "./ast/codegen/defaults.js";
|
|
@@ -546,16 +547,17 @@ function generateSequenceCode(seq, useCamelCase) {
|
|
|
546
547
|
const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
|
|
547
548
|
return `export const ${seqName} = pgSequence('${seq.name}'${optsStr})`;
|
|
548
549
|
}
|
|
549
|
-
function generateFunctionCode(func, useCamelCase) {
|
|
550
|
-
const funcName = useCamelCase ? toCamelCase(func.name) : func.name;
|
|
550
|
+
function generateFunctionCode(func, useCamelCase, varNameOverride) {
|
|
551
|
+
const funcName = varNameOverride || (useCamelCase ? toCamelCase(func.name) : func.name);
|
|
551
552
|
const parts = [];
|
|
552
553
|
parts.push(`export const ${funcName} = pgFunction('${func.name}', {`);
|
|
553
554
|
if (func.args.length > 0) {
|
|
554
|
-
const argsStr = func.args.map(arg => {
|
|
555
|
-
const argParts = [
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
555
|
+
const argsStr = func.args.map((arg, index) => {
|
|
556
|
+
const argParts = [];
|
|
557
|
+
const argName = arg.name || `$${index + 1}`;
|
|
558
|
+
argParts.push(`name: '${argName}'`);
|
|
559
|
+
argParts.push(`type: '${arg.type}'`);
|
|
560
|
+
if (arg.mode && arg.mode !== 'IN')
|
|
559
561
|
argParts.push(`mode: '${arg.mode}'`);
|
|
560
562
|
if (arg.default)
|
|
561
563
|
argParts.push(`default: '${escapeString(arg.default)}'`);
|
|
@@ -565,8 +567,26 @@ function generateFunctionCode(func, useCamelCase) {
|
|
|
565
567
|
}
|
|
566
568
|
parts.push(` returns: '${func.returnType}',`);
|
|
567
569
|
parts.push(` language: '${func.language}',`);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
+
let formattedBody = func.body;
|
|
571
|
+
const lang = func.language?.toLowerCase();
|
|
572
|
+
if (func.body && (lang === 'plpgsql' || lang === 'sql')) {
|
|
573
|
+
try {
|
|
574
|
+
formattedBody = formatSQL(func.body, {
|
|
575
|
+
language: 'postgresql',
|
|
576
|
+
tabWidth: 4,
|
|
577
|
+
keywordCase: 'upper',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
formattedBody = func.body;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const escapedBody = formattedBody.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
585
|
+
const indentedBody = escapedBody
|
|
586
|
+
.split('\n')
|
|
587
|
+
.map(line => ' ' + line)
|
|
588
|
+
.join('\n');
|
|
589
|
+
parts.push(` body: \`\n${indentedBody}\n \`,`);
|
|
570
590
|
if (func.volatility)
|
|
571
591
|
parts.push(` volatility: '${func.volatility}',`);
|
|
572
592
|
if (func.isStrict)
|
|
@@ -574,6 +594,9 @@ function generateFunctionCode(func, useCamelCase) {
|
|
|
574
594
|
if (func.securityDefiner)
|
|
575
595
|
parts.push(` securityDefiner: true,`);
|
|
576
596
|
parts.push(`})`);
|
|
597
|
+
if (func.trackingId) {
|
|
598
|
+
parts[parts.length - 1] = parts[parts.length - 1].replace('})', `}).$id('${func.trackingId}')`);
|
|
599
|
+
}
|
|
577
600
|
return parts.join('\n');
|
|
578
601
|
}
|
|
579
602
|
function generateViewCode(view, useCamelCase) {
|
|
@@ -1033,3 +1056,134 @@ function generateFKSQLComment(fk, camelCase) {
|
|
|
1033
1056
|
}
|
|
1034
1057
|
return parts.join(' | ');
|
|
1035
1058
|
}
|
|
1059
|
+
function resolveFunctionVarName(func, usedNames, useCamelCase) {
|
|
1060
|
+
const baseName = useCamelCase ? toCamelCase(func.name) : func.name;
|
|
1061
|
+
const count = usedNames.get(baseName) || 0;
|
|
1062
|
+
usedNames.set(baseName, count + 1);
|
|
1063
|
+
if (count === 0) {
|
|
1064
|
+
return baseName;
|
|
1065
|
+
}
|
|
1066
|
+
return `${baseName}_${count + 1}`;
|
|
1067
|
+
}
|
|
1068
|
+
export function generateFunctionsFile(schema, options = {}) {
|
|
1069
|
+
const { camelCase = true, importPath = 'relq/schema-builder', schemaImportPath = './schema', } = options;
|
|
1070
|
+
if (schema.functions.length === 0) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
const parts = [];
|
|
1074
|
+
parts.push('/**');
|
|
1075
|
+
parts.push(' * Auto-generated by Relq CLI');
|
|
1076
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
1077
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
1078
|
+
parts.push(' */');
|
|
1079
|
+
parts.push('');
|
|
1080
|
+
parts.push(`import { pgFunction } from '${importPath}';`);
|
|
1081
|
+
void schemaImportPath;
|
|
1082
|
+
parts.push('');
|
|
1083
|
+
const usedNames = new Map();
|
|
1084
|
+
const functionVarNames = [];
|
|
1085
|
+
parts.push('// =============================================================================');
|
|
1086
|
+
parts.push('// FUNCTIONS');
|
|
1087
|
+
parts.push('// =============================================================================');
|
|
1088
|
+
parts.push('');
|
|
1089
|
+
for (const func of schema.functions) {
|
|
1090
|
+
const varName = resolveFunctionVarName(func, usedNames, camelCase);
|
|
1091
|
+
functionVarNames.push(varName);
|
|
1092
|
+
parts.push(generateFunctionCode(func, camelCase, varName));
|
|
1093
|
+
parts.push('');
|
|
1094
|
+
}
|
|
1095
|
+
parts.push('// =============================================================================');
|
|
1096
|
+
parts.push('// EXPORTS');
|
|
1097
|
+
parts.push('// =============================================================================');
|
|
1098
|
+
parts.push('');
|
|
1099
|
+
parts.push('export const functions = {');
|
|
1100
|
+
for (const name of functionVarNames) {
|
|
1101
|
+
parts.push(` ${name},`);
|
|
1102
|
+
}
|
|
1103
|
+
parts.push('} as const;');
|
|
1104
|
+
parts.push('');
|
|
1105
|
+
return parts.join('\n');
|
|
1106
|
+
}
|
|
1107
|
+
export function generateTriggersFile(schema, options = {}) {
|
|
1108
|
+
const { camelCase = true, importPath = 'relq/schema-builder', schemaImportPath = './schema', functionsImportPath = './schema.functions', } = options;
|
|
1109
|
+
if (schema.triggers.length === 0) {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
const parts = [];
|
|
1113
|
+
parts.push('/**');
|
|
1114
|
+
parts.push(' * Auto-generated by Relq CLI');
|
|
1115
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
1116
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
1117
|
+
parts.push(' */');
|
|
1118
|
+
parts.push('');
|
|
1119
|
+
parts.push(`import { pgTrigger } from '${importPath}';`);
|
|
1120
|
+
parts.push(`import { schema } from '${schemaImportPath}';`);
|
|
1121
|
+
if (schema.functions.length > 0) {
|
|
1122
|
+
parts.push(`import { functions } from '${functionsImportPath}';`);
|
|
1123
|
+
}
|
|
1124
|
+
parts.push('');
|
|
1125
|
+
const functionNames = new Set();
|
|
1126
|
+
const usedFuncNames = new Map();
|
|
1127
|
+
for (const func of schema.functions) {
|
|
1128
|
+
const varName = resolveFunctionVarName(func, usedFuncNames, camelCase);
|
|
1129
|
+
functionNames.add(varName);
|
|
1130
|
+
}
|
|
1131
|
+
parts.push('// =============================================================================');
|
|
1132
|
+
parts.push('// TRIGGERS');
|
|
1133
|
+
parts.push('// =============================================================================');
|
|
1134
|
+
parts.push('');
|
|
1135
|
+
const triggerVarNames = [];
|
|
1136
|
+
for (const trigger of schema.triggers) {
|
|
1137
|
+
const triggerName = camelCase ? toCamelCase(trigger.name) : trigger.name;
|
|
1138
|
+
const tableName = camelCase ? toCamelCase(trigger.table) : trigger.table;
|
|
1139
|
+
triggerVarNames.push(triggerName);
|
|
1140
|
+
parts.push(`export const ${triggerName} = pgTrigger('${trigger.name}', {`);
|
|
1141
|
+
parts.push(` on: schema.${tableName},`);
|
|
1142
|
+
const events = trigger.events.length === 1
|
|
1143
|
+
? `'${trigger.events[0]}'`
|
|
1144
|
+
: `[${trigger.events.map(e => `'${e}'`).join(', ')}]`;
|
|
1145
|
+
if (trigger.timing === 'BEFORE') {
|
|
1146
|
+
parts.push(` before: ${events},`);
|
|
1147
|
+
}
|
|
1148
|
+
else if (trigger.timing === 'AFTER') {
|
|
1149
|
+
parts.push(` after: ${events},`);
|
|
1150
|
+
}
|
|
1151
|
+
else if (trigger.timing === 'INSTEAD OF') {
|
|
1152
|
+
parts.push(` insteadOf: ${events},`);
|
|
1153
|
+
}
|
|
1154
|
+
parts.push(` forEach: '${trigger.forEach || 'ROW'}',`);
|
|
1155
|
+
const funcVarName = camelCase ? toCamelCase(trigger.functionName) : trigger.functionName;
|
|
1156
|
+
if (functionNames.has(funcVarName)) {
|
|
1157
|
+
parts.push(` execute: functions.${funcVarName},`);
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
parts.push(` execute: '${trigger.functionName}',`);
|
|
1161
|
+
}
|
|
1162
|
+
if (trigger.whenClause)
|
|
1163
|
+
parts.push(` when: '${escapeString(trigger.whenClause)}',`);
|
|
1164
|
+
if (trigger.isConstraint)
|
|
1165
|
+
parts.push(` constraint: true,`);
|
|
1166
|
+
if (trigger.deferrable)
|
|
1167
|
+
parts.push(` deferrable: true,`);
|
|
1168
|
+
if (trigger.initiallyDeferred)
|
|
1169
|
+
parts.push(` initially: 'DEFERRED',`);
|
|
1170
|
+
if (trigger.trackingId) {
|
|
1171
|
+
parts.push(`}).$id('${trigger.trackingId}');`);
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
parts.push(`});`);
|
|
1175
|
+
}
|
|
1176
|
+
parts.push('');
|
|
1177
|
+
}
|
|
1178
|
+
parts.push('// =============================================================================');
|
|
1179
|
+
parts.push('// EXPORTS');
|
|
1180
|
+
parts.push('// =============================================================================');
|
|
1181
|
+
parts.push('');
|
|
1182
|
+
parts.push('export const triggers = {');
|
|
1183
|
+
for (const name of triggerVarNames) {
|
|
1184
|
+
parts.push(` ${name},`);
|
|
1185
|
+
}
|
|
1186
|
+
parts.push('} as const;');
|
|
1187
|
+
parts.push('');
|
|
1188
|
+
return parts.join('\n');
|
|
1189
|
+
}
|
|
@@ -698,7 +698,8 @@ export async function introspectedToParsedSchema(schema) {
|
|
|
698
698
|
args: parseArgTypes(f.argTypes),
|
|
699
699
|
returnType: f.returnType,
|
|
700
700
|
language: f.language,
|
|
701
|
-
body: f.definition || '',
|
|
701
|
+
body: extractFunctionBody(f.definition || ''),
|
|
702
|
+
volatility: f.volatility,
|
|
702
703
|
isStrict: false,
|
|
703
704
|
securityDefiner: false,
|
|
704
705
|
});
|
|
@@ -709,7 +710,7 @@ export async function introspectedToParsedSchema(schema) {
|
|
|
709
710
|
table: t.tableName,
|
|
710
711
|
timing: t.timing,
|
|
711
712
|
events: [t.event],
|
|
712
|
-
forEach: '
|
|
713
|
+
forEach: t.forEach || 'ROW',
|
|
713
714
|
functionName: t.functionName || '',
|
|
714
715
|
isConstraint: false,
|
|
715
716
|
});
|
|
@@ -799,6 +800,19 @@ function parseArgTypes(argTypes) {
|
|
|
799
800
|
type: typeof arg === 'string' ? arg.trim() : String(arg),
|
|
800
801
|
}));
|
|
801
802
|
}
|
|
803
|
+
function extractFunctionBody(definition) {
|
|
804
|
+
if (!definition)
|
|
805
|
+
return '';
|
|
806
|
+
const bodyMatch = definition.match(/AS\s+\$([a-zA-Z_]*)\$\s*([\s\S]*?)\s*\$\1\$/i);
|
|
807
|
+
if (bodyMatch) {
|
|
808
|
+
return bodyMatch[2].trim();
|
|
809
|
+
}
|
|
810
|
+
const singleQuoteMatch = definition.match(/AS\s+'([\s\S]*?)'\s*$/i);
|
|
811
|
+
if (singleQuoteMatch) {
|
|
812
|
+
return singleQuoteMatch[1].replace(/''/g, "'");
|
|
813
|
+
}
|
|
814
|
+
return '';
|
|
815
|
+
}
|
|
802
816
|
export { parse, deparse };
|
|
803
817
|
export function normalizedToParsedSchema(schema) {
|
|
804
818
|
return {
|
|
@@ -417,6 +417,10 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
417
417
|
JOIN pg_language l ON p.prolang = l.oid
|
|
418
418
|
WHERE n.nspname = 'public'
|
|
419
419
|
AND p.prokind IN ('f', 'a')
|
|
420
|
+
-- Exclude C language functions (typically extension functions like pgcrypto)
|
|
421
|
+
AND l.lanname != 'c'
|
|
422
|
+
-- Exclude internal language functions
|
|
423
|
+
AND l.lanname != 'internal'
|
|
420
424
|
ORDER BY p.proname;
|
|
421
425
|
`);
|
|
422
426
|
functions = functionsResult.rows.map(f => ({
|
|
@@ -450,6 +454,10 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
450
454
|
WHEN t.tgtype & 16 > 0 THEN 'UPDATE'
|
|
451
455
|
ELSE 'UNKNOWN'
|
|
452
456
|
END as event,
|
|
457
|
+
CASE
|
|
458
|
+
WHEN t.tgtype & 1 > 0 THEN 'ROW'
|
|
459
|
+
ELSE 'STATEMENT'
|
|
460
|
+
END as for_each,
|
|
453
461
|
p.proname as function_name,
|
|
454
462
|
pg_get_triggerdef(t.oid) as definition,
|
|
455
463
|
t.tgenabled != 'D' as is_enabled
|
|
@@ -459,6 +467,11 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
459
467
|
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
460
468
|
WHERE n.nspname = 'public'
|
|
461
469
|
AND NOT t.tgisinternal
|
|
470
|
+
-- Exclude triggers on partition child tables
|
|
471
|
+
AND NOT EXISTS (
|
|
472
|
+
SELECT 1 FROM pg_inherits i
|
|
473
|
+
WHERE i.inhrelid = c.oid
|
|
474
|
+
)
|
|
462
475
|
ORDER BY c.relname, t.tgname;
|
|
463
476
|
`);
|
|
464
477
|
triggers = triggersResult.rows.map(t => ({
|
|
@@ -466,6 +479,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
|
|
|
466
479
|
tableName: t.table_name,
|
|
467
480
|
timing: t.timing,
|
|
468
481
|
event: t.event,
|
|
482
|
+
forEach: t.for_each,
|
|
469
483
|
functionName: t.function_name,
|
|
470
484
|
definition: t.definition || '',
|
|
471
485
|
isEnabled: t.is_enabled,
|
|
@@ -84,6 +84,30 @@ function compareDomains(before, after) {
|
|
|
84
84
|
changes.push(createChange('DROP', 'DOMAIN', name, domain, null));
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
for (const [name, afterDomain] of afterMap) {
|
|
88
|
+
const beforeDomain = beforeMap.get(name);
|
|
89
|
+
if (!beforeDomain)
|
|
90
|
+
continue;
|
|
91
|
+
const baseTypeChanged = beforeDomain.baseType !== afterDomain.baseType;
|
|
92
|
+
const notNullChanged = (beforeDomain.isNotNull || false) !== (afterDomain.isNotNull || false);
|
|
93
|
+
const defaultChanged = (beforeDomain.defaultValue || null) !== (afterDomain.defaultValue || null);
|
|
94
|
+
const checkChanged = (beforeDomain.checkExpression || null) !== (afterDomain.checkExpression || null);
|
|
95
|
+
if (baseTypeChanged || notNullChanged || defaultChanged || checkChanged) {
|
|
96
|
+
changes.push(createChange('ALTER', 'DOMAIN', name, {
|
|
97
|
+
name: beforeDomain.name,
|
|
98
|
+
baseType: beforeDomain.baseType,
|
|
99
|
+
notNull: beforeDomain.isNotNull,
|
|
100
|
+
default: beforeDomain.defaultValue,
|
|
101
|
+
check: beforeDomain.checkExpression,
|
|
102
|
+
}, {
|
|
103
|
+
name: afterDomain.name,
|
|
104
|
+
baseType: afterDomain.baseType,
|
|
105
|
+
notNull: afterDomain.isNotNull,
|
|
106
|
+
default: afterDomain.defaultValue,
|
|
107
|
+
check: afterDomain.checkExpression,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
87
111
|
return changes;
|
|
88
112
|
}
|
|
89
113
|
function compareCompositeTypes(before, after) {
|
|
@@ -249,7 +249,7 @@ export async function introspectDatabase(connection, onProgress, options) {
|
|
|
249
249
|
if (includeFunctions) {
|
|
250
250
|
onProgress?.('fetching_functions');
|
|
251
251
|
const functionsResult = await pool.query(`
|
|
252
|
-
SELECT
|
|
252
|
+
SELECT
|
|
253
253
|
p.proname as name,
|
|
254
254
|
n.nspname as schema,
|
|
255
255
|
pg_get_function_result(p.oid) as return_type,
|
|
@@ -263,6 +263,17 @@ export async function introspectDatabase(connection, onProgress, options) {
|
|
|
263
263
|
JOIN pg_language l ON p.prolang = l.oid
|
|
264
264
|
WHERE n.nspname = 'public'
|
|
265
265
|
AND p.prokind IN ('f', 'a')
|
|
266
|
+
-- Exclude functions that belong to extensions (e.g., pgcrypto, uuid-ossp)
|
|
267
|
+
AND NOT EXISTS (
|
|
268
|
+
SELECT 1 FROM pg_depend d
|
|
269
|
+
JOIN pg_extension e ON d.refobjid = e.oid
|
|
270
|
+
WHERE d.objid = p.oid
|
|
271
|
+
AND d.deptype = 'e'
|
|
272
|
+
)
|
|
273
|
+
-- Exclude C language functions (typically extension functions)
|
|
274
|
+
AND l.lanname != 'c'
|
|
275
|
+
-- Exclude internal language functions
|
|
276
|
+
AND l.lanname != 'internal'
|
|
266
277
|
ORDER BY p.proname;
|
|
267
278
|
`);
|
|
268
279
|
functions = functionsResult.rows.map(f => ({
|
|
@@ -279,20 +290,24 @@ export async function introspectDatabase(connection, onProgress, options) {
|
|
|
279
290
|
let triggers = [];
|
|
280
291
|
if (includeTriggers) {
|
|
281
292
|
const triggersResult = await pool.query(`
|
|
282
|
-
SELECT
|
|
293
|
+
SELECT
|
|
283
294
|
t.tgname as name,
|
|
284
295
|
c.relname as table_name,
|
|
285
|
-
CASE
|
|
296
|
+
CASE
|
|
286
297
|
WHEN t.tgtype & 2 > 0 THEN 'BEFORE'
|
|
287
298
|
WHEN t.tgtype & 64 > 0 THEN 'INSTEAD OF'
|
|
288
299
|
ELSE 'AFTER'
|
|
289
300
|
END as timing,
|
|
290
|
-
CASE
|
|
301
|
+
CASE
|
|
291
302
|
WHEN t.tgtype & 4 > 0 THEN 'INSERT'
|
|
292
303
|
WHEN t.tgtype & 8 > 0 THEN 'DELETE'
|
|
293
304
|
WHEN t.tgtype & 16 > 0 THEN 'UPDATE'
|
|
294
305
|
ELSE 'UNKNOWN'
|
|
295
306
|
END as event,
|
|
307
|
+
CASE
|
|
308
|
+
WHEN t.tgtype & 1 > 0 THEN 'ROW'
|
|
309
|
+
ELSE 'STATEMENT'
|
|
310
|
+
END as for_each,
|
|
296
311
|
p.proname as function_name,
|
|
297
312
|
pg_get_triggerdef(t.oid) as definition,
|
|
298
313
|
t.tgenabled != 'D' as is_enabled
|
|
@@ -302,6 +317,11 @@ export async function introspectDatabase(connection, onProgress, options) {
|
|
|
302
317
|
JOIN pg_proc p ON t.tgfoid = p.oid
|
|
303
318
|
WHERE n.nspname = 'public'
|
|
304
319
|
AND NOT t.tgisinternal
|
|
320
|
+
-- Exclude triggers on partition child tables (only show on parent)
|
|
321
|
+
AND NOT EXISTS (
|
|
322
|
+
SELECT 1 FROM pg_inherits i
|
|
323
|
+
WHERE i.inhrelid = c.oid
|
|
324
|
+
)
|
|
305
325
|
ORDER BY c.relname, t.tgname;
|
|
306
326
|
`);
|
|
307
327
|
triggers = triggersResult.rows.map(t => ({
|
|
@@ -309,6 +329,7 @@ export async function introspectDatabase(connection, onProgress, options) {
|
|
|
309
329
|
tableName: t.table_name,
|
|
310
330
|
timing: t.timing,
|
|
311
331
|
event: t.event,
|
|
332
|
+
forEach: t.for_each,
|
|
312
333
|
functionName: t.function_name,
|
|
313
334
|
definition: t.definition || '',
|
|
314
335
|
isEnabled: t.is_enabled,
|
|
@@ -861,7 +861,9 @@ export function parseTriggers(sql) {
|
|
|
861
861
|
const tableMatch = body.match(/\bON\s+(\w+)/i);
|
|
862
862
|
const tableName = tableMatch ? tableMatch[1] : '';
|
|
863
863
|
const forEachMatch = body.match(/FOR\s+EACH\s+(ROW|STATEMENT)/i);
|
|
864
|
-
const forEach = forEachMatch
|
|
864
|
+
const forEach = forEachMatch
|
|
865
|
+
? forEachMatch[1].toUpperCase()
|
|
866
|
+
: 'ROW';
|
|
865
867
|
const whenMatch = body.match(/WHEN\s*\((.+?)\)\s*(?:EXECUTE|$)/is);
|
|
866
868
|
const condition = whenMatch ? whenMatch[1].trim() : undefined;
|
|
867
869
|
const refOldMatch = body.match(/REFERENCING\s+.*?OLD\s+TABLE\s+AS\s+(\w+)/i);
|
|
@@ -877,6 +879,7 @@ export function parseTriggers(sql) {
|
|
|
877
879
|
tableName,
|
|
878
880
|
timing,
|
|
879
881
|
event: events[0] || 'INSERT',
|
|
882
|
+
forEach,
|
|
880
883
|
functionName,
|
|
881
884
|
definition: match[0],
|
|
882
885
|
isEnabled: true,
|
|
@@ -191,5 +191,8 @@ export function validateConfig(config) {
|
|
|
191
191
|
errors.push('Pool max must be at least 1');
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
+
if (config.includeTriggers && !config.includeFunctions) {
|
|
195
|
+
errors.push('Triggers require functions to be enabled. Set includeFunctions: true in your config.');
|
|
196
|
+
}
|
|
194
197
|
return { valid: errors.length === 0, errors };
|
|
195
198
|
}
|
|
@@ -20,7 +20,7 @@ function getFunctionName(func) {
|
|
|
20
20
|
}
|
|
21
21
|
export function generateTriggerSQL(config) {
|
|
22
22
|
const { $triggerName, $options } = config;
|
|
23
|
-
const { on, before, after, insteadOf, updateOf,
|
|
23
|
+
const { on, before, after, insteadOf, updateOf, forEach = 'ROW', when, referencing, constraint, deferrable, initially, execute, executeArgs, } = $options;
|
|
24
24
|
const parts = ['CREATE'];
|
|
25
25
|
if (constraint) {
|
|
26
26
|
parts.push('CONSTRAINT');
|
|
@@ -68,10 +68,10 @@ export function generateTriggerSQL(config) {
|
|
|
68
68
|
}
|
|
69
69
|
parts.push(refParts.join(' '));
|
|
70
70
|
}
|
|
71
|
-
if (
|
|
71
|
+
if (forEach === 'STATEMENT') {
|
|
72
72
|
parts.push('\n FOR EACH STATEMENT');
|
|
73
73
|
}
|
|
74
|
-
else
|
|
74
|
+
else {
|
|
75
75
|
parts.push('\n FOR EACH ROW');
|
|
76
76
|
}
|
|
77
77
|
if (when) {
|
|
@@ -113,7 +113,7 @@ export function pgTrigger(name, options) {
|
|
|
113
113
|
timing = 'BEFORE';
|
|
114
114
|
events = ['INSERT'];
|
|
115
115
|
}
|
|
116
|
-
const
|
|
116
|
+
const forEachLevel = opts.forEach || 'ROW';
|
|
117
117
|
const functionName = typeof opts.execute === 'string'
|
|
118
118
|
? opts.execute
|
|
119
119
|
: opts.execute.$functionName;
|
|
@@ -122,7 +122,7 @@ export function pgTrigger(name, options) {
|
|
|
122
122
|
table: getTableName(opts.on),
|
|
123
123
|
timing,
|
|
124
124
|
events,
|
|
125
|
-
forEach,
|
|
125
|
+
forEach: forEachLevel,
|
|
126
126
|
functionName,
|
|
127
127
|
whenClause: opts.when,
|
|
128
128
|
isConstraint: opts.constraint ?? false,
|
|
@@ -131,6 +131,10 @@ export function pgTrigger(name, options) {
|
|
|
131
131
|
trackingId: this.$trackingId,
|
|
132
132
|
};
|
|
133
133
|
},
|
|
134
|
+
$id(trackingId) {
|
|
135
|
+
this.$trackingId = trackingId;
|
|
136
|
+
return this;
|
|
137
|
+
},
|
|
134
138
|
};
|
|
135
139
|
}
|
|
136
140
|
export function isTriggerConfig(value) {
|
package/dist/schema-builder.d.ts
CHANGED
|
@@ -3258,6 +3258,10 @@ export declare function addEnumValueSQL(enumName: string, newValue: string, posi
|
|
|
3258
3258
|
export type FunctionVolatility = "VOLATILE" | "STABLE" | "IMMUTABLE";
|
|
3259
3259
|
export type FunctionParallel = "UNSAFE" | "RESTRICTED" | "SAFE";
|
|
3260
3260
|
export type FunctionSecurity = "INVOKER" | "DEFINER";
|
|
3261
|
+
/** Common PostgreSQL function languages */
|
|
3262
|
+
export type FunctionLanguage = "plpgsql" | "sql" | "plpython3u" | "plperl" | "pltcl" | (string & {});
|
|
3263
|
+
/** Common PostgreSQL return types with autocomplete support */
|
|
3264
|
+
export type FunctionReturnType = "trigger" | "void" | "integer" | "bigint" | "smallint" | "numeric" | "real" | "double precision" | "text" | "varchar" | "char" | "name" | "boolean" | "timestamp" | "timestamptz" | "timestamp with time zone" | "timestamp without time zone" | "date" | "time" | "timetz" | "interval" | "bytea" | "uuid" | "json" | "jsonb" | "text[]" | "integer[]" | "uuid[]" | "jsonb[]" | "record" | "SETOF record" | (string & {});
|
|
3261
3265
|
export interface FunctionArgument {
|
|
3262
3266
|
/** Argument name */
|
|
3263
3267
|
name: string;
|
|
@@ -3271,10 +3275,10 @@ export interface FunctionArgument {
|
|
|
3271
3275
|
export interface FunctionOptions {
|
|
3272
3276
|
/** Function arguments */
|
|
3273
3277
|
args?: FunctionArgument[];
|
|
3274
|
-
/** Return type (e.g., '
|
|
3275
|
-
returns:
|
|
3278
|
+
/** Return type (e.g., 'integer', 'trigger', 'SETOF uuid', 'TABLE(...)') */
|
|
3279
|
+
returns: FunctionReturnType;
|
|
3276
3280
|
/** Language (default: 'plpgsql') */
|
|
3277
|
-
language?:
|
|
3281
|
+
language?: FunctionLanguage;
|
|
3278
3282
|
/** Volatility category */
|
|
3279
3283
|
volatility?: FunctionVolatility;
|
|
3280
3284
|
/** Parallel safety */
|
|
@@ -3313,6 +3317,8 @@ export interface FunctionConfig {
|
|
|
3313
3317
|
$trackingId?: string;
|
|
3314
3318
|
/** Returns AST for schema diffing and migration generation */
|
|
3315
3319
|
toAST(): ParsedFunction;
|
|
3320
|
+
/** Set tracking ID for rename detection */
|
|
3321
|
+
$id(trackingId: string): FunctionConfig;
|
|
3316
3322
|
}
|
|
3317
3323
|
/**
|
|
3318
3324
|
* Generate SQL for a function definition
|
|
@@ -3408,13 +3414,9 @@ export interface TriggerOptions<TTable = unknown> {
|
|
|
3408
3414
|
*/
|
|
3409
3415
|
updateOf?: string[];
|
|
3410
3416
|
/**
|
|
3411
|
-
* FOR EACH ROW (default:
|
|
3412
|
-
*/
|
|
3413
|
-
forEachRow?: boolean;
|
|
3414
|
-
/**
|
|
3415
|
-
* FOR EACH STATEMENT
|
|
3417
|
+
* FOR EACH ROW or FOR EACH STATEMENT (default: 'ROW')
|
|
3416
3418
|
*/
|
|
3417
|
-
|
|
3419
|
+
forEach?: TriggerLevel;
|
|
3418
3420
|
/**
|
|
3419
3421
|
* WHEN condition (SQL expression)
|
|
3420
3422
|
* Can reference OLD and NEW
|
|
@@ -3459,6 +3461,8 @@ export interface TriggerConfig<TTable = unknown> {
|
|
|
3459
3461
|
$trackingId?: string;
|
|
3460
3462
|
/** Returns AST for schema diffing and migration generation */
|
|
3461
3463
|
toAST(): ParsedTrigger;
|
|
3464
|
+
/** Set tracking ID for rename detection */
|
|
3465
|
+
$id(trackingId: string): TriggerConfig<TTable>;
|
|
3462
3466
|
}
|
|
3463
3467
|
/**
|
|
3464
3468
|
* Generate SQL for a trigger definition
|
|
@@ -3477,7 +3481,7 @@ export declare function dropTriggerSQL(config: TriggerConfig, ifExists?: boolean
|
|
|
3477
3481
|
* export const trgUpdateTimestamp = pgTrigger('trg_update_timestamp', {
|
|
3478
3482
|
* on: usersTable,
|
|
3479
3483
|
* before: 'UPDATE',
|
|
3480
|
-
*
|
|
3484
|
+
* forEach: 'ROW',
|
|
3481
3485
|
* execute: updateTimestampFunc
|
|
3482
3486
|
* });
|
|
3483
3487
|
*
|
|
@@ -3485,7 +3489,7 @@ export declare function dropTriggerSQL(config: TriggerConfig, ifExists?: boolean
|
|
|
3485
3489
|
* export const trgAudit = pgTrigger('trg_audit', {
|
|
3486
3490
|
* on: ordersTable,
|
|
3487
3491
|
* after: ['INSERT', 'UPDATE', 'DELETE'],
|
|
3488
|
-
*
|
|
3492
|
+
* forEach: 'ROW',
|
|
3489
3493
|
* execute: auditChangesFunc
|
|
3490
3494
|
* });
|
|
3491
3495
|
*
|
|
@@ -3494,7 +3498,7 @@ export declare function dropTriggerSQL(config: TriggerConfig, ifExists?: boolean
|
|
|
3494
3498
|
* on: usersTable,
|
|
3495
3499
|
* after: 'UPDATE',
|
|
3496
3500
|
* updateOf: ['status'],
|
|
3497
|
-
*
|
|
3501
|
+
* forEach: 'ROW',
|
|
3498
3502
|
* when: 'OLD.status IS DISTINCT FROM NEW.status',
|
|
3499
3503
|
* execute: notifyStatusChangeFunc
|
|
3500
3504
|
* });
|
|
@@ -3503,7 +3507,7 @@ export declare function dropTriggerSQL(config: TriggerConfig, ifExists?: boolean
|
|
|
3503
3507
|
* export const trgBulkUpdate = pgTrigger('trg_bulk_update', {
|
|
3504
3508
|
* on: productsTable,
|
|
3505
3509
|
* after: 'UPDATE',
|
|
3506
|
-
*
|
|
3510
|
+
* forEach: 'STATEMENT',
|
|
3507
3511
|
* referencing: {
|
|
3508
3512
|
* oldTable: 'old_products',
|
|
3509
3513
|
* newTable: 'new_products'
|
|
@@ -3515,7 +3519,7 @@ export declare function dropTriggerSQL(config: TriggerConfig, ifExists?: boolean
|
|
|
3515
3519
|
* export const trgViewInsert = pgTrigger('trg_view_insert', {
|
|
3516
3520
|
* on: 'users_view',
|
|
3517
3521
|
* insteadOf: 'INSERT',
|
|
3518
|
-
*
|
|
3522
|
+
* forEach: 'ROW',
|
|
3519
3523
|
* execute: handleViewInsertFunc
|
|
3520
3524
|
* });
|
|
3521
3525
|
*
|
|
@@ -3526,7 +3530,7 @@ export declare function dropTriggerSQL(config: TriggerConfig, ifExists?: boolean
|
|
|
3526
3530
|
* constraint: true,
|
|
3527
3531
|
* deferrable: true,
|
|
3528
3532
|
* initially: 'DEFERRED',
|
|
3529
|
-
*
|
|
3533
|
+
* forEach: 'ROW',
|
|
3530
3534
|
* execute: validateOrderFunc
|
|
3531
3535
|
* });
|
|
3532
3536
|
* ```
|
|
@@ -3696,8 +3700,8 @@ export declare namespace sql {
|
|
|
3696
3700
|
__raw: string;
|
|
3697
3701
|
};
|
|
3698
3702
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3703
|
+
type FunctionReturnType$1 = "trigger" | "void" | "integer" | "bigint" | "smallint" | "real" | "double precision" | "numeric" | "text" | "varchar" | "char" | "boolean" | "json" | "jsonb" | "uuid" | "timestamp" | "timestamptz" | "date" | "time" | "timetz" | "interval" | "bytea" | "record" | "setof record" | (string & {});
|
|
3704
|
+
type FunctionLanguage$1 = "plpgsql" | "sql" | "plv8" | "plpython3u" | "plperl" | (string & {});
|
|
3701
3705
|
export type FunctionType = "function" | "procedure";
|
|
3702
3706
|
type FunctionVolatility$1 = "volatile" | "stable" | "immutable";
|
|
3703
3707
|
type FunctionSecurity$1 = "invoker" | "definer";
|
|
@@ -3711,9 +3715,9 @@ interface FunctionOptions$1 {
|
|
|
3711
3715
|
/** Function name */
|
|
3712
3716
|
name: string;
|
|
3713
3717
|
/** Return type with autocomplete */
|
|
3714
|
-
returns: FunctionReturnType;
|
|
3718
|
+
returns: FunctionReturnType$1;
|
|
3715
3719
|
/** Language with autocomplete */
|
|
3716
|
-
language: FunctionLanguage;
|
|
3720
|
+
language: FunctionLanguage$1;
|
|
3717
3721
|
/** Function or procedure */
|
|
3718
3722
|
as?: FunctionType;
|
|
3719
3723
|
/** Function parameters */
|
|
@@ -3754,7 +3758,9 @@ export interface FunctionDefinition {
|
|
|
3754
3758
|
export declare function createFunction(options: FunctionOptions$1): FunctionDefinition;
|
|
3755
3759
|
|
|
3756
3760
|
export {
|
|
3761
|
+
FunctionLanguage$1 as FunctionLanguage,
|
|
3757
3762
|
FunctionOptions$1 as FunctionOptions,
|
|
3763
|
+
FunctionReturnType$1 as FunctionReturnType,
|
|
3758
3764
|
FunctionSecurity$1 as FunctionSecurity,
|
|
3759
3765
|
FunctionVolatility$1 as FunctionVolatility,
|
|
3760
3766
|
ParsedColumn$1 as ParsedColumn,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relq",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "The Fully-Typed PostgreSQL ORM for TypeScript",
|
|
5
5
|
"author": "Olajide Mathew O. <olajide.mathew@yuniq.solutions>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,5 +59,8 @@
|
|
|
59
59
|
"pgsql-deparser": "^17.17.0",
|
|
60
60
|
"pgsql-parser": "^17.9.9",
|
|
61
61
|
"strip-comments": "^2.0.1"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"sql-formatter": "^15.6.12"
|
|
62
65
|
}
|
|
63
66
|
}
|