aiecs 1.5.1__py3-none-any.whl
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.
- aiecs/__init__.py +72 -0
- aiecs/__main__.py +41 -0
- aiecs/aiecs_client.py +469 -0
- aiecs/application/__init__.py +10 -0
- aiecs/application/executors/__init__.py +10 -0
- aiecs/application/executors/operation_executor.py +363 -0
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +375 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +356 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +531 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +443 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +319 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +100 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +327 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +349 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +244 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +23 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +387 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +343 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +580 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +189 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +344 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +378 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +199 -0
- aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
- aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
- aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +347 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +504 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +167 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +630 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +654 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +477 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +390 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +217 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +169 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +872 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +554 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +19 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +596 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +423 -0
- aiecs/application/knowledge_graph/search/reranker.py +295 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +553 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +398 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +329 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +269 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +189 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +321 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +484 -0
- aiecs/config/__init__.py +16 -0
- aiecs/config/config.py +498 -0
- aiecs/config/graph_config.py +137 -0
- aiecs/config/registry.py +23 -0
- aiecs/core/__init__.py +46 -0
- aiecs/core/interface/__init__.py +34 -0
- aiecs/core/interface/execution_interface.py +152 -0
- aiecs/core/interface/storage_interface.py +171 -0
- aiecs/domain/__init__.py +289 -0
- aiecs/domain/agent/__init__.py +189 -0
- aiecs/domain/agent/base_agent.py +697 -0
- aiecs/domain/agent/exceptions.py +103 -0
- aiecs/domain/agent/graph_aware_mixin.py +559 -0
- aiecs/domain/agent/hybrid_agent.py +490 -0
- aiecs/domain/agent/integration/__init__.py +26 -0
- aiecs/domain/agent/integration/context_compressor.py +222 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +252 -0
- aiecs/domain/agent/integration/retry_policy.py +219 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +646 -0
- aiecs/domain/agent/lifecycle.py +296 -0
- aiecs/domain/agent/llm_agent.py +300 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +197 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +160 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +90 -0
- aiecs/domain/agent/models.py +317 -0
- aiecs/domain/agent/observability.py +407 -0
- aiecs/domain/agent/persistence.py +289 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +161 -0
- aiecs/domain/agent/prompts/formatters.py +189 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +260 -0
- aiecs/domain/agent/tool_agent.py +257 -0
- aiecs/domain/agent/tools/__init__.py +12 -0
- aiecs/domain/agent/tools/schema_generator.py +221 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +477 -0
- aiecs/domain/community/analytics.py +481 -0
- aiecs/domain/community/collaborative_workflow.py +642 -0
- aiecs/domain/community/communication_hub.py +645 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +800 -0
- aiecs/domain/community/community_manager.py +813 -0
- aiecs/domain/community/decision_engine.py +879 -0
- aiecs/domain/community/exceptions.py +225 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +268 -0
- aiecs/domain/community/resource_manager.py +457 -0
- aiecs/domain/community/shared_context_manager.py +603 -0
- aiecs/domain/context/__init__.py +58 -0
- aiecs/domain/context/context_engine.py +989 -0
- aiecs/domain/context/conversation_models.py +354 -0
- aiecs/domain/context/graph_memory.py +467 -0
- aiecs/domain/execution/__init__.py +12 -0
- aiecs/domain/execution/model.py +57 -0
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +130 -0
- aiecs/domain/knowledge_graph/models/evidence.py +194 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +186 -0
- aiecs/domain/knowledge_graph/models/path.py +179 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +173 -0
- aiecs/domain/knowledge_graph/models/query.py +272 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +187 -0
- aiecs/domain/knowledge_graph/models/relation.py +136 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +135 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +271 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +155 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +171 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +496 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +205 -0
- aiecs/domain/task/__init__.py +13 -0
- aiecs/domain/task/dsl_processor.py +613 -0
- aiecs/domain/task/model.py +62 -0
- aiecs/domain/task/task_context.py +268 -0
- aiecs/infrastructure/__init__.py +24 -0
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +601 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +449 -0
- aiecs/infrastructure/graph_storage/cache.py +429 -0
- aiecs/infrastructure/graph_storage/distributed.py +226 -0
- aiecs/infrastructure/graph_storage/error_handling.py +390 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +306 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +514 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +483 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +410 -0
- aiecs/infrastructure/graph_storage/metrics.py +357 -0
- aiecs/infrastructure/graph_storage/migration.py +413 -0
- aiecs/infrastructure/graph_storage/pagination.py +471 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +466 -0
- aiecs/infrastructure/graph_storage/postgres.py +871 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +635 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +623 -0
- aiecs/infrastructure/graph_storage/streaming.py +495 -0
- aiecs/infrastructure/messaging/__init__.py +13 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +383 -0
- aiecs/infrastructure/messaging/websocket_manager.py +298 -0
- aiecs/infrastructure/monitoring/__init__.py +34 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +174 -0
- aiecs/infrastructure/monitoring/global_metrics_manager.py +213 -0
- aiecs/infrastructure/monitoring/structured_logger.py +48 -0
- aiecs/infrastructure/monitoring/tracing_manager.py +410 -0
- aiecs/infrastructure/persistence/__init__.py +24 -0
- aiecs/infrastructure/persistence/context_engine_client.py +187 -0
- aiecs/infrastructure/persistence/database_manager.py +333 -0
- aiecs/infrastructure/persistence/file_storage.py +754 -0
- aiecs/infrastructure/persistence/redis_client.py +220 -0
- aiecs/llm/__init__.py +86 -0
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/callbacks/custom_callbacks.py +264 -0
- aiecs/llm/client_factory.py +420 -0
- aiecs/llm/clients/__init__.py +33 -0
- aiecs/llm/clients/base_client.py +193 -0
- aiecs/llm/clients/googleai_client.py +181 -0
- aiecs/llm/clients/openai_client.py +131 -0
- aiecs/llm/clients/vertex_client.py +437 -0
- aiecs/llm/clients/xai_client.py +184 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +275 -0
- aiecs/llm/config/config_validator.py +236 -0
- aiecs/llm/config/model_config.py +151 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +91 -0
- aiecs/main.py +363 -0
- aiecs/scripts/__init__.py +3 -0
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +97 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/version_manager.py +215 -0
- aiecs/scripts/dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md +242 -0
- aiecs/scripts/dependance_check/README_DEPENDENCY_CHECKER.md +310 -0
- aiecs/scripts/dependance_check/__init__.py +17 -0
- aiecs/scripts/dependance_check/dependency_checker.py +938 -0
- aiecs/scripts/dependance_check/dependency_fixer.py +391 -0
- aiecs/scripts/dependance_check/download_nlp_data.py +396 -0
- aiecs/scripts/dependance_check/quick_dependency_check.py +270 -0
- aiecs/scripts/dependance_check/setup_nlp_data.sh +217 -0
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/README_WEASEL_PATCH.md +126 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/dependance_patch/fix_weasel/fix_weasel_validator.py +128 -0
- aiecs/scripts/dependance_patch/fix_weasel/fix_weasel_validator.sh +82 -0
- aiecs/scripts/dependance_patch/fix_weasel/patch_weasel_library.sh +188 -0
- aiecs/scripts/dependance_patch/fix_weasel/run_weasel_patch.sh +41 -0
- aiecs/scripts/tools_develop/README.md +449 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +259 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +422 -0
- aiecs/scripts/tools_develop/verify_tools.py +356 -0
- aiecs/tasks/__init__.py +1 -0
- aiecs/tasks/worker.py +172 -0
- aiecs/tools/__init__.py +299 -0
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +381 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +413 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +388 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +303 -0
- aiecs/tools/apisource/providers/__init__.py +115 -0
- aiecs/tools/apisource/providers/base.py +664 -0
- aiecs/tools/apisource/providers/census.py +401 -0
- aiecs/tools/apisource/providers/fred.py +564 -0
- aiecs/tools/apisource/providers/newsapi.py +412 -0
- aiecs/tools/apisource/providers/worldbank.py +357 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +375 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +391 -0
- aiecs/tools/apisource/tool.py +850 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +338 -0
- aiecs/tools/base_tool.py +201 -0
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +599 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2403 -0
- aiecs/tools/docs/content_insertion_tool.py +1333 -0
- aiecs/tools/docs/document_creator_tool.py +1317 -0
- aiecs/tools/docs/document_layout_tool.py +1166 -0
- aiecs/tools/docs/document_parser_tool.py +994 -0
- aiecs/tools/docs/document_writer_tool.py +1818 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +734 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +923 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +476 -0
- aiecs/tools/langchain_adapter.py +542 -0
- aiecs/tools/schema_generator.py +275 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +589 -0
- aiecs/tools/search_tool/cache.py +260 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +216 -0
- aiecs/tools/search_tool/core.py +749 -0
- aiecs/tools/search_tool/deduplicator.py +123 -0
- aiecs/tools/search_tool/error_handler.py +271 -0
- aiecs/tools/search_tool/metrics.py +371 -0
- aiecs/tools/search_tool/rate_limiter.py +178 -0
- aiecs/tools/search_tool/schemas.py +277 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +643 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +505 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +694 -0
- aiecs/tools/statistics/data_loader_tool.py +564 -0
- aiecs/tools/statistics/data_profiler_tool.py +658 -0
- aiecs/tools/statistics/data_transformer_tool.py +573 -0
- aiecs/tools/statistics/data_visualizer_tool.py +495 -0
- aiecs/tools/statistics/model_trainer_tool.py +487 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +459 -0
- aiecs/tools/task_tools/__init__.py +86 -0
- aiecs/tools/task_tools/chart_tool.py +732 -0
- aiecs/tools/task_tools/classfire_tool.py +922 -0
- aiecs/tools/task_tools/image_tool.py +447 -0
- aiecs/tools/task_tools/office_tool.py +684 -0
- aiecs/tools/task_tools/pandas_tool.py +635 -0
- aiecs/tools/task_tools/report_tool.py +635 -0
- aiecs/tools/task_tools/research_tool.py +392 -0
- aiecs/tools/task_tools/scraper_tool.py +715 -0
- aiecs/tools/task_tools/stats_tool.py +688 -0
- aiecs/tools/temp_file_manager.py +130 -0
- aiecs/tools/tool_executor/__init__.py +37 -0
- aiecs/tools/tool_executor/tool_executor.py +881 -0
- aiecs/utils/LLM_output_structor.py +445 -0
- aiecs/utils/__init__.py +34 -0
- aiecs/utils/base_callback.py +47 -0
- aiecs/utils/cache_provider.py +695 -0
- aiecs/utils/execution_utils.py +184 -0
- aiecs/utils/logging.py +1 -0
- aiecs/utils/prompt_loader.py +14 -0
- aiecs/utils/token_usage_repository.py +323 -0
- aiecs/ws/__init__.py +0 -0
- aiecs/ws/socket_server.py +52 -0
- aiecs-1.5.1.dist-info/METADATA +608 -0
- aiecs-1.5.1.dist-info/RECORD +302 -0
- aiecs-1.5.1.dist-info/WHEEL +5 -0
- aiecs-1.5.1.dist-info/entry_points.txt +10 -0
- aiecs-1.5.1.dist-info/licenses/LICENSE +225 -0
- aiecs-1.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Node Definitions for Logic Query Parser
|
|
3
|
+
|
|
4
|
+
This module defines the Abstract Syntax Tree (AST) node hierarchy for the Logic Query DSL.
|
|
5
|
+
Each node is self-contained and responsible for its own validation and conversion to QueryPlan.
|
|
6
|
+
|
|
7
|
+
Design Principles:
|
|
8
|
+
1. Self-Contained: Each node owns its complete structure (e.g., FindNode has filters)
|
|
9
|
+
2. Polymorphic Conversion: Each node implements its own to_query_plan() method
|
|
10
|
+
3. Immutable: Nodes are frozen dataclasses (cannot be modified after creation)
|
|
11
|
+
4. Type-Safe: Full type hints for all fields and methods
|
|
12
|
+
|
|
13
|
+
Phase: 2.4 - Logic Query Parser
|
|
14
|
+
Version: 1.0
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .query_context import QueryContext
|
|
24
|
+
|
|
25
|
+
# Import QueryPlan models
|
|
26
|
+
try:
|
|
27
|
+
from aiecs.domain.knowledge_graph.models.query_plan import (
|
|
28
|
+
QueryPlan,
|
|
29
|
+
QueryStep,
|
|
30
|
+
QueryOperation,
|
|
31
|
+
)
|
|
32
|
+
from aiecs.domain.knowledge_graph.models.query import GraphQuery, QueryType
|
|
33
|
+
|
|
34
|
+
QUERY_PLAN_AVAILABLE = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
QUERY_PLAN_AVAILABLE = False
|
|
37
|
+
QueryPlan = None
|
|
38
|
+
QueryStep = None
|
|
39
|
+
QueryOperation = None
|
|
40
|
+
GraphQuery = None
|
|
41
|
+
QueryType = None
|
|
42
|
+
|
|
43
|
+
# Placeholder for ValidationError (will be defined in error_handler.py)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ValidationError:
|
|
48
|
+
"""Validation error with location information"""
|
|
49
|
+
|
|
50
|
+
line: int
|
|
51
|
+
column: int
|
|
52
|
+
message: str
|
|
53
|
+
suggestion: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class ASTNode(ABC):
|
|
58
|
+
"""
|
|
59
|
+
Base class for all AST nodes
|
|
60
|
+
|
|
61
|
+
All AST nodes must:
|
|
62
|
+
1. Store line/column metadata for error reporting
|
|
63
|
+
2. Implement validate() for semantic validation
|
|
64
|
+
3. Implement conversion to query plan (via to_query_plan or to_filter_dict)
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
line: Line number in source query (1-based)
|
|
68
|
+
column: Column number in source query (1-based)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
line: int
|
|
72
|
+
column: int
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
76
|
+
"""
|
|
77
|
+
Validate this node against the schema
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
schema: SchemaManager instance for validation
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of validation errors (empty if valid)
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class QueryNode(ASTNode):
|
|
89
|
+
"""
|
|
90
|
+
Top-level query node: Find + optional Traversals + optional WHERE
|
|
91
|
+
|
|
92
|
+
Represents a complete query with:
|
|
93
|
+
- Required: FindNode for entity selection
|
|
94
|
+
- Optional: List of TraversalNodes for graph navigation
|
|
95
|
+
- Optional: WHERE clause (embedded in FindNode.filters)
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
Find(Person) FOLLOWS AuthoredBy WHERE year > 2020
|
|
99
|
+
|
|
100
|
+
QueryNode(
|
|
101
|
+
find=FindNode(entity_type="Person", ...),
|
|
102
|
+
traversals=[TraversalNode(relation_type="AuthoredBy", ...)],
|
|
103
|
+
...
|
|
104
|
+
)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
find: "FindNode"
|
|
108
|
+
traversals: List["TraversalNode"] = field(default_factory=list)
|
|
109
|
+
|
|
110
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
111
|
+
"""Validate all parts of the query"""
|
|
112
|
+
errors = []
|
|
113
|
+
errors.extend(self.find.validate(schema))
|
|
114
|
+
for traversal in self.traversals:
|
|
115
|
+
errors.extend(traversal.validate(schema))
|
|
116
|
+
return errors
|
|
117
|
+
|
|
118
|
+
def to_query_plan(self, context: "QueryContext", original_query: str = "") -> Any:
|
|
119
|
+
"""
|
|
120
|
+
Convert QueryNode to QueryPlan
|
|
121
|
+
|
|
122
|
+
Creates a QueryPlan with multiple steps for complex queries with traversals.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
context: Query context for variable resolution
|
|
126
|
+
original_query: Original query string for documentation
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
QueryPlan with one or more QuerySteps
|
|
130
|
+
"""
|
|
131
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
132
|
+
raise ImportError("QueryPlan models not available")
|
|
133
|
+
|
|
134
|
+
# Generate plan ID
|
|
135
|
+
plan_id = f"plan_{uuid.uuid4().hex[:8]}"
|
|
136
|
+
|
|
137
|
+
# Convert to query steps
|
|
138
|
+
steps = self.to_query_steps(context)
|
|
139
|
+
|
|
140
|
+
# Create explanation
|
|
141
|
+
explanation = self._generate_explanation()
|
|
142
|
+
|
|
143
|
+
# Create QueryPlan
|
|
144
|
+
plan = QueryPlan(
|
|
145
|
+
plan_id=plan_id,
|
|
146
|
+
original_query=original_query or str(self),
|
|
147
|
+
steps=steps,
|
|
148
|
+
explanation=explanation,
|
|
149
|
+
optimized=False,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Calculate total cost
|
|
153
|
+
plan.total_estimated_cost = plan.calculate_total_cost()
|
|
154
|
+
|
|
155
|
+
return plan
|
|
156
|
+
|
|
157
|
+
def to_query_steps(self, context: "QueryContext") -> List[Any]:
|
|
158
|
+
"""
|
|
159
|
+
Convert QueryNode to list of QuerySteps
|
|
160
|
+
|
|
161
|
+
For simple queries (Find only), creates a single step.
|
|
162
|
+
For complex queries (Find + Traversals), creates multiple steps.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
context: Query context for variable resolution
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List of QueryStep objects
|
|
169
|
+
"""
|
|
170
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
171
|
+
raise ImportError("QueryPlan models not available")
|
|
172
|
+
|
|
173
|
+
steps = []
|
|
174
|
+
|
|
175
|
+
# Step 1: Find entities (always present)
|
|
176
|
+
find_step = self.find.to_query_step(context, step_id="step_1")
|
|
177
|
+
steps.append(find_step)
|
|
178
|
+
|
|
179
|
+
# Steps 2+: Traversals (if any)
|
|
180
|
+
for i, traversal in enumerate(self.traversals, start=2):
|
|
181
|
+
step_id = f"step_{i}"
|
|
182
|
+
depends_on = [f"step_{i-1}"] # Each step depends on previous
|
|
183
|
+
traversal_step = traversal.to_query_step(
|
|
184
|
+
context, step_id=step_id, depends_on=depends_on
|
|
185
|
+
)
|
|
186
|
+
steps.append(traversal_step)
|
|
187
|
+
|
|
188
|
+
return steps
|
|
189
|
+
|
|
190
|
+
def _generate_explanation(self) -> str:
|
|
191
|
+
"""Generate human-readable explanation of the query"""
|
|
192
|
+
parts = [f"Find {self.find.entity_type} entities"]
|
|
193
|
+
|
|
194
|
+
if self.find.entity_name:
|
|
195
|
+
parts.append(f"named '{self.find.entity_name}'")
|
|
196
|
+
|
|
197
|
+
if self.find.filters:
|
|
198
|
+
parts.append(f"with {len(self.find.filters)} filter(s)")
|
|
199
|
+
|
|
200
|
+
if self.traversals:
|
|
201
|
+
parts.append(f"then traverse {len(self.traversals)} relation(s)")
|
|
202
|
+
|
|
203
|
+
return " ".join(parts)
|
|
204
|
+
|
|
205
|
+
def __repr__(self) -> str:
|
|
206
|
+
"""String representation for debugging"""
|
|
207
|
+
traversals_str = f", traversals={len(self.traversals)}" if self.traversals else ""
|
|
208
|
+
return f"QueryNode(find={self.find}{traversals_str})"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass(frozen=True)
|
|
212
|
+
class FindNode(ASTNode):
|
|
213
|
+
"""
|
|
214
|
+
Entity selection node: Find(EntityType) or Find(EntityType[`Name`])
|
|
215
|
+
|
|
216
|
+
Represents entity selection with optional filters.
|
|
217
|
+
This node is self-contained and owns its filters.
|
|
218
|
+
|
|
219
|
+
Attributes:
|
|
220
|
+
entity_type: Type of entity to find (e.g., "Person", "Paper")
|
|
221
|
+
entity_name: Optional specific entity name (e.g., "Alice")
|
|
222
|
+
filters: List of filter nodes (from WHERE clause)
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
Find(Person[`Alice`]) WHERE age > 30
|
|
226
|
+
|
|
227
|
+
FindNode(
|
|
228
|
+
entity_type="Person",
|
|
229
|
+
entity_name="Alice",
|
|
230
|
+
filters=[PropertyFilterNode(property="age", operator=">", value=30)]
|
|
231
|
+
)
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
entity_type: str
|
|
235
|
+
entity_name: Optional[str] = None
|
|
236
|
+
filters: List["FilterNode"] = field(default_factory=list)
|
|
237
|
+
|
|
238
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
239
|
+
"""Validate entity type and all filters"""
|
|
240
|
+
errors = []
|
|
241
|
+
|
|
242
|
+
# Validate entity type exists (if schema has this method)
|
|
243
|
+
if hasattr(schema, "has_entity_type"):
|
|
244
|
+
if not schema.has_entity_type(self.entity_type):
|
|
245
|
+
errors.append(
|
|
246
|
+
ValidationError(
|
|
247
|
+
self.line,
|
|
248
|
+
self.column,
|
|
249
|
+
f"Entity type '{self.entity_type}' not found",
|
|
250
|
+
suggestion=f"Available types: {', '.join(schema.get_entity_types())}",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Validate all filters
|
|
255
|
+
for filter_node in self.filters:
|
|
256
|
+
errors.extend(filter_node.validate(schema))
|
|
257
|
+
|
|
258
|
+
return errors
|
|
259
|
+
|
|
260
|
+
def to_query_step(
|
|
261
|
+
self,
|
|
262
|
+
context: "QueryContext",
|
|
263
|
+
step_id: str = "step_1",
|
|
264
|
+
depends_on: List[str] = None,
|
|
265
|
+
) -> Any:
|
|
266
|
+
"""
|
|
267
|
+
Convert FindNode to QueryStep
|
|
268
|
+
|
|
269
|
+
Creates a QueryStep for entity lookup/filter operation.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
context: Query context for variable resolution
|
|
273
|
+
step_id: Unique identifier for this step
|
|
274
|
+
depends_on: List of step IDs this step depends on
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
QueryStep for entity lookup/filter
|
|
278
|
+
"""
|
|
279
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
280
|
+
raise ImportError("QueryPlan models not available")
|
|
281
|
+
|
|
282
|
+
# Build property filters
|
|
283
|
+
properties = {}
|
|
284
|
+
if self.filters:
|
|
285
|
+
# Combine all filters into a single filter dict
|
|
286
|
+
for filter_node in self.filters:
|
|
287
|
+
filter_dict = filter_node.to_filter_dict(context)
|
|
288
|
+
properties.update(filter_dict)
|
|
289
|
+
|
|
290
|
+
# Determine query type and operation
|
|
291
|
+
# If entity_name is provided, it's an entity lookup
|
|
292
|
+
# Otherwise, it's a filter operation
|
|
293
|
+
if self.entity_name:
|
|
294
|
+
query_type = QueryType.ENTITY_LOOKUP
|
|
295
|
+
operation = QueryOperation.ENTITY_LOOKUP
|
|
296
|
+
else:
|
|
297
|
+
# For filter operations, we use ENTITY_LOOKUP query type with
|
|
298
|
+
# filters
|
|
299
|
+
query_type = QueryType.ENTITY_LOOKUP
|
|
300
|
+
operation = QueryOperation.FILTER
|
|
301
|
+
|
|
302
|
+
# Create GraphQuery
|
|
303
|
+
query = GraphQuery(
|
|
304
|
+
query_type=query_type,
|
|
305
|
+
entity_type=self.entity_type,
|
|
306
|
+
entity_id=self.entity_name, # If specific entity name is provided
|
|
307
|
+
properties=properties,
|
|
308
|
+
max_results=100, # Default limit
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Create description
|
|
312
|
+
description = f"Find {self.entity_type} entities"
|
|
313
|
+
if self.entity_name:
|
|
314
|
+
description += f" named '{self.entity_name}'"
|
|
315
|
+
if self.filters:
|
|
316
|
+
description += f" with {len(self.filters)} filter(s)"
|
|
317
|
+
|
|
318
|
+
# Create QueryStep
|
|
319
|
+
step = QueryStep(
|
|
320
|
+
step_id=step_id,
|
|
321
|
+
operation=operation,
|
|
322
|
+
query=query,
|
|
323
|
+
depends_on=depends_on or [],
|
|
324
|
+
description=description,
|
|
325
|
+
estimated_cost=0.3, # Low cost for simple entity lookup
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return step
|
|
329
|
+
|
|
330
|
+
def __repr__(self) -> str:
|
|
331
|
+
"""String representation for debugging"""
|
|
332
|
+
name_str = f"[`{self.entity_name}`]" if self.entity_name else ""
|
|
333
|
+
filters_str = f", filters={len(self.filters)}" if self.filters else ""
|
|
334
|
+
return f"FindNode({self.entity_type}{name_str}{filters_str})"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@dataclass(frozen=True)
|
|
338
|
+
class TraversalNode(ASTNode):
|
|
339
|
+
"""
|
|
340
|
+
Graph traversal node: FOLLOWS RelationType [direction]
|
|
341
|
+
|
|
342
|
+
Represents navigation along graph relationships.
|
|
343
|
+
|
|
344
|
+
Attributes:
|
|
345
|
+
relation_type: Type of relation to follow (e.g., "AuthoredBy")
|
|
346
|
+
direction: Direction of traversal ("outgoing", "incoming", or None for default)
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
FOLLOWS AuthoredBy INCOMING
|
|
350
|
+
|
|
351
|
+
TraversalNode(
|
|
352
|
+
relation_type="AuthoredBy",
|
|
353
|
+
direction="incoming"
|
|
354
|
+
)
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
relation_type: str
|
|
358
|
+
direction: Optional[str] = "outgoing" # "incoming" | "outgoing" | None
|
|
359
|
+
|
|
360
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
361
|
+
"""Validate relation type exists"""
|
|
362
|
+
errors = []
|
|
363
|
+
|
|
364
|
+
# Validate relation type exists (if schema has this method)
|
|
365
|
+
if hasattr(schema, "has_relation_type"):
|
|
366
|
+
if not schema.has_relation_type(self.relation_type):
|
|
367
|
+
errors.append(
|
|
368
|
+
ValidationError(
|
|
369
|
+
self.line,
|
|
370
|
+
self.column,
|
|
371
|
+
f"Relation type '{self.relation_type}' not found",
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Validate direction
|
|
376
|
+
if self.direction and self.direction not in ["incoming", "outgoing"]:
|
|
377
|
+
errors.append(
|
|
378
|
+
ValidationError(
|
|
379
|
+
self.line,
|
|
380
|
+
self.column,
|
|
381
|
+
f"Invalid direction '{self.direction}'. Must be 'incoming' or 'outgoing'",
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return errors
|
|
386
|
+
|
|
387
|
+
def to_query_step(self, context: "QueryContext", step_id: str, depends_on: List[str]) -> Any:
|
|
388
|
+
"""
|
|
389
|
+
Convert TraversalNode to QueryStep
|
|
390
|
+
|
|
391
|
+
Creates a QueryStep for graph traversal operation.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
context: Query context for variable resolution
|
|
395
|
+
step_id: Unique identifier for this step
|
|
396
|
+
depends_on: List of step IDs this step depends on
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
QueryStep for graph traversal
|
|
400
|
+
"""
|
|
401
|
+
if not QUERY_PLAN_AVAILABLE:
|
|
402
|
+
raise ImportError("QueryPlan models not available")
|
|
403
|
+
|
|
404
|
+
# Create GraphQuery for traversal
|
|
405
|
+
query = GraphQuery(
|
|
406
|
+
query_type=QueryType.TRAVERSAL,
|
|
407
|
+
relation_type=self.relation_type,
|
|
408
|
+
max_depth=1, # Single hop traversal
|
|
409
|
+
max_results=100, # Default limit
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Create description
|
|
413
|
+
direction_str = self.direction.upper() if self.direction else "OUTGOING"
|
|
414
|
+
description = f"Traverse {self.relation_type} relation ({direction_str})"
|
|
415
|
+
|
|
416
|
+
# Create QueryStep
|
|
417
|
+
step = QueryStep(
|
|
418
|
+
step_id=step_id,
|
|
419
|
+
operation=QueryOperation.TRAVERSAL,
|
|
420
|
+
query=query,
|
|
421
|
+
depends_on=depends_on,
|
|
422
|
+
description=description,
|
|
423
|
+
estimated_cost=0.5, # Medium cost for traversal
|
|
424
|
+
metadata={"direction": self.direction or "outgoing"},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return step
|
|
428
|
+
|
|
429
|
+
def __repr__(self) -> str:
|
|
430
|
+
"""String representation for debugging"""
|
|
431
|
+
dir_str = f" {self.direction.upper()}" if self.direction else ""
|
|
432
|
+
return f"TraversalNode({self.relation_type}{dir_str})"
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass(frozen=True)
|
|
436
|
+
class FilterNode(ASTNode):
|
|
437
|
+
"""
|
|
438
|
+
Base class for filter nodes (WHERE conditions)
|
|
439
|
+
|
|
440
|
+
Filter nodes represent conditions in WHERE clauses.
|
|
441
|
+
They convert to filter dictionaries (MongoDB-style) for query execution.
|
|
442
|
+
|
|
443
|
+
Subclasses:
|
|
444
|
+
- PropertyFilterNode: property operator value (e.g., age > 30)
|
|
445
|
+
- BooleanFilterNode: AND/OR/NOT combinations
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
@abstractmethod
|
|
449
|
+
def to_filter_dict(self, context: "QueryContext") -> Dict[str, Any]:
|
|
450
|
+
"""
|
|
451
|
+
Convert filter to MongoDB-style filter dictionary
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
context: Query context for variable resolution
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Filter dictionary (e.g., {"age": {"$gt": 30}})
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@dataclass(frozen=True)
|
|
462
|
+
class PropertyFilterNode(FilterNode):
|
|
463
|
+
"""
|
|
464
|
+
Property filter node: property operator value
|
|
465
|
+
|
|
466
|
+
Represents a comparison between a property and a value.
|
|
467
|
+
|
|
468
|
+
Attributes:
|
|
469
|
+
property_path: Property name or nested path (e.g., "age" or "address.city")
|
|
470
|
+
operator: Comparison operator (==, !=, >, <, >=, <=, IN, CONTAINS)
|
|
471
|
+
value: Value to compare against
|
|
472
|
+
|
|
473
|
+
Example:
|
|
474
|
+
age > 30
|
|
475
|
+
|
|
476
|
+
PropertyFilterNode(
|
|
477
|
+
property_path="age",
|
|
478
|
+
operator=">",
|
|
479
|
+
value=30
|
|
480
|
+
)
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
property_path: str # Can be nested: "address.city"
|
|
484
|
+
operator: str # "==", "!=", ">", "<", ">=", "<=", "IN", "CONTAINS"
|
|
485
|
+
value: Any
|
|
486
|
+
|
|
487
|
+
def to_filter_dict(self, context: "QueryContext") -> Dict[str, Any]:
|
|
488
|
+
"""Convert to MongoDB-style filter dict"""
|
|
489
|
+
operator_map = {
|
|
490
|
+
"==": "$eq",
|
|
491
|
+
"!=": "$ne",
|
|
492
|
+
">": "$gt",
|
|
493
|
+
"<": "$lt",
|
|
494
|
+
">=": "$gte",
|
|
495
|
+
"<=": "$lte",
|
|
496
|
+
"IN": "$in",
|
|
497
|
+
"CONTAINS": "$regex",
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
mongo_op = operator_map.get(self.operator, "$eq")
|
|
501
|
+
|
|
502
|
+
# For CONTAINS, convert to regex pattern
|
|
503
|
+
if self.operator == "CONTAINS":
|
|
504
|
+
return {self.property_path: {mongo_op: self.value}}
|
|
505
|
+
|
|
506
|
+
return {self.property_path: {mongo_op: self.value}}
|
|
507
|
+
|
|
508
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
509
|
+
"""Validate property exists and type matches"""
|
|
510
|
+
errors = []
|
|
511
|
+
|
|
512
|
+
# Validate operator is valid
|
|
513
|
+
valid_operators = ["==", "!=", ">", "<", ">=", "<=", "IN", "CONTAINS"]
|
|
514
|
+
if self.operator not in valid_operators:
|
|
515
|
+
errors.append(
|
|
516
|
+
ValidationError(
|
|
517
|
+
self.line,
|
|
518
|
+
self.column,
|
|
519
|
+
f"Invalid operator '{self.operator}'. Must be one of: {', '.join(valid_operators)}",
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Validate IN operator has list value
|
|
524
|
+
if self.operator == "IN" and not isinstance(self.value, list):
|
|
525
|
+
errors.append(
|
|
526
|
+
ValidationError(
|
|
527
|
+
self.line,
|
|
528
|
+
self.column,
|
|
529
|
+
f"IN operator requires a list value, got {type(self.value).__name__}",
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Validate CONTAINS operator has string value
|
|
534
|
+
if self.operator == "CONTAINS" and not isinstance(self.value, str):
|
|
535
|
+
errors.append(
|
|
536
|
+
ValidationError(
|
|
537
|
+
self.line,
|
|
538
|
+
self.column,
|
|
539
|
+
f"CONTAINS operator requires a string value, got {type(self.value).__name__}",
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# TODO: Validate property exists in schema (requires entity context)
|
|
544
|
+
|
|
545
|
+
return errors
|
|
546
|
+
|
|
547
|
+
def __repr__(self) -> str:
|
|
548
|
+
"""String representation for debugging"""
|
|
549
|
+
value_repr = f'"{self.value}"' if isinstance(self.value, str) else str(self.value)
|
|
550
|
+
return f"PropertyFilterNode({self.property_path} {self.operator} {value_repr})"
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@dataclass(frozen=True)
|
|
554
|
+
class BooleanFilterNode(FilterNode):
|
|
555
|
+
"""
|
|
556
|
+
Boolean filter node: AND/OR/NOT combinations
|
|
557
|
+
|
|
558
|
+
Represents boolean combinations of filters.
|
|
559
|
+
|
|
560
|
+
Attributes:
|
|
561
|
+
operator: Boolean operator ("AND", "OR", "NOT")
|
|
562
|
+
operands: List of filter nodes to combine
|
|
563
|
+
|
|
564
|
+
Example:
|
|
565
|
+
age > 30 AND status == "active"
|
|
566
|
+
|
|
567
|
+
BooleanFilterNode(
|
|
568
|
+
operator="AND",
|
|
569
|
+
operands=[
|
|
570
|
+
PropertyFilterNode(property_path="age", operator=">", value=30),
|
|
571
|
+
PropertyFilterNode(property_path="status", operator="==", value="active")
|
|
572
|
+
]
|
|
573
|
+
)
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
operator: str # "AND", "OR", "NOT"
|
|
577
|
+
operands: List[FilterNode] = field(default_factory=list)
|
|
578
|
+
|
|
579
|
+
def to_filter_dict(self, context: "QueryContext") -> Dict[str, Any]:
|
|
580
|
+
"""Convert to MongoDB-style boolean filter"""
|
|
581
|
+
op_map = {"AND": "$and", "OR": "$or", "NOT": "$not"}
|
|
582
|
+
|
|
583
|
+
mongo_op = op_map.get(self.operator, "$and")
|
|
584
|
+
operand_dicts = [op.to_filter_dict(context) for op in self.operands]
|
|
585
|
+
|
|
586
|
+
# NOT operator has special handling (single operand)
|
|
587
|
+
if self.operator == "NOT":
|
|
588
|
+
if len(operand_dicts) == 1:
|
|
589
|
+
return {mongo_op: operand_dicts[0]}
|
|
590
|
+
else:
|
|
591
|
+
# Multiple operands: NOT (a AND b AND c) = NOT {$and: [a, b,
|
|
592
|
+
# c]}
|
|
593
|
+
return {mongo_op: {"$and": operand_dicts}}
|
|
594
|
+
|
|
595
|
+
return {mongo_op: operand_dicts}
|
|
596
|
+
|
|
597
|
+
def validate(self, schema: Any) -> List[ValidationError]:
|
|
598
|
+
"""Validate all operands"""
|
|
599
|
+
errors = []
|
|
600
|
+
|
|
601
|
+
# Validate operator is valid
|
|
602
|
+
valid_operators = ["AND", "OR", "NOT"]
|
|
603
|
+
if self.operator not in valid_operators:
|
|
604
|
+
errors.append(
|
|
605
|
+
ValidationError(
|
|
606
|
+
self.line,
|
|
607
|
+
self.column,
|
|
608
|
+
f"Invalid boolean operator '{self.operator}'. Must be one of: {', '.join(valid_operators)}",
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Validate operand count
|
|
613
|
+
if not self.operands:
|
|
614
|
+
errors.append(
|
|
615
|
+
ValidationError(
|
|
616
|
+
self.line,
|
|
617
|
+
self.column,
|
|
618
|
+
f"Boolean operator '{self.operator}' requires at least one operand",
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Validate all operands
|
|
623
|
+
for operand in self.operands:
|
|
624
|
+
errors.extend(operand.validate(schema))
|
|
625
|
+
|
|
626
|
+
return errors
|
|
627
|
+
|
|
628
|
+
def __repr__(self) -> str:
|
|
629
|
+
"""String representation for debugging"""
|
|
630
|
+
return f"BooleanFilterNode({self.operator}, operands={len(self.operands)})"
|