dzql 0.6.19 → 0.6.21
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/docs/README.md +71 -321
- package/docs/for_ai.md +355 -552
- package/package.json +1 -1
- package/src/cli/codegen/sql.ts +26 -0
- package/src/cli/index.ts +75 -1
package/package.json
CHANGED
package/src/cli/codegen/sql.ts
CHANGED
|
@@ -583,6 +583,31 @@ export function generateGetFunction(name: string, entityIR: EntityIR): string {
|
|
|
583
583
|
// Build SELECT expression excluding hidden fields
|
|
584
584
|
const selectExpr = buildVisibleJsonb(name, entityIR.columns, hidden);
|
|
585
585
|
|
|
586
|
+
// FK expansion for GET (only direct FKs where {key}_id column exists)
|
|
587
|
+
// Reverse FK expansion (one-to-many) should be handled by subscribables for complex queries
|
|
588
|
+
const includes: Record<string, IncludeIR> = entityIR.includes || {};
|
|
589
|
+
const includeKeys = Object.keys(includes);
|
|
590
|
+
const fkExpansion = includeKeys.map(key => {
|
|
591
|
+
const config: IncludeIR = includes[key];
|
|
592
|
+
const targetEntity = config.entity;
|
|
593
|
+
const fkField = `${key}_id`; // Convention: author -> author_id
|
|
594
|
+
|
|
595
|
+
// Only expand direct FKs (key_id column exists on this entity)
|
|
596
|
+
const hasFkColumn = entityIR.columns.some((c: ColumnInfo) => c.name === fkField);
|
|
597
|
+
|
|
598
|
+
if (hasFkColumn) {
|
|
599
|
+
// Direct FK: single object expansion (e.g., author_id -> author object)
|
|
600
|
+
return `
|
|
601
|
+
-- FK: Add ${key} to result (from ${fkField})
|
|
602
|
+
IF (v_result->>'${fkField}') IS NOT NULL THEN
|
|
603
|
+
v_result := v_result || jsonb_build_object('${key}',
|
|
604
|
+
(SELECT to_jsonb(t.*) FROM ${targetEntity} t WHERE t.id = (v_result->>'${fkField}')::int));
|
|
605
|
+
END IF;`;
|
|
606
|
+
}
|
|
607
|
+
// Skip reverse FKs - use subscribables for complex document graphs
|
|
608
|
+
return '';
|
|
609
|
+
}).filter(s => s).join('\n');
|
|
610
|
+
|
|
586
611
|
// M2M expansion for GET
|
|
587
612
|
const m2m: Record<string, ManyToManyIR> = entityIR.manyToMany || {};
|
|
588
613
|
const m2mKeys = Object.keys(m2m);
|
|
@@ -625,6 +650,7 @@ BEGIN
|
|
|
625
650
|
IF v_result IS NULL THEN
|
|
626
651
|
RETURN NULL;
|
|
627
652
|
END IF;
|
|
653
|
+
${fkExpansion}
|
|
628
654
|
${m2mExpansion}
|
|
629
655
|
|
|
630
656
|
RETURN v_result;
|
package/src/cli/index.ts
CHANGED
|
@@ -67,8 +67,14 @@ async function main() {
|
|
|
67
67
|
|
|
68
68
|
// Phase 3: Generate SQL
|
|
69
69
|
const coreSQL = generateCoreSQL();
|
|
70
|
+
|
|
71
|
+
// Topologically sort entities by FK dependencies
|
|
72
|
+
// Entities must be created before entities that reference them
|
|
73
|
+
const sortedEntityNames = topologicalSortEntities(ir.entities);
|
|
74
|
+
|
|
70
75
|
const entitySQL: string[] = [];
|
|
71
|
-
for (const
|
|
76
|
+
for (const name of sortedEntityNames) {
|
|
77
|
+
const entityIR = ir.entities[name];
|
|
72
78
|
entitySQL.push(generateSchemaSQL(name, entityIR));
|
|
73
79
|
// Skip CRUD generation for unmanaged entities (e.g., junction tables)
|
|
74
80
|
if (entityIR.managed !== false) {
|
|
@@ -171,3 +177,71 @@ async function main() {
|
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
main();
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Topologically sort entities based on FK dependencies.
|
|
183
|
+
* Entities that are referenced by others come first.
|
|
184
|
+
* Uses Kahn's algorithm for topological sorting.
|
|
185
|
+
*/
|
|
186
|
+
function topologicalSortEntities(entities: Record<string, any>): string[] {
|
|
187
|
+
const entityNames = Object.keys(entities);
|
|
188
|
+
|
|
189
|
+
// Build dependency graph: entity -> entities it depends on (references)
|
|
190
|
+
const dependencies: Record<string, Set<string>> = {};
|
|
191
|
+
const dependents: Record<string, Set<string>> = {};
|
|
192
|
+
|
|
193
|
+
for (const name of entityNames) {
|
|
194
|
+
dependencies[name] = new Set();
|
|
195
|
+
dependents[name] = new Set();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Parse REFERENCES from column types
|
|
199
|
+
for (const name of entityNames) {
|
|
200
|
+
const entity = entities[name];
|
|
201
|
+
for (const col of entity.columns || []) {
|
|
202
|
+
const match = col.type?.match(/REFERENCES\s+(\w+)/i);
|
|
203
|
+
if (match) {
|
|
204
|
+
const referencedEntity = match[1];
|
|
205
|
+
// Only track dependencies to entities we're managing
|
|
206
|
+
if (entityNames.includes(referencedEntity)) {
|
|
207
|
+
dependencies[name].add(referencedEntity);
|
|
208
|
+
dependents[referencedEntity].add(name);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Kahn's algorithm
|
|
215
|
+
const result: string[] = [];
|
|
216
|
+
const noIncoming: string[] = [];
|
|
217
|
+
|
|
218
|
+
// Find entities with no dependencies (no incoming edges)
|
|
219
|
+
for (const name of entityNames) {
|
|
220
|
+
if (dependencies[name].size === 0) {
|
|
221
|
+
noIncoming.push(name);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
while (noIncoming.length > 0) {
|
|
226
|
+
const node = noIncoming.shift()!;
|
|
227
|
+
result.push(node);
|
|
228
|
+
|
|
229
|
+
// Remove this node from the graph
|
|
230
|
+
for (const dependent of dependents[node]) {
|
|
231
|
+
dependencies[dependent].delete(node);
|
|
232
|
+
if (dependencies[dependent].size === 0) {
|
|
233
|
+
noIncoming.push(dependent);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for cycles
|
|
239
|
+
if (result.length !== entityNames.length) {
|
|
240
|
+
const remaining = entityNames.filter(n => !result.includes(n));
|
|
241
|
+
console.warn(`[Compiler] Warning: Circular FK dependencies detected among: ${remaining.join(', ')}`);
|
|
242
|
+
// Add remaining entities anyway (they may have circular refs)
|
|
243
|
+
result.push(...remaining);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
}
|