dzql 0.6.20 → 0.6.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dzql",
3
- "version": "0.6.20",
3
+ "version": "0.6.22",
4
4
  "description": "Database-first real-time framework with TypeScript support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -203,14 +203,32 @@ function buildFinalExistsCheck(finalHop: string, valueExpr: string): string {
203
203
  conditions.push(`${targetTable}.id = ${valueExpr}`);
204
204
  }
205
205
 
206
- // Temporal/active condition
206
+ // Temporal/active condition - may have multiple comma-separated parts
207
+ // e.g., {active} or {role='admin'} or {active,role=admin}
207
208
  if (condition) {
208
- // Handle simple boolean condition like {active}
209
- if (condition.match(/^[a-zA-Z0-9_]+$/)) {
210
- conditions.push(`${targetTable}.${condition} = true`);
211
- } else {
212
- // Handle complex condition like {role='admin'}
213
- conditions.push(`${targetTable}.${condition}`);
209
+ // Split by comma for multiple conditions
210
+ const conditionParts = condition.split(',').map(c => c.trim());
211
+ for (const part of conditionParts) {
212
+ // Handle simple boolean condition like {active}
213
+ if (part.match(/^[a-zA-Z0-9_]+$/)) {
214
+ conditions.push(`${targetTable}.${part} = true`);
215
+ } else {
216
+ // Handle complex condition like role='admin' or role=admin
217
+ // Convert role=admin to role = 'admin' for proper SQL
218
+ const eqMatch = part.match(/^([a-zA-Z0-9_]+)=(.+)$/);
219
+ if (eqMatch) {
220
+ const field = eqMatch[1];
221
+ let value = eqMatch[2];
222
+ // If value is already quoted, use as-is; otherwise quote it
223
+ if (!value.startsWith("'") && !value.match(/^\d+$/)) {
224
+ value = `'${value}'`;
225
+ }
226
+ conditions.push(`${targetTable}.${field} = ${value}`);
227
+ } else {
228
+ // Fallback: use as-is (e.g., complex expression)
229
+ conditions.push(`${targetTable}.${part}`);
230
+ }
231
+ }
214
232
  }
215
233
  }
216
234
 
@@ -224,4 +242,4 @@ function buildFinalExistsCheck(finalHop: string, valueExpr: string): string {
224
242
  const whereClause = conditions.join(' AND ');
225
243
 
226
244
  return `EXISTS (SELECT 1 FROM ${targetTable} WHERE ${whereClause})`;
227
- }
245
+ }
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 [name, entityIR] of Object.entries(ir.entities)) {
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
+ }