dzql 0.6.23 → 0.6.24
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/package.json +1 -1
- package/src/cli/codegen/sql.ts +27 -2
- package/src/cli/index.ts +99 -12
package/package.json
CHANGED
package/src/cli/codegen/sql.ts
CHANGED
|
@@ -179,9 +179,23 @@ $$;
|
|
|
179
179
|
`;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Generate CREATE TABLE SQL for an entity.
|
|
184
|
+
* @param name - Entity/table name
|
|
185
|
+
* @param entityIR - Entity IR definition
|
|
186
|
+
* @param circularFKs - Set of "table.column" strings for circular FK columns to defer
|
|
187
|
+
*/
|
|
188
|
+
export function generateSchemaSQL(name: string, entityIR: EntityIR, circularFKs?: Set<string>): string {
|
|
183
189
|
const columns = entityIR.columns.map((c: ColumnInfo) => {
|
|
184
|
-
|
|
190
|
+
let colType = c.type;
|
|
191
|
+
|
|
192
|
+
// If this column is part of a circular FK, strip the REFERENCES clause
|
|
193
|
+
// The constraint will be added later via ALTER TABLE
|
|
194
|
+
if (circularFKs && circularFKs.has(`${name}.${c.name}`)) {
|
|
195
|
+
colType = stripFKReferences(colType);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return `${c.name} ${colType}`;
|
|
185
199
|
}).join(',\n ');
|
|
186
200
|
|
|
187
201
|
return `
|
|
@@ -191,6 +205,17 @@ CREATE TABLE IF NOT EXISTS ${name} (
|
|
|
191
205
|
`;
|
|
192
206
|
}
|
|
193
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Remove REFERENCES clause from a column type for deferred FK creation.
|
|
210
|
+
* Preserves NOT NULL, DEFAULT, and other modifiers.
|
|
211
|
+
* e.g., "int NOT NULL REFERENCES users(id) ON DELETE CASCADE" -> "int NOT NULL"
|
|
212
|
+
*/
|
|
213
|
+
function stripFKReferences(columnType: string): string {
|
|
214
|
+
return columnType
|
|
215
|
+
.replace(/\s*REFERENCES\s+\w+(\([^)]+\))?(\s+ON\s+(DELETE|UPDATE)\s+(CASCADE|SET NULL|SET DEFAULT|RESTRICT|NO ACTION))*/gi, '')
|
|
216
|
+
.trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
// === SAVE FUNCTION (Upsert) ===
|
|
195
220
|
export function generateSaveFunction(name: string, entityIR: EntityIR): string {
|
|
196
221
|
const cols = entityIR.columns.map((c: ColumnInfo) => c.name);
|
package/src/cli/index.ts
CHANGED
|
@@ -70,12 +70,17 @@ async function main() {
|
|
|
70
70
|
|
|
71
71
|
// Topologically sort entities by FK dependencies
|
|
72
72
|
// Entities must be created before entities that reference them
|
|
73
|
-
|
|
73
|
+
// Also detects circular FKs that need deferred constraint creation
|
|
74
|
+
const { sorted: sortedEntityNames, circularFKs } = topologicalSortEntities(ir.entities);
|
|
75
|
+
|
|
76
|
+
// Build set of circular FK columns for stripping during schema generation
|
|
77
|
+
const circularFKSet = new Set(circularFKs.map(fk => `${fk.table}.${fk.column}`));
|
|
74
78
|
|
|
75
79
|
const entitySQL: string[] = [];
|
|
76
80
|
for (const name of sortedEntityNames) {
|
|
77
81
|
const entityIR = ir.entities[name];
|
|
78
|
-
|
|
82
|
+
// Pass circular FK info so schema generation can strip those constraints
|
|
83
|
+
entitySQL.push(generateSchemaSQL(name, entityIR, circularFKSet));
|
|
79
84
|
// Skip CRUD generation for unmanaged entities (e.g., junction tables)
|
|
80
85
|
if (entityIR.managed !== false) {
|
|
81
86
|
entitySQL.push(generateEntitySQL(name, entityIR));
|
|
@@ -84,6 +89,11 @@ async function main() {
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
// Add deferred FK constraints at the end (for circular dependencies)
|
|
93
|
+
if (circularFKs.length > 0) {
|
|
94
|
+
entitySQL.push(generateDeferredFKConstraints(circularFKs));
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
// Generate subscribable SQL functions
|
|
88
98
|
const subscribableSQL: string[] = [];
|
|
89
99
|
const subscribableNames = Object.keys(ir.subscribables);
|
|
@@ -178,18 +188,36 @@ async function main() {
|
|
|
178
188
|
|
|
179
189
|
main();
|
|
180
190
|
|
|
191
|
+
/** FK info for dependency analysis */
|
|
192
|
+
interface FKInfo {
|
|
193
|
+
table: string;
|
|
194
|
+
column: string;
|
|
195
|
+
referencedTable: string;
|
|
196
|
+
fullConstraint: string; // e.g., "int NOT NULL REFERENCES users(id)"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Result of topological sort with cycle detection */
|
|
200
|
+
interface SortResult {
|
|
201
|
+
sorted: string[];
|
|
202
|
+
circularFKs: FKInfo[];
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
/**
|
|
182
206
|
* Topologically sort entities based on FK dependencies.
|
|
183
207
|
* Entities that are referenced by others come first.
|
|
184
208
|
* Uses Kahn's algorithm for topological sorting.
|
|
209
|
+
* Returns both sorted order and any circular FK constraints that need deferred creation.
|
|
185
210
|
*/
|
|
186
|
-
function topologicalSortEntities(entities: Record<string, any>):
|
|
211
|
+
function topologicalSortEntities(entities: Record<string, any>): SortResult {
|
|
187
212
|
const entityNames = Object.keys(entities);
|
|
188
213
|
|
|
189
214
|
// Build dependency graph: entity -> entities it depends on (references)
|
|
190
215
|
const dependencies: Record<string, Set<string>> = {};
|
|
191
216
|
const dependents: Record<string, Set<string>> = {};
|
|
192
217
|
|
|
218
|
+
// Track all FK info for cycle detection
|
|
219
|
+
const allFKs: FKInfo[] = [];
|
|
220
|
+
|
|
193
221
|
for (const name of entityNames) {
|
|
194
222
|
dependencies[name] = new Set();
|
|
195
223
|
dependents[name] = new Set();
|
|
@@ -206,6 +234,12 @@ function topologicalSortEntities(entities: Record<string, any>): string[] {
|
|
|
206
234
|
if (entityNames.includes(referencedEntity)) {
|
|
207
235
|
dependencies[name].add(referencedEntity);
|
|
208
236
|
dependents[referencedEntity].add(name);
|
|
237
|
+
allFKs.push({
|
|
238
|
+
table: name,
|
|
239
|
+
column: col.name,
|
|
240
|
+
referencedTable: referencedEntity,
|
|
241
|
+
fullConstraint: col.type
|
|
242
|
+
});
|
|
209
243
|
}
|
|
210
244
|
}
|
|
211
245
|
}
|
|
@@ -214,10 +248,16 @@ function topologicalSortEntities(entities: Record<string, any>): string[] {
|
|
|
214
248
|
// Kahn's algorithm
|
|
215
249
|
const result: string[] = [];
|
|
216
250
|
const noIncoming: string[] = [];
|
|
251
|
+
const remainingDeps: Record<string, Set<string>> = {};
|
|
252
|
+
|
|
253
|
+
// Copy dependencies for mutation
|
|
254
|
+
for (const name of entityNames) {
|
|
255
|
+
remainingDeps[name] = new Set(dependencies[name]);
|
|
256
|
+
}
|
|
217
257
|
|
|
218
258
|
// Find entities with no dependencies (no incoming edges)
|
|
219
259
|
for (const name of entityNames) {
|
|
220
|
-
if (
|
|
260
|
+
if (remainingDeps[name].size === 0) {
|
|
221
261
|
noIncoming.push(name);
|
|
222
262
|
}
|
|
223
263
|
}
|
|
@@ -228,20 +268,67 @@ function topologicalSortEntities(entities: Record<string, any>): string[] {
|
|
|
228
268
|
|
|
229
269
|
// Remove this node from the graph
|
|
230
270
|
for (const dependent of dependents[node]) {
|
|
231
|
-
|
|
232
|
-
if (
|
|
271
|
+
remainingDeps[dependent].delete(node);
|
|
272
|
+
if (remainingDeps[dependent].size === 0) {
|
|
233
273
|
noIncoming.push(dependent);
|
|
234
274
|
}
|
|
235
275
|
}
|
|
236
276
|
}
|
|
237
277
|
|
|
238
|
-
//
|
|
278
|
+
// Detect circular FKs
|
|
279
|
+
const circularFKs: FKInfo[] = [];
|
|
239
280
|
if (result.length !== entityNames.length) {
|
|
240
|
-
const
|
|
241
|
-
console.warn(`[Compiler] Warning: Circular FK dependencies detected among: ${
|
|
242
|
-
|
|
243
|
-
|
|
281
|
+
const cycleEntities = new Set(entityNames.filter(n => !result.includes(n)));
|
|
282
|
+
console.warn(`[Compiler] Warning: Circular FK dependencies detected among: ${[...cycleEntities].join(', ')}`);
|
|
283
|
+
|
|
284
|
+
// Find FKs that are part of the cycle - these need deferred creation
|
|
285
|
+
for (const fk of allFKs) {
|
|
286
|
+
if (cycleEntities.has(fk.table) && cycleEntities.has(fk.referencedTable)) {
|
|
287
|
+
circularFKs.push(fk);
|
|
288
|
+
console.log(`[Compiler] Deferring circular FK: ${fk.table}.${fk.column} -> ${fk.referencedTable}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Add remaining entities (cycle members) to result
|
|
293
|
+
result.push(...cycleEntities);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { sorted: result, circularFKs };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Generate ALTER TABLE statements for deferred circular FK constraints.
|
|
301
|
+
*/
|
|
302
|
+
function generateDeferredFKConstraints(circularFKs: FKInfo[]): string {
|
|
303
|
+
if (circularFKs.length === 0) return '';
|
|
304
|
+
|
|
305
|
+
const statements: string[] = [
|
|
306
|
+
'\n-- === DEFERRED FK CONSTRAINTS (for circular dependencies) ===\n'
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
for (const fk of circularFKs) {
|
|
310
|
+
// Extract the REFERENCES part from the full constraint
|
|
311
|
+
const refMatch = fk.fullConstraint.match(/REFERENCES\s+(\w+)(\([^)]+\))?(\s+ON\s+.+)?/i);
|
|
312
|
+
if (refMatch) {
|
|
313
|
+
const targetTable = refMatch[1];
|
|
314
|
+
const targetCol = refMatch[2] || '(id)';
|
|
315
|
+
const cascadeClause = refMatch[3] || '';
|
|
316
|
+
const constraintName = `fk_${fk.table}_${fk.column}`;
|
|
317
|
+
|
|
318
|
+
statements.push(`ALTER TABLE ${fk.table} ADD CONSTRAINT ${constraintName} FOREIGN KEY (${fk.column}) REFERENCES ${targetTable}${targetCol}${cascadeClause};`);
|
|
319
|
+
}
|
|
244
320
|
}
|
|
245
321
|
|
|
246
|
-
return
|
|
322
|
+
return statements.join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Remove REFERENCES clause from a column type for deferred FK creation.
|
|
327
|
+
* Preserves NOT NULL and other modifiers.
|
|
328
|
+
*/
|
|
329
|
+
function stripReferences(columnType: string): string {
|
|
330
|
+
// Remove REFERENCES clause and any ON DELETE/UPDATE clauses
|
|
331
|
+
return columnType
|
|
332
|
+
.replace(/\s*REFERENCES\s+\w+(\([^)]+\))?(\s+ON\s+(DELETE|UPDATE)\s+(CASCADE|SET NULL|SET DEFAULT|RESTRICT|NO ACTION))*\s*/gi, ' ')
|
|
333
|
+
.trim();
|
|
247
334
|
}
|