Skip to content

05_02_08 - Query Interface

📋 Descripción General

Interfaz de consultas expresiva tipo GraphQL/Cypher para interrogar el grafo de dependencias sin necesidad de algoritmos complejos. Permite consultas declarativas en C++ con sintaxis fluida.

🎯 Objetivos

  1. API Fluida para consultas legibles tipo SQL
  2. Consultas complejas sin escribir algoritmos de grafos
  3. Performance O(V+E) para consultas típicas
  4. Type-safe en tiempo de compilación
  5. Composición de consultas reutilizables

🏗️ Arquitectura

QueryEngine
├── GraphQuery (fluent API builder)
├── QueryExecutor (execution engine)
├── Traversal Algorithms
│   ├── BreadthFirstTraversal
│   ├── DepthFirstTraversal
│   └── BidirectionalTraversal
├── Predicates
│   ├── NodePredicate
│   └── EdgePredicate
└── Result Aggregators
    ├── NodeSet
    ├── PathSet
    └── Statistics

🔍 Sintaxis de Consultas

Consultas Básicas

#include "query_engine.hpp"

using namespace audio_lab::dependency_graph;

Graph graph = /* ... */;

// ¿Qué depende de este módulo? (dependents)
auto dependents = GraphQuery(graph)
    .start_from("filter_biquad")
    .traverse_upstream()
    .get_nodes();

// ¿De qué depende este módulo? (dependencies)
auto dependencies = GraphQuery(graph)
    .start_from("voice_mono")
    .traverse_downstream()
    .get_nodes();

// ¿Todos los módulos L3_ENGINE?
auto engines = GraphQuery(graph)
    .select_all()
    .where_level(L3_ENGINE)
    .get_nodes();

Consultas con Filtros

// Filtros que consumen más de 100 CPU cycles
auto expensive_filters = GraphQuery(graph)
    .select_all()
    .where_category("FILTER")
    .where_cpu_greater_than(100)
    .get_nodes();

// Módulos en estado EXPERIMENTAL
auto experimental = GraphQuery(graph)
    .select_all()
    .where_status(ModuleStatus::EXPERIMENTAL)
    .get_nodes();

// Módulos con alta conectividad (degree > 5)
auto hubs = GraphQuery(graph)
    .select_all()
    .where([](const NodePtr& n) {
        return n->total_degree() > 5;
    })
    .get_nodes();

Consultas de Caminos

// ¿Camino más corto entre dos módulos?
auto path = GraphQuery(graph)
    .start_from("synth_engine")
    .find_path_to("osc_sine")
    .get_shortest_path();

// ¿Todos los caminos posibles?
auto all_paths = GraphQuery(graph)
    .start_from("synth_engine")
    .find_path_to("osc_sine")
    .get_all_paths();

// ¿Camino crítico por CPU?
auto critical = GraphQuery(graph)
    .start_from("synth_engine")
    .find_critical_path([](const NodePtr& n) {
        return n->cpu_cycles;
    })
    .get_path();

Consultas de Alcance

// ¿Todos los dependientes a cualquier profundidad?
auto all_dependents = GraphQuery(graph)
    .start_from("filter_biquad")
    .traverse_upstream()
    .recursive()  // Transitive closure
    .get_nodes();

// ¿Dependencias directas solamente?
auto direct_deps = GraphQuery(graph)
    .start_from("voice_mono")
    .traverse_downstream()
    .depth(1)  // Solo nivel 1
    .get_nodes();

// ¿Dependencias hasta nivel L1_ATOM?
auto deps_to_atoms = GraphQuery(graph)
    .start_from("synth_engine")
    .traverse_downstream()
    .until_level(L1_ATOM)
    .get_nodes();

Consultas de Análisis

// ¿Módulos que están en el camino crítico de X a Y?
auto critical_nodes = GraphQuery(graph)
    .start_from("synth_engine")
    .find_path_to("osc_sine")
    .get_critical_nodes();

// ¿Quién sería afectado si elimino este módulo?
auto impact = GraphQuery(graph)
    .start_from("filter_biquad")
    .traverse_upstream()
    .recursive()
    .get_impact_analysis();

// ¿Módulos aislados (sin conexiones)?
auto isolated = GraphQuery(graph)
    .select_all()
    .where([](const NodePtr& n) {
        return n->total_degree() == 0;
    })
    .get_nodes();

Consultas Compuestas

// Combinar múltiples condiciones
auto result = GraphQuery(graph)
    .select_all()
    .where_level(L2_CELL)
    .where_category("VOICE")
    .where_cpu_greater_than(150)
    .where([](const NodePtr& n) {
        return n->status == ModuleStatus::STABLE;
    })
    .get_nodes();

// Intersección de conjuntos
auto deps_of_A = GraphQuery(graph)
    .start_from("module_A")
    .traverse_downstream()
    .get_nodes();

auto deps_of_B = GraphQuery(graph)
    .start_from("module_B")
    .traverse_downstream()
    .get_nodes();

// Dependencias comunes
std::vector<NodePtr> common_deps;
std::set_intersection(
    deps_of_A.begin(), deps_of_A.end(),
    deps_of_B.begin(), deps_of_B.end(),
    std::back_inserter(common_deps)
);

📊 Casos de Uso Prácticos

1. Análisis de Impacto de Cambios

Pregunta: Si modifico filter_biquad, ¿qué módulos se verán afectados?

auto impact_report = GraphQuery(graph)
    .start_from("filter_biquad")
    .traverse_upstream()
    .recursive()
    .get_impact_analysis();

std::cout << "Módulos afectados: " << impact_report.affected_count << "\n";
std::cout << "CPU total afectado: " << impact_report.total_cpu_cycles << "\n";
std::cout << "Jerarquías afectadas:\n";
for (const auto& [level, count] : impact_report.by_hierarchy) {
    std::cout << "  " << to_string(level) << ": " << count << " módulos\n";
}

2. Detección de Cuellos de Botella

Pregunta: ¿Qué módulos tienen más dependientes (son cuellos de botella)?

auto bottlenecks = GraphQuery(graph)
    .select_all()
    .where([](const NodePtr& n) {
        return n->in_degree > 10;  // Más de 10 dependientes
    })
    .order_by([](const NodePtr& a, const NodePtr& b) {
        return a->in_degree > b->in_degree;
    })
    .limit(10)
    .get_nodes();

std::cout << "Top 10 cuellos de botella:\n";
for (const auto& node : bottlenecks) {
    std::cout << "  " << node->label << " (";
    std::cout << node->in_degree << " dependientes)\n";
}

3. Verificación de Dependencias Cíclicas

Pregunta: ¿Hay ciclos en las dependencias?

auto cycles = GraphQuery(graph)
    .select_all()
    .detect_cycles()
    .get_cycles();

if (!cycles.empty()) {
    std::cout << "⚠ Encontrados " << cycles.size() << " ciclos:\n";
    for (const auto& cycle : cycles) {
        std::cout << "  Ciclo: ";
        for (const auto& node_id : cycle.path) {
            std::cout << node_id << " → ";
        }
        std::cout << cycle.path.front() << "\n";
    }
} else {
    std::cout << "✓ No hay ciclos (DAG válido)\n";
}

4. Búsqueda de Alternativas

Pregunta: ¿Qué otros filtros puedo usar en lugar de filter_biquad?

auto biquad = graph.get_node("filter_biquad");

auto alternatives = GraphQuery(graph)
    .select_all()
    .where_category(biquad->category)  // Mismo tipo
    .where_level(biquad->level)        // Mismo nivel jerárquico
    .where([&](const NodePtr& n) {
        // CPU similar (±20%)
        return std::abs(int(n->cpu_cycles) - int(biquad->cpu_cycles))
               < biquad->cpu_cycles * 0.2;
    })
    .where([&](const NodePtr& n) {
        return n->id != biquad->id;  // Excluir el mismo
    })
    .get_nodes();

std::cout << "Alternativas a " << biquad->label << ":\n";
for (const auto& alt : alternatives) {
    std::cout << "  - " << alt->label;
    std::cout << " (CPU: " << alt->cpu_cycles << ")\n";
}

5. Optimización de Build Order

Pregunta: ¿En qué orden debo compilar los módulos?

auto build_order = GraphQuery(graph)
    .select_all()
    .topological_sort()
    .get_nodes();

std::cout << "Orden de compilación:\n";
for (size_t i = 0; i < build_order.size(); ++i) {
    std::cout << i+1 << ". " << build_order[i]->label << "\n";
}

6. Análisis de Modularidad

Pregunta: ¿Qué módulos L3 dependen de módulos L0 directamente (saltando L1/L2)?

// Encontrar violaciones de jerarquía
auto violations = GraphQuery(graph)
    .select_all()
    .where_level(L3_ENGINE)
    .where([&](const NodePtr& n) {
        auto deps = graph.get_dependencies(n->id);
        for (const auto& dep_id : deps) {
            auto dep = graph.get_node(dep_id);
            if (dep && dep->level == L0_KERNEL) {
                return true;  // L3 → L0 directo (violación)
            }
        }
        return false;
    })
    .get_nodes();

if (!violations.empty()) {
    std::cout << "⚠ Violaciones de jerarquía:\n";
    for (const auto& node : violations) {
        std::cout << "  " << node->label << " (L3) depende directamente de L0\n";
    }
}

🎨 API Fluida Completa

Métodos de Inicio

  • select_all() - Seleccionar todos los nodos
  • start_from(id) - Comenzar desde un nodo específico
  • start_from_multiple(ids) - Comenzar desde múltiples nodos

Métodos de Filtrado

  • where(predicate) - Filtro personalizado
  • where_level(level) - Filtrar por nivel jerárquico
  • where_category(category) - Filtrar por categoría
  • where_status(status) - Filtrar por estado
  • where_cpu_greater_than(cpu) - CPU > valor
  • where_cpu_less_than(cpu) - CPU < valor
  • where_latency_greater_than(lat) - Latencia > valor
  • where_version(version) - Versión específica

Métodos de Traversal

  • traverse_upstream() - Hacia dependientes (quien usa esto)
  • traverse_downstream() - Hacia dependencias (qué usa esto)
  • traverse_both() - Ambas direcciones
  • depth(n) - Limitar profundidad
  • recursive() - Transitive closure (sin límite)
  • until_level(level) - Hasta alcanzar nivel jerárquico
  • until(predicate) - Hasta cumplir condición

Métodos de Búsqueda de Caminos

  • find_path_to(target) - Buscar caminos
  • get_shortest_path() - Camino más corto
  • get_all_paths() - Todos los caminos
  • get_critical_path() - Camino crítico (por peso)

Métodos de Análisis

  • detect_cycles() - Detectar ciclos
  • topological_sort() - Ordenamiento topológico
  • get_impact_analysis() - Análisis de impacto
  • get_statistics() - Estadísticas del resultado

Métodos de Ordenamiento

  • order_by(comparator) - Ordenar por función
  • order_by_cpu() - Ordenar por CPU
  • order_by_degree() - Ordenar por conectividad
  • limit(n) - Limitar resultados

Métodos de Resultado

  • get_nodes() - Obtener nodos
  • get_edges() - Obtener aristas
  • get_paths() - Obtener caminos
  • get_count() - Contar resultados
  • get_first() - Primer resultado
  • exists() - ¿Existe algún resultado?

📐 Predicados Predefinidos

namespace Predicates {
    // Predicados de nivel
    auto is_kernel = [](const NodePtr& n) {
        return n->level == L0_KERNEL;
    };
    auto is_atom = [](const NodePtr& n) {
        return n->level == L1_ATOM;
    };
    auto is_cell = [](const NodePtr& n) {
        return n->level == L2_CELL;
    };
    auto is_engine = [](const NodePtr& n) {
        return n->level == L3_ENGINE;
    };

    // Predicados de estado
    auto is_stable = [](const NodePtr& n) {
        return n->status == ModuleStatus::STABLE;
    };
    auto is_experimental = [](const NodePtr& n) {
        return n->status == ModuleStatus::EXPERIMENTAL;
    };

    // Predicados de performance
    auto is_expensive(uint32_t threshold) {
        return [threshold](const NodePtr& n) {
            return n->cpu_cycles > threshold;
        };
    }
    auto has_latency = [](const NodePtr& n) {
        return n->latency_samples > 0;
    };

    // Predicados de conectividad
    auto is_hub(uint32_t min_degree) {
        return [min_degree](const NodePtr& n) {
            return n->total_degree() >= min_degree;
        };
    }
    auto is_source = [](const NodePtr& n) {
        return n->in_degree == 0;
    };
    auto is_sink = [](const NodePtr& n) {
        return n->out_degree == 0;
    };
}

🚀 Performance

Complejidad de Consultas

Operación Complejidad Notas
select_all() O(1) Referencia, no copia
where(predicate) O(V) Escaneo lineal
traverse(depth=1) O(V) Adyacencia directa
traverse(recursive) O(V+E) BFS/DFS completo
find_shortest_path O(V+E) BFS
topological_sort O(V+E) DFS
detect_cycles O(V+E) DFS con colores

Optimizaciones

  1. Lazy Evaluation: Las consultas se ejecutan solo al llamar get_*()
  2. Early Termination: Filtros aplicados durante traversal
  3. Index Lookup: Uso de índices bidireccionales O(1)
  4. Result Caching: Resultados cacheados para reutilización
  5. Move Semantics: Evita copias innecesarias

🔗 Integración con Otros Módulos

Con Filtros (TAREA 6)

// Combinar FilterChain con Query
auto filter = FilterChain()
    .by_level({L2_CELL, L3_ENGINE})
    .cpu_greater_than(100);

Graph filtered = filter.apply(graph);

auto result = GraphQuery(filtered)
    .select_all()
    .topological_sort()
    .get_nodes();

Con Path Analysis (TAREA 3)

// Query devuelve nodos, Path Analyzer calcula métricas
auto critical_nodes = GraphQuery(graph)
    .start_from("synth_engine")
    .find_critical_path()
    .get_nodes();

CriticalPathAnalyzer analyzer(graph);
auto metrics = analyzer.calculate_path_metrics(critical_nodes);

Con Metrics (TAREA 5)

// Query + Metrics para encontrar nodos importantes
MetricsEngine metrics(graph);
metrics.compute_all();

auto important = GraphQuery(graph)
    .select_all()
    .where([&](const NodePtr& n) {
        return n->betweenness > 0.5;  // Alta centralidad
    })
    .order_by([](const NodePtr& a, const NodePtr& b) {
        return a->betweenness > b->betweenness;
    })
    .get_nodes();

📚 Referencias

  • GraphQL: Inspiración para API declarativa
  • Cypher (Neo4j): Lenguaje de consulta de grafos
  • LINQ (C#): Fluent API para consultas
  • jQuery: Encadenamiento de métodos
  • Range-v3: Composición de operaciones en C++

Parte de: 05_02_DEPENDENCY_GRAPH - Dependency Graph Visualizer Requiere: graph.hpp Exporta: query_engine.hpp