relq 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli/commands/add.cjs +252 -12
- package/dist/cjs/cli/commands/commit.cjs +12 -1
- package/dist/cjs/cli/commands/export.cjs +25 -19
- package/dist/cjs/cli/commands/import.cjs +219 -100
- package/dist/cjs/cli/commands/init.cjs +86 -14
- package/dist/cjs/cli/commands/pull.cjs +104 -23
- package/dist/cjs/cli/commands/push.cjs +38 -3
- package/dist/cjs/cli/index.cjs +9 -1
- package/dist/cjs/cli/utils/ast/codegen/builder.cjs +297 -0
- package/dist/cjs/cli/utils/ast/codegen/constraints.cjs +185 -0
- package/dist/cjs/cli/utils/ast/codegen/defaults.cjs +311 -0
- package/dist/cjs/cli/utils/ast/codegen/index.cjs +24 -0
- package/dist/cjs/cli/utils/ast/codegen/type-map.cjs +116 -0
- package/dist/cjs/cli/utils/ast/codegen/utils.cjs +69 -0
- package/dist/cjs/cli/utils/ast/index.cjs +19 -0
- package/dist/cjs/cli/utils/ast/transformer/helpers.cjs +154 -0
- package/dist/cjs/cli/utils/ast/transformer/index.cjs +25 -0
- package/dist/cjs/cli/utils/ast/types.cjs +2 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +949 -0
- package/dist/cjs/cli/utils/ast-transformer.cjs +916 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +50 -1
- package/dist/cjs/cli/utils/cli-utils.cjs +151 -0
- package/dist/cjs/cli/utils/fast-introspect.cjs +149 -23
- package/dist/cjs/cli/utils/pg-parser.cjs +1 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +121 -4
- package/dist/cjs/cli/utils/schema-comparator.cjs +98 -14
- package/dist/cjs/cli/utils/schema-introspect.cjs +56 -19
- package/dist/cjs/cli/utils/snapshot-manager.cjs +0 -1
- package/dist/cjs/cli/utils/sql-generator.cjs +353 -64
- package/dist/cjs/cli/utils/type-generator.cjs +114 -15
- package/dist/cjs/core/relq-client.cjs +22 -6
- package/dist/cjs/schema-definition/column-types.cjs +149 -13
- package/dist/cjs/schema-definition/defaults.cjs +72 -0
- package/dist/cjs/schema-definition/index.cjs +15 -1
- package/dist/cjs/schema-definition/introspection.cjs +7 -3
- package/dist/cjs/schema-definition/pg-relations.cjs +169 -0
- package/dist/cjs/schema-definition/pg-view.cjs +30 -0
- package/dist/cjs/schema-definition/table-definition.cjs +110 -4
- package/dist/cjs/types/config-types.cjs +13 -4
- package/dist/cjs/utils/aws-dsql.cjs +177 -0
- package/dist/config.d.ts +146 -1
- package/dist/esm/cli/commands/add.js +250 -13
- package/dist/esm/cli/commands/commit.js +12 -1
- package/dist/esm/cli/commands/export.js +25 -19
- package/dist/esm/cli/commands/import.js +221 -102
- package/dist/esm/cli/commands/init.js +86 -14
- package/dist/esm/cli/commands/pull.js +106 -25
- package/dist/esm/cli/commands/push.js +39 -4
- package/dist/esm/cli/index.js +9 -1
- package/dist/esm/cli/utils/ast/codegen/builder.js +291 -0
- package/dist/esm/cli/utils/ast/codegen/constraints.js +176 -0
- package/dist/esm/cli/utils/ast/codegen/defaults.js +305 -0
- package/dist/esm/cli/utils/ast/codegen/index.js +6 -0
- package/dist/esm/cli/utils/ast/codegen/type-map.js +111 -0
- package/dist/esm/cli/utils/ast/codegen/utils.js +60 -0
- package/dist/esm/cli/utils/ast/index.js +3 -0
- package/dist/esm/cli/utils/ast/transformer/helpers.js +141 -0
- package/dist/esm/cli/utils/ast/transformer/index.js +2 -0
- package/dist/esm/cli/utils/ast/types.js +1 -0
- package/dist/esm/cli/utils/ast-codegen.js +945 -0
- package/dist/esm/cli/utils/ast-transformer.js +907 -0
- package/dist/esm/cli/utils/change-tracker.js +50 -1
- package/dist/esm/cli/utils/cli-utils.js +147 -0
- package/dist/esm/cli/utils/fast-introspect.js +149 -23
- package/dist/esm/cli/utils/pg-parser.js +1 -0
- package/dist/esm/cli/utils/repo-manager.js +114 -4
- package/dist/esm/cli/utils/schema-comparator.js +98 -14
- package/dist/esm/cli/utils/schema-introspect.js +56 -19
- package/dist/esm/cli/utils/snapshot-manager.js +0 -1
- package/dist/esm/cli/utils/sql-generator.js +353 -64
- package/dist/esm/cli/utils/type-generator.js +114 -15
- package/dist/esm/core/relq-client.js +23 -7
- package/dist/esm/schema-definition/column-types.js +146 -12
- package/dist/esm/schema-definition/defaults.js +69 -0
- package/dist/esm/schema-definition/index.js +3 -0
- package/dist/esm/schema-definition/introspection.js +7 -3
- package/dist/esm/schema-definition/pg-relations.js +161 -0
- package/dist/esm/schema-definition/pg-view.js +24 -0
- package/dist/esm/schema-definition/table-definition.js +110 -4
- package/dist/esm/types/config-types.js +12 -4
- package/dist/esm/utils/aws-dsql.js +139 -0
- package/dist/index.d.ts +159 -1
- package/dist/schema-builder.d.ts +1314 -32
- package/package.json +1 -1
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
import { toCamelCase, escapeString, isBalanced, } from "./ast/codegen/utils.js";
|
|
2
|
+
import { astToBuilder } from "./ast/codegen/builder.js";
|
|
3
|
+
import { getColumnBuilder, getColumnBuilderWithInfo } from "./ast/codegen/type-map.js";
|
|
4
|
+
import { formatDefaultValue, resetDefaultImportFlags, getDefaultImportNeeded, getDefaultSqlImportNeeded, } from "./ast/codegen/defaults.js";
|
|
5
|
+
import { extractEnumValues, generateConstraintCode, generateCheckConstraintsOption, resetSqlImportFlag, getSqlImportNeeded, } from "./ast/codegen/constraints.js";
|
|
6
|
+
let needsDefaultImport = false;
|
|
7
|
+
let needsSqlImport = false;
|
|
8
|
+
let trackingIdCounter = 0;
|
|
9
|
+
function generateTrackingId(prefix) {
|
|
10
|
+
trackingIdCounter++;
|
|
11
|
+
const base = (Date.now().toString(36) + trackingIdCounter.toString(36)).slice(-5);
|
|
12
|
+
return prefix + base.padStart(5, '0');
|
|
13
|
+
}
|
|
14
|
+
export function resetTrackingIdCounter() {
|
|
15
|
+
trackingIdCounter = 0;
|
|
16
|
+
}
|
|
17
|
+
function getExplicitFKName(constraintName, tableName, columnName) {
|
|
18
|
+
if (!constraintName)
|
|
19
|
+
return undefined;
|
|
20
|
+
const expectedAutoName = `${tableName}_${columnName}_fkey`;
|
|
21
|
+
if (constraintName.toLowerCase() === expectedAutoName.toLowerCase()) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return constraintName;
|
|
25
|
+
}
|
|
26
|
+
function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOverride) {
|
|
27
|
+
const colName = useCamelCase ? toCamelCase(col.name) : col.name;
|
|
28
|
+
const commentSuffix = col.comment ? `.comment('${escapeString(col.comment)}')` : '';
|
|
29
|
+
let line;
|
|
30
|
+
const normalizedType = col.type.toLowerCase();
|
|
31
|
+
if (enumNames.has(normalizedType) || enumNames.has(col.type)) {
|
|
32
|
+
const enumName = `${toCamelCase(col.type)}Enum`;
|
|
33
|
+
line = ` ${colName}: enumType(${enumName})`;
|
|
34
|
+
}
|
|
35
|
+
else if (domainNames.has(normalizedType) || domainNames.has(col.type)) {
|
|
36
|
+
const domainName = `${toCamelCase(col.type)}Domain`;
|
|
37
|
+
line = ` ${colName}: domainType(${domainName})`;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
let typeBuilder;
|
|
41
|
+
if (useCamelCase && colName !== col.name) {
|
|
42
|
+
const builderInfo = getColumnBuilderWithInfo(col.type, col.typeParams);
|
|
43
|
+
typeBuilder = `${builderInfo.builderName}('${col.name}')`;
|
|
44
|
+
if (builderInfo.length != null) {
|
|
45
|
+
typeBuilder += `.length(${builderInfo.length})`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
typeBuilder = getColumnBuilder(col.type, col.typeParams);
|
|
50
|
+
}
|
|
51
|
+
if (col.isArray) {
|
|
52
|
+
typeBuilder = `${typeBuilder}.array()`;
|
|
53
|
+
}
|
|
54
|
+
line = ` ${colName}: ${typeBuilder}`;
|
|
55
|
+
}
|
|
56
|
+
if (col.isPrimaryKey) {
|
|
57
|
+
line += '.primaryKey()';
|
|
58
|
+
}
|
|
59
|
+
if (col.isUnique && !col.isPrimaryKey) {
|
|
60
|
+
line += '.unique()';
|
|
61
|
+
}
|
|
62
|
+
if (checkOverride) {
|
|
63
|
+
const valuesStr = checkOverride.values.map(v => `'${escapeString(v)}'`).join(', ');
|
|
64
|
+
line += `.check('${checkOverride.name}', [${valuesStr}])`;
|
|
65
|
+
}
|
|
66
|
+
else if (col.checkConstraint) {
|
|
67
|
+
const enumValues = extractEnumValues(col.checkConstraint.expression);
|
|
68
|
+
if (enumValues && enumValues.length > 0) {
|
|
69
|
+
const valuesStr = enumValues.map(v => `'${escapeString(v)}'`).join(', ');
|
|
70
|
+
line += `.check('${col.checkConstraint.name}', [${valuesStr}])`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (col.hasDefault && col.defaultValue) {
|
|
74
|
+
const defaultVal = formatDefaultValue(col.defaultValue, col.type);
|
|
75
|
+
line += `.default(${defaultVal})`;
|
|
76
|
+
}
|
|
77
|
+
if (col.isGenerated && col.generatedExpression) {
|
|
78
|
+
const escapedExpr = col.generatedExpression.replace(/`/g, '\\`');
|
|
79
|
+
line += `.generatedAlwaysAs(F => F.raw(\`${escapedExpr}\`))`;
|
|
80
|
+
}
|
|
81
|
+
if (!col.isNullable && !col.isPrimaryKey) {
|
|
82
|
+
line += '.notNull()';
|
|
83
|
+
}
|
|
84
|
+
const trackingId = generateTrackingId('c');
|
|
85
|
+
line += `.$id('${trackingId}')`;
|
|
86
|
+
return line + commentSuffix;
|
|
87
|
+
}
|
|
88
|
+
function generateIndexCode(index, useCamelCase) {
|
|
89
|
+
const cols = index.columns.map(c => {
|
|
90
|
+
if (index.isExpression || c.includes('(') || c.includes(' ')) {
|
|
91
|
+
return `'${c}'`;
|
|
92
|
+
}
|
|
93
|
+
return `table.${useCamelCase ? toCamelCase(c) : c}`;
|
|
94
|
+
}).join(', ');
|
|
95
|
+
let line = ` index('${index.name}').on(${cols})`;
|
|
96
|
+
if (index.isUnique) {
|
|
97
|
+
line += '.unique()';
|
|
98
|
+
}
|
|
99
|
+
if (index.method && index.method.toUpperCase() !== 'BTREE') {
|
|
100
|
+
line += `.using('${index.method.toUpperCase()}')`;
|
|
101
|
+
}
|
|
102
|
+
if (index.opclass) {
|
|
103
|
+
line += `.opclass('${index.opclass}')`;
|
|
104
|
+
}
|
|
105
|
+
if (index.whereClauseAst) {
|
|
106
|
+
try {
|
|
107
|
+
const whereExpr = astToBuilder(index.whereClauseAst, {
|
|
108
|
+
prefix: 'table',
|
|
109
|
+
useCamelCase,
|
|
110
|
+
useTableRef: true
|
|
111
|
+
});
|
|
112
|
+
line += `.where(${whereExpr})`;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
needsSqlImport = true;
|
|
116
|
+
line += `.where(sql\`${escapeString(index.whereClause || '')}\`)`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (index.whereClause) {
|
|
120
|
+
const typedWhere = convertWhereClauseToTyped(index.whereClause, useCamelCase);
|
|
121
|
+
if (typedWhere.startsWith('sql`')) {
|
|
122
|
+
needsSqlImport = true;
|
|
123
|
+
}
|
|
124
|
+
line += `.where(${typedWhere})`;
|
|
125
|
+
}
|
|
126
|
+
if (index.includeColumns && index.includeColumns.length > 0) {
|
|
127
|
+
const includeCols = index.includeColumns.map(c => `table.${useCamelCase ? toCamelCase(c) : c}`).join(', ');
|
|
128
|
+
line += `.include(${includeCols})`;
|
|
129
|
+
}
|
|
130
|
+
const trackingId = generateTrackingId('i');
|
|
131
|
+
line += `.$id('${trackingId}')`;
|
|
132
|
+
if (index.comment) {
|
|
133
|
+
line += `.comment('${escapeString(index.comment)}')`;
|
|
134
|
+
}
|
|
135
|
+
return line;
|
|
136
|
+
}
|
|
137
|
+
function convertWhereClauseToTyped(whereClause, useCamelCase) {
|
|
138
|
+
const trimmed = whereClause.trim();
|
|
139
|
+
let expr = trimmed;
|
|
140
|
+
while (expr.startsWith('(') && expr.endsWith(')')) {
|
|
141
|
+
const inner = expr.slice(1, -1);
|
|
142
|
+
if (isBalanced(inner)) {
|
|
143
|
+
expr = inner;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const boolMatch = expr.match(/^(\w+)\s*=\s*(true|false)$/i);
|
|
150
|
+
if (boolMatch) {
|
|
151
|
+
const colName = useCamelCase ? toCamelCase(boolMatch[1]) : boolMatch[1];
|
|
152
|
+
const value = boolMatch[2].toLowerCase();
|
|
153
|
+
return `table.${colName}.eq(${value})`;
|
|
154
|
+
}
|
|
155
|
+
const numMatch = expr.match(/^(\w+)\s*=\s*(-?\d+(?:\.\d+)?)$/i);
|
|
156
|
+
if (numMatch) {
|
|
157
|
+
const colName = useCamelCase ? toCamelCase(numMatch[1]) : numMatch[1];
|
|
158
|
+
return `table.${colName}.eq(${numMatch[2]})`;
|
|
159
|
+
}
|
|
160
|
+
const strMatch = expr.match(/^(\w+)\s*=\s*'([^']*)'$/i);
|
|
161
|
+
if (strMatch) {
|
|
162
|
+
const colName = useCamelCase ? toCamelCase(strMatch[1]) : strMatch[1];
|
|
163
|
+
return `table.${colName}.eq('${strMatch[2]}')`;
|
|
164
|
+
}
|
|
165
|
+
const isNullMatch = expr.match(/^(\w+)\s+IS\s+NULL$/i);
|
|
166
|
+
if (isNullMatch) {
|
|
167
|
+
const colName = useCamelCase ? toCamelCase(isNullMatch[1]) : isNullMatch[1];
|
|
168
|
+
return `table.${colName}.isNull()`;
|
|
169
|
+
}
|
|
170
|
+
const isNotNullMatch = expr.match(/^(\w+)\s+IS\s+NOT\s+NULL$/i);
|
|
171
|
+
if (isNotNullMatch) {
|
|
172
|
+
const colName = useCamelCase ? toCamelCase(isNotNullMatch[1]) : isNotNullMatch[1];
|
|
173
|
+
return `table.${colName}.isNotNull()`;
|
|
174
|
+
}
|
|
175
|
+
const andMatch = expr.match(/^\((.+?)\)\s+AND\s+\((.+)\)$/i);
|
|
176
|
+
if (andMatch) {
|
|
177
|
+
const typed1 = convertWhereClauseToTyped(`(${andMatch[1]})`, useCamelCase);
|
|
178
|
+
const typed2 = convertWhereClauseToTyped(`(${andMatch[2]})`, useCamelCase);
|
|
179
|
+
if (!typed1.startsWith('sql`') && !typed2.startsWith('sql`')) {
|
|
180
|
+
return `${typed1}.and(${typed2})`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const orMatch = expr.match(/^\((.+?)\)\s+OR\s+\((.+)\)$/i);
|
|
184
|
+
if (orMatch) {
|
|
185
|
+
const typed1 = convertWhereClauseToTyped(`(${orMatch[1]})`, useCamelCase);
|
|
186
|
+
const typed2 = convertWhereClauseToTyped(`(${orMatch[2]})`, useCamelCase);
|
|
187
|
+
if (!typed1.startsWith('sql`') && !typed2.startsWith('sql`')) {
|
|
188
|
+
return `${typed1}.or(${typed2})`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return `sql\`${escapeString(whereClause)}\``;
|
|
192
|
+
}
|
|
193
|
+
function parsePartitionValues(valuesStr) {
|
|
194
|
+
const values = [];
|
|
195
|
+
let current = '';
|
|
196
|
+
let depth = 0;
|
|
197
|
+
let inString = false;
|
|
198
|
+
let stringChar = '';
|
|
199
|
+
for (let i = 0; i < valuesStr.length; i++) {
|
|
200
|
+
const char = valuesStr[i];
|
|
201
|
+
if (!inString && (char === "'" || char === '"')) {
|
|
202
|
+
inString = true;
|
|
203
|
+
stringChar = char;
|
|
204
|
+
current += char;
|
|
205
|
+
}
|
|
206
|
+
else if (inString && char === stringChar) {
|
|
207
|
+
if (i + 1 < valuesStr.length && valuesStr[i + 1] === stringChar) {
|
|
208
|
+
current += char + valuesStr[i + 1];
|
|
209
|
+
i++;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
inString = false;
|
|
213
|
+
current += char;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (!inString && char === '(') {
|
|
217
|
+
depth++;
|
|
218
|
+
current += char;
|
|
219
|
+
}
|
|
220
|
+
else if (!inString && char === ')') {
|
|
221
|
+
depth--;
|
|
222
|
+
current += char;
|
|
223
|
+
}
|
|
224
|
+
else if (!inString && char === ',' && depth === 0) {
|
|
225
|
+
if (current.trim()) {
|
|
226
|
+
values.push(current.trim());
|
|
227
|
+
}
|
|
228
|
+
current = '';
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
current += char;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (current.trim()) {
|
|
235
|
+
values.push(current.trim());
|
|
236
|
+
}
|
|
237
|
+
return values;
|
|
238
|
+
}
|
|
239
|
+
function formatPartitionValue(value) {
|
|
240
|
+
const trimmed = value.trim();
|
|
241
|
+
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
|
|
242
|
+
const inner = trimmed.slice(1, -1);
|
|
243
|
+
const parts = parsePartitionValues(inner);
|
|
244
|
+
return `[${parts.join(', ')}]`;
|
|
245
|
+
}
|
|
246
|
+
const upper = trimmed.toUpperCase();
|
|
247
|
+
if (upper === 'MINVALUE')
|
|
248
|
+
return "'MINVALUE'";
|
|
249
|
+
if (upper === 'MAXVALUE')
|
|
250
|
+
return "'MAXVALUE'";
|
|
251
|
+
return trimmed;
|
|
252
|
+
}
|
|
253
|
+
function generatePartitionCode(child, partitionType, useCamelCase) {
|
|
254
|
+
const childName = child.name;
|
|
255
|
+
const bound = child.partitionBound || '';
|
|
256
|
+
const partType = partitionType.toUpperCase();
|
|
257
|
+
if (bound.toUpperCase().includes('DEFAULT')) {
|
|
258
|
+
return ` partition('${childName}').default()`;
|
|
259
|
+
}
|
|
260
|
+
if (partType === 'LIST') {
|
|
261
|
+
const inMatch = bound.match(/FOR VALUES IN\s*\((.+)\)/i);
|
|
262
|
+
if (inMatch) {
|
|
263
|
+
const valuesStr = inMatch[1].trim();
|
|
264
|
+
const values = parsePartitionValues(valuesStr);
|
|
265
|
+
const formattedValues = values.map(formatPartitionValue).join(', ');
|
|
266
|
+
return ` partition('${childName}').in([${formattedValues}])`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (partType === 'RANGE') {
|
|
270
|
+
const rangeMatch = bound.match(/FOR VALUES FROM\s*\((.+?)\)\s*TO\s*\((.+?)\)$/i);
|
|
271
|
+
if (rangeMatch) {
|
|
272
|
+
const fromRaw = rangeMatch[1].trim();
|
|
273
|
+
const toRaw = rangeMatch[2].trim();
|
|
274
|
+
let fromVal;
|
|
275
|
+
if (fromRaw.toUpperCase() === 'MINVALUE') {
|
|
276
|
+
fromVal = "'MINVALUE'";
|
|
277
|
+
}
|
|
278
|
+
else if (fromRaw.includes(',')) {
|
|
279
|
+
const fromParts = parsePartitionValues(fromRaw);
|
|
280
|
+
fromVal = `[${fromParts.map(formatPartitionValue).join(', ')}]`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
fromVal = formatPartitionValue(fromRaw);
|
|
284
|
+
}
|
|
285
|
+
let toVal;
|
|
286
|
+
if (toRaw.toUpperCase() === 'MAXVALUE') {
|
|
287
|
+
toVal = "'MAXVALUE'";
|
|
288
|
+
}
|
|
289
|
+
else if (toRaw.includes(',')) {
|
|
290
|
+
const toParts = parsePartitionValues(toRaw);
|
|
291
|
+
toVal = `[${toParts.map(formatPartitionValue).join(', ')}]`;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
toVal = formatPartitionValue(toRaw);
|
|
295
|
+
}
|
|
296
|
+
return ` partition('${childName}').from(${fromVal}).to(${toVal})`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (partType === 'HASH') {
|
|
300
|
+
const hashMatch = bound.match(/FOR VALUES WITH\s*\(MODULUS\s*(\d+),\s*REMAINDER\s*(\d+)\)/i);
|
|
301
|
+
if (hashMatch) {
|
|
302
|
+
const remainder = hashMatch[2];
|
|
303
|
+
return ` partition('${childName}').remainder(${remainder})`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
needsSqlImport = true;
|
|
307
|
+
return ` partition('${childName}').in([sql\`${escapeString(bound)}\`])`;
|
|
308
|
+
}
|
|
309
|
+
function generateTableCode(table, useCamelCase, enumNames, domainNames) {
|
|
310
|
+
const tableName = useCamelCase ? toCamelCase(table.name) : table.name;
|
|
311
|
+
const parts = [];
|
|
312
|
+
const tableComment = table.comment ? ` comment: '${escapeString(table.comment)}'` : null;
|
|
313
|
+
const columnChecks = new Map();
|
|
314
|
+
for (const c of table.constraints) {
|
|
315
|
+
if (c.type === 'CHECK' && c.expression) {
|
|
316
|
+
for (const col of table.columns) {
|
|
317
|
+
const colPattern = new RegExp(`\\(${col.name}\\)|\\b${col.name}\\b`, 'i');
|
|
318
|
+
if (colPattern.test(c.expression)) {
|
|
319
|
+
const values = extractEnumValues(c.expression);
|
|
320
|
+
if (values && values.length > 0) {
|
|
321
|
+
columnChecks.set(col.name, { name: c.name, values });
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const singleColPkColumns = new Set();
|
|
329
|
+
for (const c of table.constraints) {
|
|
330
|
+
if (c.type === 'PRIMARY KEY' && c.columns.length === 1) {
|
|
331
|
+
const normalizedCol = useCamelCase ? toCamelCase(c.columns[0]) : c.columns[0];
|
|
332
|
+
singleColPkColumns.add(normalizedCol);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const compositeColPkColumns = new Set();
|
|
336
|
+
for (const c of table.constraints) {
|
|
337
|
+
if (c.type === 'PRIMARY KEY' && c.columns.length > 1) {
|
|
338
|
+
for (const col of c.columns) {
|
|
339
|
+
const normalizedCol = useCamelCase ? toCamelCase(col) : col;
|
|
340
|
+
compositeColPkColumns.add(normalizedCol);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const uniqueColumns = new Set();
|
|
345
|
+
for (const c of table.constraints) {
|
|
346
|
+
if (c.type === 'UNIQUE' && c.columns.length === 1) {
|
|
347
|
+
const normalizedCol = useCamelCase ? toCamelCase(c.columns[0]) : c.columns[0];
|
|
348
|
+
uniqueColumns.add(normalizedCol);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const updatedColumns = table.columns.map(col => {
|
|
352
|
+
const normalizedColName = useCamelCase ? toCamelCase(col.name) : col.name;
|
|
353
|
+
const isSingleColPk = singleColPkColumns.has(col.name) || singleColPkColumns.has(normalizedColName);
|
|
354
|
+
const isCompositePkMember = compositeColPkColumns.has(col.name) || compositeColPkColumns.has(normalizedColName);
|
|
355
|
+
return {
|
|
356
|
+
...col,
|
|
357
|
+
isPrimaryKey: isSingleColPk && !isCompositePkMember,
|
|
358
|
+
isUnique: col.isUnique || uniqueColumns.has(col.name) || uniqueColumns.has(normalizedColName),
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
const columnLines = updatedColumns.map(col => generateColumnCode(col, useCamelCase, enumNames, domainNames, columnChecks.get(col.name)));
|
|
362
|
+
const optionParts = [];
|
|
363
|
+
if (table.isPartitioned && table.partitionType && table.partitionKey?.length) {
|
|
364
|
+
const partCol = useCamelCase ? toCamelCase(table.partitionKey[0]) : table.partitionKey[0];
|
|
365
|
+
const partType = table.partitionType.toLowerCase();
|
|
366
|
+
if (partType === 'hash') {
|
|
367
|
+
optionParts.push(` partitionBy: (table, p) => p.hash(table.${partCol}, 4)`);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
optionParts.push(` partitionBy: (table, p) => p.${partType}(table.${partCol})`);
|
|
371
|
+
}
|
|
372
|
+
if (table.childPartitions && table.childPartitions.length > 0) {
|
|
373
|
+
const partitionLines = table.childPartitions.map(child => {
|
|
374
|
+
return generatePartitionCode(child, table.partitionType, useCamelCase);
|
|
375
|
+
});
|
|
376
|
+
optionParts.push(` partitions: (partition) => [\n${partitionLines.join(',\n')},\n ]`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const compositePKs = table.constraints.filter(c => c.type === 'PRIMARY KEY' && c.columns.length > 1);
|
|
380
|
+
const compositeUniques = table.constraints.filter(c => c.type === 'UNIQUE' && c.columns.length > 1);
|
|
381
|
+
const excludeConstraints = table.constraints.filter(c => c.type === 'EXCLUDE');
|
|
382
|
+
if (compositePKs.length > 0 || compositeUniques.length > 0 || excludeConstraints.length > 0) {
|
|
383
|
+
const constraintLines = generateConstraintCode([...compositePKs, ...compositeUniques, ...excludeConstraints], useCamelCase);
|
|
384
|
+
if (constraintLines.length > 0) {
|
|
385
|
+
optionParts.push(` constraints: (table, constraint) => [\n${constraintLines.join(',\n')},\n ]`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const checkConstraintsOption = generateCheckConstraintsOption(table.constraints, useCamelCase);
|
|
389
|
+
if (checkConstraintsOption) {
|
|
390
|
+
optionParts.push(checkConstraintsOption);
|
|
391
|
+
}
|
|
392
|
+
if (table.indexes.length > 0) {
|
|
393
|
+
const indexLines = table.indexes.map(idx => generateIndexCode(idx, useCamelCase));
|
|
394
|
+
optionParts.push(` indexes: (table, index) => [\n${indexLines.join(',\n')},\n ]`);
|
|
395
|
+
}
|
|
396
|
+
if (tableComment) {
|
|
397
|
+
optionParts.push(tableComment);
|
|
398
|
+
}
|
|
399
|
+
const tableTrackingId = generateTrackingId('t');
|
|
400
|
+
optionParts.push(` $trackingId: '${tableTrackingId}'`);
|
|
401
|
+
let tableCode;
|
|
402
|
+
if (optionParts.length > 0) {
|
|
403
|
+
tableCode = `export const ${tableName} = defineTable('${table.name}', {\n${columnLines.join(',\n')},\n}, {\n${optionParts.join(',\n')},\n})`;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
tableCode = `export const ${tableName} = defineTable('${table.name}', {\n${columnLines.join(',\n')},\n})`;
|
|
407
|
+
}
|
|
408
|
+
parts.push(tableCode);
|
|
409
|
+
return parts.join('\n');
|
|
410
|
+
}
|
|
411
|
+
function generateEnumCode(enumDef, useCamelCase) {
|
|
412
|
+
const baseName = useCamelCase ? toCamelCase(enumDef.name) : enumDef.name;
|
|
413
|
+
const enumName = `${baseName}Enum`;
|
|
414
|
+
const valuesStr = enumDef.values.map(v => `'${escapeString(v)}'`).join(', ');
|
|
415
|
+
return `export const ${enumName} = pgEnum('${enumDef.name}', [${valuesStr}])`;
|
|
416
|
+
}
|
|
417
|
+
function generateDomainCode(domain, useCamelCase) {
|
|
418
|
+
const baseName = useCamelCase ? toCamelCase(domain.name) : domain.name;
|
|
419
|
+
const domainName = `${baseName}Domain`;
|
|
420
|
+
const baseType = getColumnBuilder(domain.baseType);
|
|
421
|
+
let line = `export const ${domainName} = pgDomain('${domain.name}', ${baseType})`;
|
|
422
|
+
if (domain.notNull) {
|
|
423
|
+
line += '.notNull()';
|
|
424
|
+
}
|
|
425
|
+
if (domain.defaultValue) {
|
|
426
|
+
line += `.default(${formatDefaultValue(domain.defaultValue)})`;
|
|
427
|
+
}
|
|
428
|
+
if (domain.checkExpression) {
|
|
429
|
+
line += `.check('${escapeString(domain.checkExpression)}')`;
|
|
430
|
+
}
|
|
431
|
+
return line;
|
|
432
|
+
}
|
|
433
|
+
function generateExtensionsCode(extensions) {
|
|
434
|
+
if (extensions.length === 0)
|
|
435
|
+
return '';
|
|
436
|
+
const extList = extensions.map(e => `'${escapeString(e)}'`).join(', ');
|
|
437
|
+
return `export const extensions = pgExtensions(${extList})`;
|
|
438
|
+
}
|
|
439
|
+
function generateSequenceCode(seq, useCamelCase) {
|
|
440
|
+
const seqName = useCamelCase ? toCamelCase(seq.name) : seq.name;
|
|
441
|
+
const opts = [];
|
|
442
|
+
if (seq.startValue !== undefined)
|
|
443
|
+
opts.push(`start: ${seq.startValue}`);
|
|
444
|
+
if (seq.increment !== undefined)
|
|
445
|
+
opts.push(`increment: ${seq.increment}`);
|
|
446
|
+
if (seq.minValue !== undefined)
|
|
447
|
+
opts.push(`minValue: ${seq.minValue}`);
|
|
448
|
+
if (seq.maxValue !== undefined)
|
|
449
|
+
opts.push(`maxValue: ${seq.maxValue}`);
|
|
450
|
+
if (seq.cache !== undefined)
|
|
451
|
+
opts.push(`cache: ${seq.cache}`);
|
|
452
|
+
if (seq.cycle)
|
|
453
|
+
opts.push(`cycle: true`);
|
|
454
|
+
if (seq.ownedBy)
|
|
455
|
+
opts.push(`ownedBy: { table: '${seq.ownedBy.table}', column: '${seq.ownedBy.column}' }`);
|
|
456
|
+
const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
|
|
457
|
+
return `export const ${seqName} = pgSequence('${seq.name}'${optsStr})`;
|
|
458
|
+
}
|
|
459
|
+
function generateFunctionCode(func, useCamelCase) {
|
|
460
|
+
const funcName = useCamelCase ? toCamelCase(func.name) : func.name;
|
|
461
|
+
const parts = [];
|
|
462
|
+
parts.push(`export const ${funcName} = pgFunction('${func.name}', {`);
|
|
463
|
+
if (func.args.length > 0) {
|
|
464
|
+
const argsStr = func.args.map(arg => {
|
|
465
|
+
const argParts = [`type: '${arg.type}'`];
|
|
466
|
+
if (arg.name)
|
|
467
|
+
argParts.push(`name: '${arg.name}'`);
|
|
468
|
+
if (arg.mode)
|
|
469
|
+
argParts.push(`mode: '${arg.mode}'`);
|
|
470
|
+
if (arg.default)
|
|
471
|
+
argParts.push(`default: '${escapeString(arg.default)}'`);
|
|
472
|
+
return `{ ${argParts.join(', ')} }`;
|
|
473
|
+
}).join(', ');
|
|
474
|
+
parts.push(` args: [${argsStr}],`);
|
|
475
|
+
}
|
|
476
|
+
parts.push(` returns: '${func.returnType}',`);
|
|
477
|
+
parts.push(` language: '${func.language}',`);
|
|
478
|
+
const escapedBody = func.body.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
479
|
+
parts.push(` body: \`${escapedBody}\`,`);
|
|
480
|
+
if (func.volatility)
|
|
481
|
+
parts.push(` volatility: '${func.volatility}',`);
|
|
482
|
+
if (func.isStrict)
|
|
483
|
+
parts.push(` strict: true,`);
|
|
484
|
+
if (func.securityDefiner)
|
|
485
|
+
parts.push(` securityDefiner: true,`);
|
|
486
|
+
parts.push(`})`);
|
|
487
|
+
return parts.join('\n');
|
|
488
|
+
}
|
|
489
|
+
function generateViewCode(view, useCamelCase) {
|
|
490
|
+
const viewName = useCamelCase ? toCamelCase(view.name) : view.name;
|
|
491
|
+
const funcName = view.isMaterialized ? 'pgMaterializedView' : 'pgView';
|
|
492
|
+
const escapedDef = view.definition.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
493
|
+
return `export const ${viewName} = ${funcName}('${view.name}', \`${escapedDef}\`)`;
|
|
494
|
+
}
|
|
495
|
+
function generateTriggerCode(trigger, useCamelCase, functionNames) {
|
|
496
|
+
const triggerName = useCamelCase ? toCamelCase(trigger.name) : trigger.name;
|
|
497
|
+
const tableName = useCamelCase ? toCamelCase(trigger.table) : trigger.table;
|
|
498
|
+
const parts = [];
|
|
499
|
+
parts.push(`export const ${triggerName} = pgTrigger('${trigger.name}', {`);
|
|
500
|
+
parts.push(` table: ${tableName},`);
|
|
501
|
+
parts.push(` timing: '${trigger.timing}',`);
|
|
502
|
+
parts.push(` events: [${trigger.events.map(e => `'${e}'`).join(', ')}],`);
|
|
503
|
+
parts.push(` forEach: '${trigger.forEach}',`);
|
|
504
|
+
const funcVarName = useCamelCase ? toCamelCase(trigger.functionName) : trigger.functionName;
|
|
505
|
+
if (functionNames.has(trigger.functionName)) {
|
|
506
|
+
parts.push(` execute: ${funcVarName},`);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
parts.push(` execute: '${trigger.functionName}',`);
|
|
510
|
+
}
|
|
511
|
+
if (trigger.whenClause)
|
|
512
|
+
parts.push(` when: '${escapeString(trigger.whenClause)}',`);
|
|
513
|
+
if (trigger.isConstraint)
|
|
514
|
+
parts.push(` constraint: true,`);
|
|
515
|
+
if (trigger.deferrable)
|
|
516
|
+
parts.push(` deferrable: true,`);
|
|
517
|
+
if (trigger.initiallyDeferred)
|
|
518
|
+
parts.push(` initiallyDeferred: true,`);
|
|
519
|
+
parts.push(`})`);
|
|
520
|
+
return parts.join('\n');
|
|
521
|
+
}
|
|
522
|
+
export function generateTypeScriptFromAST(schema, options = {}) {
|
|
523
|
+
const { camelCase = true, importPath = 'relq/schema-builder', includeEnums = true, includeDomains = true, includeTables = true, includeFunctions = true, includeTriggers = true, } = options;
|
|
524
|
+
const parts = [];
|
|
525
|
+
parts.push('/**');
|
|
526
|
+
parts.push(' * Auto-generated by Relq CLI (AST-based)');
|
|
527
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
528
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
529
|
+
parts.push(' */');
|
|
530
|
+
parts.push('');
|
|
531
|
+
needsDefaultImport = false;
|
|
532
|
+
needsSqlImport = false;
|
|
533
|
+
resetDefaultImportFlags();
|
|
534
|
+
resetSqlImportFlag();
|
|
535
|
+
const needsPgExtensions = schema.extensions.length > 0;
|
|
536
|
+
const needsPgSequence = schema.sequences.length > 0;
|
|
537
|
+
const needsPgFunction = includeFunctions && schema.functions.length > 0;
|
|
538
|
+
const needsPgTrigger = includeTriggers && schema.triggers.length > 0;
|
|
539
|
+
const needsPgEnum = includeEnums && schema.enums.length > 0;
|
|
540
|
+
const needsPgView = options.includeViews !== false && schema.views.filter(v => !v.isMaterialized).length > 0;
|
|
541
|
+
const needsPgMaterializedView = options.includeViews !== false && schema.views.filter(v => v.isMaterialized).length > 0;
|
|
542
|
+
const enumNames = new Set(schema.enums.map(e => e.name.toLowerCase()));
|
|
543
|
+
const domainNames = new Set(schema.domains.map(d => d.name.toLowerCase()));
|
|
544
|
+
const tableCodeParts = [];
|
|
545
|
+
const usedColumnTypes = new Set();
|
|
546
|
+
let needsEnumType = false;
|
|
547
|
+
let needsDomainType = false;
|
|
548
|
+
const sortedTables = [...schema.tables].sort((a, b) => {
|
|
549
|
+
if (a.partitionOf)
|
|
550
|
+
return 1;
|
|
551
|
+
if (b.partitionOf)
|
|
552
|
+
return -1;
|
|
553
|
+
return a.name.localeCompare(b.name);
|
|
554
|
+
});
|
|
555
|
+
for (const table of sortedTables) {
|
|
556
|
+
if (table.partitionOf)
|
|
557
|
+
continue;
|
|
558
|
+
const code = generateTableCode(table, camelCase, enumNames, domainNames);
|
|
559
|
+
tableCodeParts.push(code);
|
|
560
|
+
for (const col of table.columns) {
|
|
561
|
+
if (enumNames.has(col.type.toLowerCase())) {
|
|
562
|
+
needsEnumType = true;
|
|
563
|
+
}
|
|
564
|
+
else if (domainNames.has(col.type.toLowerCase())) {
|
|
565
|
+
needsDomainType = true;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const builder = getColumnBuilder(col.type, col.typeParams);
|
|
569
|
+
const typeName = builder.split('(')[0];
|
|
570
|
+
if (typeName) {
|
|
571
|
+
usedColumnTypes.add(typeName);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const needsRelations = schema.tables.some(table => !table.partitionOf && (table.constraints.some(c => c.type === 'FOREIGN KEY' && c.references) ||
|
|
577
|
+
table.columns.some(c => c.references)));
|
|
578
|
+
const imports = ['defineTable'];
|
|
579
|
+
if (needsPgEnum)
|
|
580
|
+
imports.push('pgEnum');
|
|
581
|
+
if (needsEnumType)
|
|
582
|
+
imports.push('enumType');
|
|
583
|
+
if (includeDomains && schema.domains.length > 0)
|
|
584
|
+
imports.push('pgDomain');
|
|
585
|
+
if (needsDomainType)
|
|
586
|
+
imports.push('domainType');
|
|
587
|
+
if (needsPgExtensions)
|
|
588
|
+
imports.push('pgExtensions');
|
|
589
|
+
if (needsPgSequence)
|
|
590
|
+
imports.push('pgSequence');
|
|
591
|
+
if (needsPgView)
|
|
592
|
+
imports.push('pgView');
|
|
593
|
+
if (needsPgMaterializedView)
|
|
594
|
+
imports.push('pgMaterializedView');
|
|
595
|
+
if (needsPgFunction)
|
|
596
|
+
imports.push('pgFunction');
|
|
597
|
+
if (needsPgTrigger)
|
|
598
|
+
imports.push('pgTrigger');
|
|
599
|
+
const finalNeedsDefaultImport = needsDefaultImport || getDefaultImportNeeded();
|
|
600
|
+
const finalNeedsSqlImport = needsSqlImport || getSqlImportNeeded() || getDefaultSqlImportNeeded();
|
|
601
|
+
if (finalNeedsDefaultImport)
|
|
602
|
+
imports.push('DEFAULT');
|
|
603
|
+
if (finalNeedsSqlImport)
|
|
604
|
+
imports.push('sql');
|
|
605
|
+
if (needsRelations)
|
|
606
|
+
imports.push('pgRelations');
|
|
607
|
+
for (const type of usedColumnTypes) {
|
|
608
|
+
if (!imports.includes(type))
|
|
609
|
+
imports.push(type);
|
|
610
|
+
}
|
|
611
|
+
parts.push(`import {`);
|
|
612
|
+
parts.push(` ${imports.join(',\n ')},`);
|
|
613
|
+
parts.push(` type RelqDatabaseSchema,`);
|
|
614
|
+
parts.push(`} from '${importPath}';`);
|
|
615
|
+
parts.push('');
|
|
616
|
+
if (needsPgExtensions) {
|
|
617
|
+
parts.push('// =============================================================================');
|
|
618
|
+
parts.push('// EXTENSIONS');
|
|
619
|
+
parts.push('// =============================================================================');
|
|
620
|
+
parts.push('');
|
|
621
|
+
parts.push(generateExtensionsCode(schema.extensions));
|
|
622
|
+
parts.push('');
|
|
623
|
+
}
|
|
624
|
+
if (needsPgEnum) {
|
|
625
|
+
parts.push('// =============================================================================');
|
|
626
|
+
parts.push('// ENUMS');
|
|
627
|
+
parts.push('// =============================================================================');
|
|
628
|
+
parts.push('');
|
|
629
|
+
for (const enumDef of schema.enums) {
|
|
630
|
+
parts.push(generateEnumCode(enumDef, camelCase));
|
|
631
|
+
}
|
|
632
|
+
parts.push('');
|
|
633
|
+
}
|
|
634
|
+
if (includeDomains && schema.domains.length > 0) {
|
|
635
|
+
parts.push('// =============================================================================');
|
|
636
|
+
parts.push('// DOMAINS');
|
|
637
|
+
parts.push('// =============================================================================');
|
|
638
|
+
parts.push('');
|
|
639
|
+
for (const domain of schema.domains) {
|
|
640
|
+
parts.push(generateDomainCode(domain, camelCase));
|
|
641
|
+
}
|
|
642
|
+
parts.push('');
|
|
643
|
+
}
|
|
644
|
+
if (needsPgSequence) {
|
|
645
|
+
parts.push('// =============================================================================');
|
|
646
|
+
parts.push('// SEQUENCES');
|
|
647
|
+
parts.push('// =============================================================================');
|
|
648
|
+
parts.push('');
|
|
649
|
+
for (const seq of schema.sequences) {
|
|
650
|
+
parts.push(generateSequenceCode(seq, camelCase));
|
|
651
|
+
}
|
|
652
|
+
parts.push('');
|
|
653
|
+
}
|
|
654
|
+
if (includeTables && schema.tables.length > 0) {
|
|
655
|
+
parts.push('// =============================================================================');
|
|
656
|
+
parts.push('// TABLES');
|
|
657
|
+
parts.push('// =============================================================================');
|
|
658
|
+
parts.push('');
|
|
659
|
+
const sortedTables = [...schema.tables].sort((a, b) => {
|
|
660
|
+
if (a.partitionOf)
|
|
661
|
+
return 1;
|
|
662
|
+
if (b.partitionOf)
|
|
663
|
+
return -1;
|
|
664
|
+
return a.name.localeCompare(b.name);
|
|
665
|
+
});
|
|
666
|
+
for (const table of sortedTables) {
|
|
667
|
+
if (table.partitionOf)
|
|
668
|
+
continue;
|
|
669
|
+
parts.push(generateTableCode(table, camelCase, enumNames, domainNames));
|
|
670
|
+
parts.push('');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if ((needsPgView || needsPgMaterializedView) && schema.views.length > 0) {
|
|
674
|
+
parts.push('// =============================================================================');
|
|
675
|
+
parts.push('// VIEWS');
|
|
676
|
+
parts.push('// =============================================================================');
|
|
677
|
+
parts.push('');
|
|
678
|
+
for (const view of schema.views) {
|
|
679
|
+
parts.push(generateViewCode(view, camelCase));
|
|
680
|
+
parts.push('');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (needsPgFunction) {
|
|
684
|
+
parts.push('// =============================================================================');
|
|
685
|
+
parts.push('// FUNCTIONS');
|
|
686
|
+
parts.push('// =============================================================================');
|
|
687
|
+
parts.push('');
|
|
688
|
+
for (const func of schema.functions) {
|
|
689
|
+
parts.push(generateFunctionCode(func, camelCase));
|
|
690
|
+
parts.push('');
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (needsPgTrigger) {
|
|
694
|
+
parts.push('// =============================================================================');
|
|
695
|
+
parts.push('// TRIGGERS');
|
|
696
|
+
parts.push('// =============================================================================');
|
|
697
|
+
parts.push('');
|
|
698
|
+
const functionNames = new Set(schema.functions.map(f => f.name));
|
|
699
|
+
for (const trigger of schema.triggers) {
|
|
700
|
+
parts.push(generateTriggerCode(trigger, camelCase, functionNames));
|
|
701
|
+
parts.push('');
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
parts.push('// =============================================================================');
|
|
705
|
+
parts.push('// SCHEMA EXPORT');
|
|
706
|
+
parts.push('// =============================================================================');
|
|
707
|
+
parts.push('');
|
|
708
|
+
const tableNames = schema.tables
|
|
709
|
+
.filter(t => !t.partitionOf)
|
|
710
|
+
.map(t => camelCase ? toCamelCase(t.name) : t.name)
|
|
711
|
+
.sort();
|
|
712
|
+
parts.push('export const schema = {');
|
|
713
|
+
for (const name of tableNames) {
|
|
714
|
+
parts.push(` ${name},`);
|
|
715
|
+
}
|
|
716
|
+
parts.push('} as const;');
|
|
717
|
+
parts.push('');
|
|
718
|
+
parts.push('export type DatabaseSchema = RelqDatabaseSchema<typeof schema>;');
|
|
719
|
+
parts.push('');
|
|
720
|
+
const relationsCode = generateRelationsCode(schema, camelCase);
|
|
721
|
+
if (relationsCode) {
|
|
722
|
+
parts.push('// =============================================================================');
|
|
723
|
+
parts.push('// RELATIONS');
|
|
724
|
+
parts.push('// =============================================================================');
|
|
725
|
+
parts.push('');
|
|
726
|
+
parts.push(relationsCode);
|
|
727
|
+
parts.push('');
|
|
728
|
+
}
|
|
729
|
+
return parts.join('\n');
|
|
730
|
+
}
|
|
731
|
+
function generateRelationsCode(schema, camelCase) {
|
|
732
|
+
const foreignKeys = [];
|
|
733
|
+
for (const table of schema.tables) {
|
|
734
|
+
if (table.partitionOf)
|
|
735
|
+
continue;
|
|
736
|
+
for (const column of table.columns) {
|
|
737
|
+
if (column.references) {
|
|
738
|
+
foreignKeys.push({
|
|
739
|
+
fromTable: table.name,
|
|
740
|
+
fromColumn: column.name,
|
|
741
|
+
toTable: column.references.table,
|
|
742
|
+
toColumn: column.references.column,
|
|
743
|
+
onDelete: column.references.onDelete,
|
|
744
|
+
onUpdate: column.references.onUpdate,
|
|
745
|
+
match: column.references.match,
|
|
746
|
+
deferrable: column.references.deferrable,
|
|
747
|
+
initiallyDeferred: column.references.initiallyDeferred,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
for (const constraint of table.constraints) {
|
|
752
|
+
if (constraint.type === 'FOREIGN KEY' && constraint.references) {
|
|
753
|
+
if (constraint.columns.length === 1 && constraint.references.columns.length === 1) {
|
|
754
|
+
const alreadyExists = foreignKeys.some(fk => fk.fromTable === table.name &&
|
|
755
|
+
fk.fromColumn === constraint.columns[0] &&
|
|
756
|
+
fk.toTable === constraint.references.table);
|
|
757
|
+
if (!alreadyExists) {
|
|
758
|
+
foreignKeys.push({
|
|
759
|
+
fromTable: table.name,
|
|
760
|
+
fromColumn: constraint.columns[0],
|
|
761
|
+
toTable: constraint.references.table,
|
|
762
|
+
toColumn: constraint.references.columns[0],
|
|
763
|
+
constraintName: getExplicitFKName(constraint.name, table.name, constraint.columns[0]),
|
|
764
|
+
onDelete: constraint.references.onDelete,
|
|
765
|
+
onUpdate: constraint.references.onUpdate,
|
|
766
|
+
match: constraint.references.match,
|
|
767
|
+
deferrable: constraint.references.deferrable,
|
|
768
|
+
initiallyDeferred: constraint.references.initiallyDeferred,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else if (constraint.columns.length > 1) {
|
|
773
|
+
foreignKeys.push({
|
|
774
|
+
fromTable: table.name,
|
|
775
|
+
fromColumn: constraint.columns[0],
|
|
776
|
+
toTable: constraint.references.table,
|
|
777
|
+
toColumn: constraint.references.columns[0],
|
|
778
|
+
constraintName: getExplicitFKName(constraint.name, table.name, constraint.columns[0]),
|
|
779
|
+
onDelete: constraint.references.onDelete,
|
|
780
|
+
onUpdate: constraint.references.onUpdate,
|
|
781
|
+
match: constraint.references.match,
|
|
782
|
+
deferrable: constraint.references.deferrable,
|
|
783
|
+
initiallyDeferred: constraint.references.initiallyDeferred,
|
|
784
|
+
isComposite: true,
|
|
785
|
+
fromColumns: constraint.columns,
|
|
786
|
+
toColumns: constraint.references.columns,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (foreignKeys.length === 0) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const relationsByTable = new Map();
|
|
796
|
+
const usedRelationNames = new Map();
|
|
797
|
+
for (const fk of foreignKeys) {
|
|
798
|
+
const fromTableName = camelCase ? toCamelCase(fk.fromTable) : fk.fromTable;
|
|
799
|
+
const toTableName = camelCase ? toCamelCase(fk.toTable) : fk.toTable;
|
|
800
|
+
const fromColName = camelCase ? toCamelCase(fk.fromColumn) : fk.fromColumn;
|
|
801
|
+
const toColName = camelCase ? toCamelCase(fk.toColumn) : fk.toColumn;
|
|
802
|
+
const relationName = fromColName;
|
|
803
|
+
if (!usedRelationNames.has(fromTableName)) {
|
|
804
|
+
usedRelationNames.set(fromTableName, new Set());
|
|
805
|
+
}
|
|
806
|
+
const usedNames = usedRelationNames.get(fromTableName);
|
|
807
|
+
let finalRelationName = relationName;
|
|
808
|
+
let suffix = 1;
|
|
809
|
+
while (usedNames.has(finalRelationName)) {
|
|
810
|
+
finalRelationName = `${relationName}${suffix}`;
|
|
811
|
+
suffix++;
|
|
812
|
+
}
|
|
813
|
+
usedNames.add(finalRelationName);
|
|
814
|
+
const options = [];
|
|
815
|
+
if (fk.constraintName) {
|
|
816
|
+
options.push(`name: '${fk.constraintName}'`);
|
|
817
|
+
}
|
|
818
|
+
const isValidColumn = fk.toColumn &&
|
|
819
|
+
fk.toColumn.length > 0 &&
|
|
820
|
+
/^[a-zA-Z_]/.test(fk.toColumn) &&
|
|
821
|
+
fk.toColumn.toLowerCase() !== 'id';
|
|
822
|
+
if (isValidColumn) {
|
|
823
|
+
options.push(`references: t.${toColName}`);
|
|
824
|
+
}
|
|
825
|
+
if (fk.onDelete && fk.onDelete !== 'NO ACTION') {
|
|
826
|
+
options.push(`onDelete: '${fk.onDelete}'`);
|
|
827
|
+
}
|
|
828
|
+
if (fk.onUpdate && fk.onUpdate !== 'NO ACTION') {
|
|
829
|
+
options.push(`onUpdate: '${fk.onUpdate}'`);
|
|
830
|
+
}
|
|
831
|
+
if (fk.match && fk.match !== 'SIMPLE') {
|
|
832
|
+
options.push(`match: '${fk.match}'`);
|
|
833
|
+
}
|
|
834
|
+
if (fk.deferrable) {
|
|
835
|
+
options.push(`deferrable: true`);
|
|
836
|
+
}
|
|
837
|
+
if (fk.initiallyDeferred) {
|
|
838
|
+
options.push(`initiallyDeferred: true`);
|
|
839
|
+
}
|
|
840
|
+
if (fk.isComposite && fk.fromColumns && fk.toColumns) {
|
|
841
|
+
const fromCols = fk.fromColumns.map(c => `r.${fromTableName}.${camelCase ? toCamelCase(c) : c}`).join(', ');
|
|
842
|
+
const toCols = fk.toColumns.map(c => `t.${camelCase ? toCamelCase(c) : c}`).join(', ');
|
|
843
|
+
const hasName = fk.constraintName;
|
|
844
|
+
options.length = 0;
|
|
845
|
+
if (hasName) {
|
|
846
|
+
options.push(`name: '${fk.constraintName}'`);
|
|
847
|
+
}
|
|
848
|
+
options.push(`columns: [${fromCols}]`);
|
|
849
|
+
options.push(`references: [${toCols}]`);
|
|
850
|
+
if (fk.onDelete && fk.onDelete !== 'NO ACTION') {
|
|
851
|
+
options.push(`onDelete: '${fk.onDelete}'`);
|
|
852
|
+
}
|
|
853
|
+
if (fk.onUpdate && fk.onUpdate !== 'NO ACTION') {
|
|
854
|
+
options.push(`onUpdate: '${fk.onUpdate}'`);
|
|
855
|
+
}
|
|
856
|
+
if (fk.match && fk.match !== 'SIMPLE') {
|
|
857
|
+
options.push(`match: '${fk.match}'`);
|
|
858
|
+
}
|
|
859
|
+
if (fk.deferrable) {
|
|
860
|
+
options.push(`deferrable: true`);
|
|
861
|
+
}
|
|
862
|
+
if (fk.initiallyDeferred) {
|
|
863
|
+
options.push(`initiallyDeferred: true`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const trackingId = generateTrackingId('f');
|
|
867
|
+
options.push(`trackingId: '${trackingId}'`);
|
|
868
|
+
const sqlComment = generateFKSQLComment(fk, camelCase);
|
|
869
|
+
if (sqlComment) {
|
|
870
|
+
options.push(`comment: '${escapeString(sqlComment)}'`);
|
|
871
|
+
}
|
|
872
|
+
let code;
|
|
873
|
+
if (options.length === 0) {
|
|
874
|
+
code = `${finalRelationName}: r.referenceTo.${toTableName}(t => ({}))`;
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
const optionsStr = options.join(',\n ');
|
|
878
|
+
code = `${finalRelationName}: r.referenceTo.${toTableName}(t => ({
|
|
879
|
+
${optionsStr},
|
|
880
|
+
}))`;
|
|
881
|
+
}
|
|
882
|
+
if (!relationsByTable.has(fromTableName)) {
|
|
883
|
+
relationsByTable.set(fromTableName, []);
|
|
884
|
+
}
|
|
885
|
+
relationsByTable.get(fromTableName).push({ relationName: finalRelationName, code });
|
|
886
|
+
}
|
|
887
|
+
const lines = [];
|
|
888
|
+
const tableNames = new Set(schema.tables.map(t => camelCase ? toCamelCase(t.name) : t.name));
|
|
889
|
+
let relationsExportName = 'relations';
|
|
890
|
+
if (tableNames.has('relations')) {
|
|
891
|
+
if (!tableNames.has('dbRelations')) {
|
|
892
|
+
relationsExportName = 'dbRelations';
|
|
893
|
+
}
|
|
894
|
+
else if (!tableNames.has('fdbRelations')) {
|
|
895
|
+
relationsExportName = 'fdbRelations';
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
relationsExportName = '_relqRelations';
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
lines.push(`export const ${relationsExportName} = pgRelations(schema, (tables) => ({`);
|
|
902
|
+
const sortedTables = [...relationsByTable.keys()].sort();
|
|
903
|
+
for (const tableName of sortedTables) {
|
|
904
|
+
const tableRelations = relationsByTable.get(tableName);
|
|
905
|
+
lines.push(` ${tableName}: tables.${tableName}((r) => ({`);
|
|
906
|
+
for (const rel of tableRelations) {
|
|
907
|
+
lines.push(` ${rel.code},`);
|
|
908
|
+
}
|
|
909
|
+
lines.push(' })),');
|
|
910
|
+
}
|
|
911
|
+
lines.push('}));');
|
|
912
|
+
return lines.join('\n');
|
|
913
|
+
}
|
|
914
|
+
function generateFKSQLComment(fk, camelCase) {
|
|
915
|
+
const parts = [];
|
|
916
|
+
const isValidCol = (col) => col && col.length > 0 && /^[a-zA-Z_]/.test(col);
|
|
917
|
+
if (fk.isComposite && fk.fromColumns) {
|
|
918
|
+
const validToCols = fk.toColumns?.filter(isValidCol);
|
|
919
|
+
if (validToCols && validToCols.length > 0) {
|
|
920
|
+
parts.push(`(${fk.fromColumns.join(', ')}) → ${fk.toTable}(${validToCols.join(', ')})`);
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
parts.push(`(${fk.fromColumns.join(', ')}) → ${fk.toTable}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
const targetCol = isValidCol(fk.toColumn) && fk.toColumn.toLowerCase() !== 'id'
|
|
928
|
+
? `(${fk.toColumn})`
|
|
929
|
+
: '';
|
|
930
|
+
parts.push(`→ ${fk.toTable}${targetCol}`);
|
|
931
|
+
}
|
|
932
|
+
if (fk.onDelete && fk.onDelete !== 'NO ACTION') {
|
|
933
|
+
parts.push(`ON DELETE ${fk.onDelete}`);
|
|
934
|
+
}
|
|
935
|
+
if (fk.onUpdate && fk.onUpdate !== 'NO ACTION') {
|
|
936
|
+
parts.push(`ON UPDATE ${fk.onUpdate}`);
|
|
937
|
+
}
|
|
938
|
+
if (fk.match && fk.match !== 'SIMPLE') {
|
|
939
|
+
parts.push(`MATCH ${fk.match}`);
|
|
940
|
+
}
|
|
941
|
+
if (fk.deferrable) {
|
|
942
|
+
parts.push(fk.initiallyDeferred ? 'DEFERRABLE INITIALLY DEFERRED' : 'DEFERRABLE');
|
|
943
|
+
}
|
|
944
|
+
return parts.join(' | ');
|
|
945
|
+
}
|