aiecs 1.0.1__py3-none-any.whl → 1.7.6__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.
Potentially problematic release.
This version of aiecs might be problematic. Click here for more details.
- aiecs/__init__.py +13 -16
- aiecs/__main__.py +7 -7
- aiecs/aiecs_client.py +269 -75
- aiecs/application/executors/operation_executor.py +79 -54
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
- aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
- aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
- aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +98 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
- aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
- aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
- aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
- aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
- aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
- aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
- aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
- aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
- aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
- aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -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 +341 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -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 +913 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
- aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
- aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
- aiecs/application/knowledge_graph/search/reranker.py +293 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +471 -0
- aiecs/config/__init__.py +20 -5
- aiecs/config/config.py +762 -31
- aiecs/config/graph_config.py +131 -0
- aiecs/config/tool_config.py +399 -0
- aiecs/core/__init__.py +29 -13
- aiecs/core/interface/__init__.py +2 -2
- aiecs/core/interface/execution_interface.py +22 -22
- aiecs/core/interface/storage_interface.py +37 -88
- aiecs/core/registry/__init__.py +31 -0
- aiecs/core/registry/service_registry.py +92 -0
- aiecs/domain/__init__.py +270 -1
- aiecs/domain/agent/__init__.py +191 -0
- aiecs/domain/agent/base_agent.py +3870 -0
- aiecs/domain/agent/exceptions.py +99 -0
- aiecs/domain/agent/graph_aware_mixin.py +569 -0
- aiecs/domain/agent/hybrid_agent.py +1435 -0
- aiecs/domain/agent/integration/__init__.py +29 -0
- aiecs/domain/agent/integration/context_compressor.py +216 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
- aiecs/domain/agent/integration/protocols.py +281 -0
- aiecs/domain/agent/integration/retry_policy.py +218 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
- aiecs/domain/agent/lifecycle.py +291 -0
- aiecs/domain/agent/llm_agent.py +692 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +1124 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +163 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
- aiecs/domain/agent/models.py +884 -0
- aiecs/domain/agent/observability.py +479 -0
- aiecs/domain/agent/persistence.py +449 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +159 -0
- aiecs/domain/agent/prompts/formatters.py +187 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +253 -0
- aiecs/domain/agent/tool_agent.py +444 -0
- aiecs/domain/agent/tools/__init__.py +15 -0
- aiecs/domain/agent/tools/schema_generator.py +364 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +469 -0
- aiecs/domain/community/analytics.py +432 -0
- aiecs/domain/community/collaborative_workflow.py +648 -0
- aiecs/domain/community/communication_hub.py +634 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +796 -0
- aiecs/domain/community/community_manager.py +803 -0
- aiecs/domain/community/decision_engine.py +849 -0
- aiecs/domain/community/exceptions.py +231 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +234 -0
- aiecs/domain/community/resource_manager.py +461 -0
- aiecs/domain/community/shared_context_manager.py +589 -0
- aiecs/domain/context/__init__.py +40 -10
- aiecs/domain/context/context_engine.py +1910 -0
- aiecs/domain/context/conversation_models.py +87 -53
- aiecs/domain/context/graph_memory.py +582 -0
- aiecs/domain/execution/model.py +12 -4
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +148 -0
- aiecs/domain/knowledge_graph/models/evidence.py +178 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
- aiecs/domain/knowledge_graph/models/path.py +171 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
- aiecs/domain/knowledge_graph/models/query.py +261 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
- aiecs/domain/knowledge_graph/models/relation.py +202 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
- aiecs/domain/task/dsl_processor.py +172 -56
- aiecs/domain/task/model.py +20 -8
- aiecs/domain/task/task_context.py +27 -24
- aiecs/infrastructure/__init__.py +0 -2
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +837 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
- aiecs/infrastructure/graph_storage/cache.py +424 -0
- aiecs/infrastructure/graph_storage/distributed.py +223 -0
- aiecs/infrastructure/graph_storage/error_handling.py +380 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
- aiecs/infrastructure/graph_storage/metrics.py +344 -0
- aiecs/infrastructure/graph_storage/migration.py +400 -0
- aiecs/infrastructure/graph_storage/pagination.py +483 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
- aiecs/infrastructure/graph_storage/postgres.py +1563 -0
- aiecs/infrastructure/graph_storage/property_storage.py +353 -0
- aiecs/infrastructure/graph_storage/protocols.py +76 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
- aiecs/infrastructure/graph_storage/streaming.py +487 -0
- aiecs/infrastructure/graph_storage/tenant.py +412 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
- aiecs/infrastructure/messaging/websocket_manager.py +51 -35
- aiecs/infrastructure/monitoring/__init__.py +22 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
- aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
- aiecs/infrastructure/monitoring/structured_logger.py +3 -7
- aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
- aiecs/infrastructure/persistence/__init__.py +14 -1
- aiecs/infrastructure/persistence/context_engine_client.py +184 -0
- aiecs/infrastructure/persistence/database_manager.py +67 -43
- aiecs/infrastructure/persistence/file_storage.py +180 -103
- aiecs/infrastructure/persistence/redis_client.py +74 -21
- aiecs/llm/__init__.py +73 -25
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
- aiecs/llm/client_factory.py +224 -36
- aiecs/llm/client_resolver.py +155 -0
- aiecs/llm/clients/__init__.py +38 -0
- aiecs/llm/clients/base_client.py +324 -0
- aiecs/llm/clients/google_function_calling_mixin.py +457 -0
- aiecs/llm/clients/googleai_client.py +241 -0
- aiecs/llm/clients/openai_client.py +158 -0
- aiecs/llm/clients/openai_compatible_mixin.py +367 -0
- aiecs/llm/clients/vertex_client.py +897 -0
- aiecs/llm/clients/xai_client.py +201 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +272 -0
- aiecs/llm/config/config_validator.py +206 -0
- aiecs/llm/config/model_config.py +143 -0
- aiecs/llm/protocols.py +149 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +89 -0
- aiecs/main.py +140 -121
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/module_checker.py +499 -0
- aiecs/scripts/aid/version_manager.py +235 -0
- aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
- aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
- aiecs/scripts/dependance_check/__init__.py +15 -0
- aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
- aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
- aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
- aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
- aiecs/scripts/knowledge_graph/__init__.py +3 -0
- aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
- aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
- aiecs/scripts/tools_develop/README.md +671 -0
- aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
- aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
- aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
- aiecs/scripts/tools_develop/schema_coverage.py +511 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
- aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
- aiecs/scripts/tools_develop/verify_tools.py +352 -0
- aiecs/tasks/__init__.py +0 -1
- aiecs/tasks/worker.py +115 -47
- aiecs/tools/__init__.py +194 -72
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +330 -0
- aiecs/tools/apisource/providers/__init__.py +112 -0
- aiecs/tools/apisource/providers/base.py +671 -0
- aiecs/tools/apisource/providers/census.py +397 -0
- aiecs/tools/apisource/providers/fred.py +535 -0
- aiecs/tools/apisource/providers/newsapi.py +409 -0
- aiecs/tools/apisource/providers/worldbank.py +352 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +363 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
- aiecs/tools/apisource/tool.py +832 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +334 -0
- aiecs/tools/base_tool.py +415 -21
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +607 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
- aiecs/tools/docs/content_insertion_tool.py +1320 -0
- aiecs/tools/docs/document_creator_tool.py +1323 -0
- aiecs/tools/docs/document_layout_tool.py +1160 -0
- aiecs/tools/docs/document_parser_tool.py +1011 -0
- aiecs/tools/docs/document_writer_tool.py +1829 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
- aiecs/tools/langchain_adapter.py +300 -138
- aiecs/tools/schema_generator.py +455 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +581 -0
- aiecs/tools/search_tool/cache.py +264 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +224 -0
- aiecs/tools/search_tool/core.py +778 -0
- aiecs/tools/search_tool/deduplicator.py +119 -0
- aiecs/tools/search_tool/error_handler.py +242 -0
- aiecs/tools/search_tool/metrics.py +343 -0
- aiecs/tools/search_tool/rate_limiter.py +172 -0
- aiecs/tools/search_tool/schemas.py +275 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
- aiecs/tools/statistics/data_loader_tool.py +555 -0
- aiecs/tools/statistics/data_profiler_tool.py +638 -0
- aiecs/tools/statistics/data_transformer_tool.py +580 -0
- aiecs/tools/statistics/data_visualizer_tool.py +498 -0
- aiecs/tools/statistics/model_trainer_tool.py +507 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
- aiecs/tools/task_tools/__init__.py +49 -36
- aiecs/tools/task_tools/chart_tool.py +200 -184
- aiecs/tools/task_tools/classfire_tool.py +268 -267
- aiecs/tools/task_tools/image_tool.py +175 -131
- aiecs/tools/task_tools/office_tool.py +226 -146
- aiecs/tools/task_tools/pandas_tool.py +477 -121
- aiecs/tools/task_tools/report_tool.py +390 -142
- aiecs/tools/task_tools/research_tool.py +149 -79
- aiecs/tools/task_tools/scraper_tool.py +339 -145
- aiecs/tools/task_tools/stats_tool.py +448 -209
- aiecs/tools/temp_file_manager.py +26 -24
- aiecs/tools/tool_executor/__init__.py +18 -16
- aiecs/tools/tool_executor/tool_executor.py +364 -52
- aiecs/utils/LLM_output_structor.py +74 -48
- aiecs/utils/__init__.py +14 -3
- aiecs/utils/base_callback.py +0 -3
- aiecs/utils/cache_provider.py +696 -0
- aiecs/utils/execution_utils.py +50 -31
- aiecs/utils/prompt_loader.py +1 -0
- aiecs/utils/token_usage_repository.py +37 -11
- aiecs/ws/socket_server.py +14 -4
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/METADATA +52 -15
- aiecs-1.7.6.dist-info/RECORD +337 -0
- aiecs-1.7.6.dist-info/entry_points.txt +13 -0
- aiecs/config/registry.py +0 -19
- aiecs/domain/context/content_engine.py +0 -982
- aiecs/llm/base_client.py +0 -99
- aiecs/llm/openai_client.py +0 -125
- aiecs/llm/vertex_client.py +0 -186
- aiecs/llm/xai_client.py +0 -184
- aiecs/scripts/dependency_checker.py +0 -857
- aiecs/scripts/quick_dependency_check.py +0 -269
- aiecs/tools/task_tools/search_api.py +0 -7
- aiecs-1.0.1.dist-info/RECORD +0 -90
- aiecs-1.0.1.dist-info/entry_points.txt +0 -7
- /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
- /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
- /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
- /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/WHEEL +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SQLite Graph Storage Backend
|
|
3
|
+
|
|
4
|
+
Provides file-based persistent graph storage using SQLite.
|
|
5
|
+
|
|
6
|
+
Multi-tenancy Support:
|
|
7
|
+
- SHARED_SCHEMA mode: Single database with tenant_id column filtering
|
|
8
|
+
- SEPARATE_SCHEMA mode: Table prefixes per tenant (tenant_xxx_entities, tenant_xxx_relations)
|
|
9
|
+
- Global namespace for tenant_id=NULL (backward compatible)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import aiosqlite
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
from pathlib import Path as PathLibPath
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
|
|
18
|
+
from aiecs.domain.knowledge_graph.models.entity import Entity
|
|
19
|
+
from aiecs.domain.knowledge_graph.models.relation import Relation
|
|
20
|
+
from aiecs.domain.knowledge_graph.models.path import Path
|
|
21
|
+
from aiecs.infrastructure.graph_storage.base import GraphStore
|
|
22
|
+
from aiecs.infrastructure.graph_storage.tenant import (
|
|
23
|
+
TenantContext,
|
|
24
|
+
TenantIsolationMode,
|
|
25
|
+
CrossTenantRelationError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# SQL Schema for SQLite graph storage with multi-tenancy support
|
|
30
|
+
SCHEMA_SQL = """
|
|
31
|
+
-- Entities table with tenant_id for multi-tenancy
|
|
32
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
33
|
+
id TEXT NOT NULL,
|
|
34
|
+
tenant_id TEXT, -- NULL for global namespace
|
|
35
|
+
entity_type TEXT NOT NULL,
|
|
36
|
+
properties TEXT NOT NULL, -- JSON
|
|
37
|
+
embedding BLOB, -- Vector embedding (serialized)
|
|
38
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
39
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
40
|
+
PRIMARY KEY (id, tenant_id)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Relations table with tenant_id for multi-tenancy
|
|
44
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
45
|
+
id TEXT NOT NULL,
|
|
46
|
+
tenant_id TEXT, -- NULL for global namespace
|
|
47
|
+
relation_type TEXT NOT NULL,
|
|
48
|
+
source_id TEXT NOT NULL,
|
|
49
|
+
target_id TEXT NOT NULL,
|
|
50
|
+
properties TEXT NOT NULL, -- JSON
|
|
51
|
+
weight REAL DEFAULT 1.0,
|
|
52
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
53
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
54
|
+
PRIMARY KEY (id, tenant_id)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- Indexes for performance
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_entities_tenant_type ON entities(tenant_id, entity_type);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_relations_tenant_source ON relations(tenant_id, source_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_relations_tenant_target ON relations(tenant_id, target_id);
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Migration SQL for adding tenant_id to existing databases
|
|
70
|
+
MIGRATION_ADD_TENANT_ID = """
|
|
71
|
+
-- Add tenant_id column to entities if not exists
|
|
72
|
+
ALTER TABLE entities ADD COLUMN tenant_id TEXT;
|
|
73
|
+
|
|
74
|
+
-- Add tenant_id column to relations if not exists
|
|
75
|
+
ALTER TABLE relations ADD COLUMN tenant_id TEXT;
|
|
76
|
+
|
|
77
|
+
-- Create tenant indexes
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_entities_tenant_type ON entities(tenant_id, entity_type);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_relations_tenant_source ON relations(tenant_id, source_id);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_relations_tenant_target ON relations(tenant_id, target_id);
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SQLiteGraphStore(GraphStore):
|
|
87
|
+
"""
|
|
88
|
+
SQLite-based graph storage implementation
|
|
89
|
+
|
|
90
|
+
Provides persistent file-based graph storage with:
|
|
91
|
+
- ACID transactions
|
|
92
|
+
- SQL-optimized queries
|
|
93
|
+
- Optional recursive CTEs for traversal
|
|
94
|
+
- Connection pooling
|
|
95
|
+
|
|
96
|
+
Features:
|
|
97
|
+
- File-based persistence (single .db file)
|
|
98
|
+
- Automatic schema initialization
|
|
99
|
+
- Efficient SQL queries for graph operations
|
|
100
|
+
- Optional Tier 2 optimizations
|
|
101
|
+
|
|
102
|
+
Multi-Tenancy Support:
|
|
103
|
+
- SHARED_SCHEMA mode: Single database with tenant_id column filtering
|
|
104
|
+
- SEPARATE_SCHEMA mode: Table prefixes per tenant
|
|
105
|
+
- Global namespace for tenant_id=NULL (backward compatible)
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
```python
|
|
109
|
+
store = SQLiteGraphStore("knowledge_graph.db")
|
|
110
|
+
await store.initialize()
|
|
111
|
+
|
|
112
|
+
# Single-tenant usage (backward compatible)
|
|
113
|
+
entity = Entity(id="e1", entity_type="Person", properties={"name": "Alice"})
|
|
114
|
+
await store.add_entity(entity)
|
|
115
|
+
|
|
116
|
+
# Multi-tenant usage
|
|
117
|
+
from aiecs.infrastructure.graph_storage.tenant import TenantContext
|
|
118
|
+
context = TenantContext(tenant_id="acme-corp")
|
|
119
|
+
await store.add_entity(entity, context=context)
|
|
120
|
+
|
|
121
|
+
await store.close()
|
|
122
|
+
```
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, db_path: str = ":memory:", isolation_mode: TenantIsolationMode = TenantIsolationMode.SHARED_SCHEMA, **kwargs):
|
|
126
|
+
"""
|
|
127
|
+
Initialize SQLite graph store
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
db_path: Path to SQLite database file (":memory:" for in-memory)
|
|
131
|
+
isolation_mode: Tenant isolation mode (SHARED_SCHEMA or SEPARATE_SCHEMA)
|
|
132
|
+
**kwargs: Additional SQLite connection parameters
|
|
133
|
+
"""
|
|
134
|
+
super().__init__()
|
|
135
|
+
self.db_path = db_path
|
|
136
|
+
self.isolation_mode = isolation_mode
|
|
137
|
+
self.conn_kwargs = kwargs
|
|
138
|
+
self.conn: Optional[aiosqlite.Connection] = None
|
|
139
|
+
self._is_initialized = False
|
|
140
|
+
self._in_transaction = False
|
|
141
|
+
self._initialized_tenant_tables: set = set() # Track created tenant tables for SEPARATE_SCHEMA
|
|
142
|
+
|
|
143
|
+
async def initialize(self):
|
|
144
|
+
"""Initialize SQLite database and create schema"""
|
|
145
|
+
# Create directory if needed
|
|
146
|
+
if self.db_path != ":memory:":
|
|
147
|
+
PathLibPath(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
|
|
149
|
+
# Connect to database
|
|
150
|
+
self.conn = await aiosqlite.connect(self.db_path, **self.conn_kwargs)
|
|
151
|
+
|
|
152
|
+
# Enable foreign keys
|
|
153
|
+
if self.conn is None:
|
|
154
|
+
raise RuntimeError("Failed to initialize database connection")
|
|
155
|
+
await self.conn.execute("PRAGMA foreign_keys = ON")
|
|
156
|
+
|
|
157
|
+
# Create schema (for SHARED_SCHEMA mode or base tables)
|
|
158
|
+
await self.conn.executescript(SCHEMA_SQL)
|
|
159
|
+
await self.conn.commit()
|
|
160
|
+
|
|
161
|
+
self._is_initialized = True
|
|
162
|
+
self._initialized_tenant_tables = set()
|
|
163
|
+
|
|
164
|
+
async def close(self):
|
|
165
|
+
"""Close database connection"""
|
|
166
|
+
if self.conn:
|
|
167
|
+
await self.conn.close()
|
|
168
|
+
self.conn = None
|
|
169
|
+
self._is_initialized = False
|
|
170
|
+
self._initialized_tenant_tables = set()
|
|
171
|
+
|
|
172
|
+
# =========================================================================
|
|
173
|
+
# Multi-Tenancy Helpers
|
|
174
|
+
# =========================================================================
|
|
175
|
+
|
|
176
|
+
def _get_tenant_id(self, context: Optional[TenantContext]) -> Optional[str]:
|
|
177
|
+
"""Extract tenant_id from context, returns None for global namespace."""
|
|
178
|
+
return context.tenant_id if context else None
|
|
179
|
+
|
|
180
|
+
def _get_table_name(self, base_table: str, tenant_id: Optional[str]) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Get table name based on isolation mode.
|
|
183
|
+
|
|
184
|
+
For SHARED_SCHEMA: Returns base table name (filtering done via WHERE clause)
|
|
185
|
+
For SEPARATE_SCHEMA: Returns prefixed table name (tenant_xxx_entities)
|
|
186
|
+
"""
|
|
187
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
188
|
+
# Sanitize tenant_id for use in table name (replace - with _)
|
|
189
|
+
safe_tenant = tenant_id.replace("-", "_")
|
|
190
|
+
return f"tenant_{safe_tenant}_{base_table}"
|
|
191
|
+
return base_table
|
|
192
|
+
|
|
193
|
+
async def _ensure_tenant_tables(self, tenant_id: str) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Ensure tenant-specific tables exist for SEPARATE_SCHEMA mode.
|
|
196
|
+
|
|
197
|
+
Creates tables like tenant_xxx_entities and tenant_xxx_relations.
|
|
198
|
+
"""
|
|
199
|
+
if self.isolation_mode != TenantIsolationMode.SEPARATE_SCHEMA:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
if tenant_id in self._initialized_tenant_tables:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
if self.conn is None:
|
|
206
|
+
raise RuntimeError("Database connection not initialized")
|
|
207
|
+
|
|
208
|
+
safe_tenant = tenant_id.replace("-", "_")
|
|
209
|
+
entities_table = f"tenant_{safe_tenant}_entities"
|
|
210
|
+
relations_table = f"tenant_{safe_tenant}_relations"
|
|
211
|
+
|
|
212
|
+
# Create tenant-specific tables
|
|
213
|
+
tenant_schema = f"""
|
|
214
|
+
CREATE TABLE IF NOT EXISTS {entities_table} (
|
|
215
|
+
id TEXT PRIMARY KEY,
|
|
216
|
+
entity_type TEXT NOT NULL,
|
|
217
|
+
properties TEXT NOT NULL,
|
|
218
|
+
embedding BLOB,
|
|
219
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
220
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
CREATE TABLE IF NOT EXISTS {relations_table} (
|
|
224
|
+
id TEXT PRIMARY KEY,
|
|
225
|
+
relation_type TEXT NOT NULL,
|
|
226
|
+
source_id TEXT NOT NULL,
|
|
227
|
+
target_id TEXT NOT NULL,
|
|
228
|
+
properties TEXT NOT NULL,
|
|
229
|
+
weight REAL DEFAULT 1.0,
|
|
230
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
231
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_entities_type ON {entities_table}(entity_type);
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_relations_type ON {relations_table}(relation_type);
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_relations_source ON {relations_table}(source_id);
|
|
237
|
+
CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_relations_target ON {relations_table}(target_id);
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
await self.conn.executescript(tenant_schema)
|
|
241
|
+
await self.conn.commit()
|
|
242
|
+
self._initialized_tenant_tables.add(tenant_id)
|
|
243
|
+
|
|
244
|
+
def _build_tenant_filter(self, tenant_id: Optional[str], table_alias: str = "") -> Tuple[str, List]:
|
|
245
|
+
"""
|
|
246
|
+
Build SQL WHERE clause for tenant filtering in SHARED_SCHEMA mode.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (WHERE clause fragment, parameters list)
|
|
250
|
+
"""
|
|
251
|
+
prefix = f"{table_alias}." if table_alias else ""
|
|
252
|
+
|
|
253
|
+
if tenant_id is None:
|
|
254
|
+
return f"{prefix}tenant_id IS NULL", []
|
|
255
|
+
else:
|
|
256
|
+
return f"{prefix}tenant_id = ?", [tenant_id]
|
|
257
|
+
|
|
258
|
+
@asynccontextmanager
|
|
259
|
+
async def transaction(self):
|
|
260
|
+
"""
|
|
261
|
+
Transaction context manager for atomic operations
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
```python
|
|
265
|
+
async with store.transaction():
|
|
266
|
+
await store.add_entity(entity1)
|
|
267
|
+
await store.add_entity(entity2)
|
|
268
|
+
# Both entities added atomically
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Note: SQLite uses connection-level transactions. Within a transaction,
|
|
272
|
+
commits are deferred until the context exits successfully.
|
|
273
|
+
"""
|
|
274
|
+
if not self._is_initialized:
|
|
275
|
+
raise RuntimeError("GraphStore not initialized")
|
|
276
|
+
|
|
277
|
+
# Track transaction state to prevent auto-commits in operations
|
|
278
|
+
self._in_transaction = True
|
|
279
|
+
try:
|
|
280
|
+
# Begin transaction
|
|
281
|
+
await self.conn.execute("BEGIN")
|
|
282
|
+
yield
|
|
283
|
+
# Commit on success
|
|
284
|
+
await self.conn.commit()
|
|
285
|
+
except Exception:
|
|
286
|
+
# Rollback on error
|
|
287
|
+
await self.conn.rollback()
|
|
288
|
+
raise
|
|
289
|
+
finally:
|
|
290
|
+
self._in_transaction = False
|
|
291
|
+
|
|
292
|
+
# =========================================================================
|
|
293
|
+
# Tier 1: Basic Interface (SQL-optimized implementations)
|
|
294
|
+
# =========================================================================
|
|
295
|
+
|
|
296
|
+
async def add_entity(self, entity: Entity, context: Optional[TenantContext] = None) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Add entity to SQLite database
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
entity: Entity to add
|
|
302
|
+
context: Optional tenant context for multi-tenant isolation
|
|
303
|
+
"""
|
|
304
|
+
if not self._is_initialized:
|
|
305
|
+
raise RuntimeError("GraphStore not initialized")
|
|
306
|
+
if self.conn is None:
|
|
307
|
+
raise RuntimeError("Database connection not initialized")
|
|
308
|
+
|
|
309
|
+
tenant_id = self._get_tenant_id(context)
|
|
310
|
+
|
|
311
|
+
# Ensure tenant tables exist for SEPARATE_SCHEMA mode
|
|
312
|
+
if tenant_id and self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
|
|
313
|
+
await self._ensure_tenant_tables(tenant_id)
|
|
314
|
+
|
|
315
|
+
table_name = self._get_table_name("entities", tenant_id)
|
|
316
|
+
|
|
317
|
+
# Check if entity already exists (within tenant scope)
|
|
318
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
319
|
+
cursor = await self.conn.execute(f"SELECT id FROM {table_name} WHERE id = ?", (entity.id,))
|
|
320
|
+
else:
|
|
321
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
322
|
+
cursor = await self.conn.execute(
|
|
323
|
+
f"SELECT id FROM {table_name} WHERE id = ? AND {tenant_filter}",
|
|
324
|
+
[entity.id] + params
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
existing = await cursor.fetchone()
|
|
328
|
+
if existing:
|
|
329
|
+
raise ValueError(f"Entity with ID '{entity.id}' already exists")
|
|
330
|
+
|
|
331
|
+
# Set tenant_id on entity if context provided
|
|
332
|
+
if tenant_id is not None and entity.tenant_id is None:
|
|
333
|
+
entity.tenant_id = tenant_id
|
|
334
|
+
|
|
335
|
+
# Serialize data
|
|
336
|
+
properties_json = json.dumps(entity.properties)
|
|
337
|
+
embedding_blob = self._serialize_embedding(entity.embedding) if entity.embedding else None
|
|
338
|
+
|
|
339
|
+
# Insert entity
|
|
340
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
341
|
+
await self.conn.execute(
|
|
342
|
+
f"""
|
|
343
|
+
INSERT INTO {table_name} (id, entity_type, properties, embedding)
|
|
344
|
+
VALUES (?, ?, ?, ?)
|
|
345
|
+
""",
|
|
346
|
+
(entity.id, entity.entity_type, properties_json, embedding_blob),
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
await self.conn.execute(
|
|
350
|
+
f"""
|
|
351
|
+
INSERT INTO {table_name} (id, tenant_id, entity_type, properties, embedding)
|
|
352
|
+
VALUES (?, ?, ?, ?, ?)
|
|
353
|
+
""",
|
|
354
|
+
(entity.id, tenant_id, entity.entity_type, properties_json, embedding_blob),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if not self._in_transaction:
|
|
358
|
+
await self.conn.commit()
|
|
359
|
+
|
|
360
|
+
async def get_entity(self, entity_id: str, context: Optional[TenantContext] = None) -> Optional[Entity]:
|
|
361
|
+
"""
|
|
362
|
+
Get entity from SQLite database
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
entity_id: Entity ID to retrieve
|
|
366
|
+
context: Optional tenant context for multi-tenant isolation
|
|
367
|
+
"""
|
|
368
|
+
if not self._is_initialized:
|
|
369
|
+
raise RuntimeError("GraphStore not initialized")
|
|
370
|
+
if self.conn is None:
|
|
371
|
+
raise RuntimeError("Database connection not initialized")
|
|
372
|
+
|
|
373
|
+
tenant_id = self._get_tenant_id(context)
|
|
374
|
+
table_name = self._get_table_name("entities", tenant_id)
|
|
375
|
+
|
|
376
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
377
|
+
# SEPARATE_SCHEMA: No tenant_id column in tenant-specific tables
|
|
378
|
+
cursor = await self.conn.execute(
|
|
379
|
+
f"""
|
|
380
|
+
SELECT id, entity_type, properties, embedding
|
|
381
|
+
FROM {table_name}
|
|
382
|
+
WHERE id = ?
|
|
383
|
+
""",
|
|
384
|
+
(entity_id,),
|
|
385
|
+
)
|
|
386
|
+
row = await cursor.fetchone()
|
|
387
|
+
if not row:
|
|
388
|
+
return None
|
|
389
|
+
return self._row_to_entity(tuple(row), tenant_id=tenant_id)
|
|
390
|
+
else:
|
|
391
|
+
# SHARED_SCHEMA: Filter by tenant_id column
|
|
392
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
393
|
+
cursor = await self.conn.execute(
|
|
394
|
+
f"""
|
|
395
|
+
SELECT id, tenant_id, entity_type, properties, embedding
|
|
396
|
+
FROM {table_name}
|
|
397
|
+
WHERE id = ? AND {tenant_filter}
|
|
398
|
+
""",
|
|
399
|
+
[entity_id] + params,
|
|
400
|
+
)
|
|
401
|
+
row = await cursor.fetchone()
|
|
402
|
+
if not row:
|
|
403
|
+
return None
|
|
404
|
+
return self._row_to_entity_with_tenant(tuple(row))
|
|
405
|
+
|
|
406
|
+
async def update_entity(self, entity: Entity, context: Optional[TenantContext] = None) -> Entity:
|
|
407
|
+
"""
|
|
408
|
+
Update entity in SQLite database
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
entity: Entity to update
|
|
412
|
+
context: Optional tenant context for multi-tenant isolation
|
|
413
|
+
"""
|
|
414
|
+
if not self._is_initialized:
|
|
415
|
+
raise RuntimeError("GraphStore not initialized")
|
|
416
|
+
if self.conn is None:
|
|
417
|
+
raise RuntimeError("Database connection not initialized")
|
|
418
|
+
|
|
419
|
+
tenant_id = self._get_tenant_id(context)
|
|
420
|
+
table_name = self._get_table_name("entities", tenant_id)
|
|
421
|
+
|
|
422
|
+
# Check if entity exists
|
|
423
|
+
existing = await self.get_entity(entity.id, context=context)
|
|
424
|
+
if not existing:
|
|
425
|
+
raise ValueError(f"Entity with ID '{entity.id}' does not exist")
|
|
426
|
+
|
|
427
|
+
# Serialize data
|
|
428
|
+
properties_json = json.dumps(entity.properties)
|
|
429
|
+
embedding_blob = self._serialize_embedding(entity.embedding) if entity.embedding else None
|
|
430
|
+
|
|
431
|
+
# Update entity
|
|
432
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
433
|
+
await self.conn.execute(
|
|
434
|
+
f"""
|
|
435
|
+
UPDATE {table_name}
|
|
436
|
+
SET entity_type = ?, properties = ?, embedding = ?, updated_at = CURRENT_TIMESTAMP
|
|
437
|
+
WHERE id = ?
|
|
438
|
+
""",
|
|
439
|
+
(entity.entity_type, properties_json, embedding_blob, entity.id),
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
443
|
+
await self.conn.execute(
|
|
444
|
+
f"""
|
|
445
|
+
UPDATE {table_name}
|
|
446
|
+
SET entity_type = ?, properties = ?, embedding = ?, updated_at = CURRENT_TIMESTAMP
|
|
447
|
+
WHERE id = ? AND {tenant_filter}
|
|
448
|
+
""",
|
|
449
|
+
[entity.entity_type, properties_json, embedding_blob, entity.id] + params,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if not self._in_transaction:
|
|
453
|
+
await self.conn.commit()
|
|
454
|
+
|
|
455
|
+
return entity
|
|
456
|
+
|
|
457
|
+
async def delete_entity(self, entity_id: str, context: Optional[TenantContext] = None):
|
|
458
|
+
"""
|
|
459
|
+
Delete entity and its relations from SQLite database
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
entity_id: Entity ID to delete
|
|
463
|
+
context: Optional tenant context for multi-tenant isolation
|
|
464
|
+
"""
|
|
465
|
+
if not self._is_initialized:
|
|
466
|
+
raise RuntimeError("GraphStore not initialized")
|
|
467
|
+
if self.conn is None:
|
|
468
|
+
raise RuntimeError("Database connection not initialized")
|
|
469
|
+
|
|
470
|
+
tenant_id = self._get_tenant_id(context)
|
|
471
|
+
entities_table = self._get_table_name("entities", tenant_id)
|
|
472
|
+
relations_table = self._get_table_name("relations", tenant_id)
|
|
473
|
+
|
|
474
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
475
|
+
# Delete relations first (no foreign key in SEPARATE_SCHEMA)
|
|
476
|
+
await self.conn.execute(
|
|
477
|
+
f"DELETE FROM {relations_table} WHERE source_id = ? OR target_id = ?",
|
|
478
|
+
(entity_id, entity_id)
|
|
479
|
+
)
|
|
480
|
+
await self.conn.execute(f"DELETE FROM {entities_table} WHERE id = ?", (entity_id,))
|
|
481
|
+
else:
|
|
482
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
483
|
+
# Delete relations first
|
|
484
|
+
await self.conn.execute(
|
|
485
|
+
f"DELETE FROM {relations_table} WHERE (source_id = ? OR target_id = ?) AND {tenant_filter}",
|
|
486
|
+
[entity_id, entity_id] + params
|
|
487
|
+
)
|
|
488
|
+
await self.conn.execute(
|
|
489
|
+
f"DELETE FROM {entities_table} WHERE id = ? AND {tenant_filter}",
|
|
490
|
+
[entity_id] + params
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if not self._in_transaction:
|
|
494
|
+
await self.conn.commit()
|
|
495
|
+
|
|
496
|
+
async def add_relation(self, relation: Relation, context: Optional[TenantContext] = None) -> None:
|
|
497
|
+
"""
|
|
498
|
+
Add relation to SQLite database
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
relation: Relation to add
|
|
502
|
+
context: Optional tenant context for multi-tenant isolation
|
|
503
|
+
|
|
504
|
+
Raises:
|
|
505
|
+
CrossTenantRelationError: If source and target entities belong to different tenants
|
|
506
|
+
"""
|
|
507
|
+
if not self._is_initialized:
|
|
508
|
+
raise RuntimeError("GraphStore not initialized")
|
|
509
|
+
if self.conn is None:
|
|
510
|
+
raise RuntimeError("Database connection not initialized")
|
|
511
|
+
|
|
512
|
+
tenant_id = self._get_tenant_id(context)
|
|
513
|
+
|
|
514
|
+
# Ensure tenant tables exist for SEPARATE_SCHEMA mode
|
|
515
|
+
if tenant_id and self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
|
|
516
|
+
await self._ensure_tenant_tables(tenant_id)
|
|
517
|
+
|
|
518
|
+
table_name = self._get_table_name("relations", tenant_id)
|
|
519
|
+
|
|
520
|
+
# Check if relation already exists (within tenant scope)
|
|
521
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
522
|
+
cursor = await self.conn.execute(f"SELECT id FROM {table_name} WHERE id = ?", (relation.id,))
|
|
523
|
+
else:
|
|
524
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
525
|
+
cursor = await self.conn.execute(
|
|
526
|
+
f"SELECT id FROM {table_name} WHERE id = ? AND {tenant_filter}",
|
|
527
|
+
[relation.id] + params
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
existing = await cursor.fetchone()
|
|
531
|
+
if existing:
|
|
532
|
+
raise ValueError(f"Relation with ID '{relation.id}' already exists")
|
|
533
|
+
|
|
534
|
+
# Check if entities exist within tenant scope
|
|
535
|
+
source_entity = await self.get_entity(relation.source_id, context=context)
|
|
536
|
+
target_entity = await self.get_entity(relation.target_id, context=context)
|
|
537
|
+
|
|
538
|
+
if not source_entity:
|
|
539
|
+
raise ValueError(f"Source entity '{relation.source_id}' does not exist")
|
|
540
|
+
if not target_entity:
|
|
541
|
+
raise ValueError(f"Target entity '{relation.target_id}' does not exist")
|
|
542
|
+
|
|
543
|
+
# Enforce same-tenant constraint
|
|
544
|
+
if tenant_id is not None:
|
|
545
|
+
source_tenant = source_entity.tenant_id
|
|
546
|
+
target_tenant = target_entity.tenant_id
|
|
547
|
+
if source_tenant != target_tenant:
|
|
548
|
+
raise CrossTenantRelationError(source_tenant, target_tenant)
|
|
549
|
+
|
|
550
|
+
# Set tenant_id on relation if context provided
|
|
551
|
+
if tenant_id is not None and relation.tenant_id is None:
|
|
552
|
+
relation.tenant_id = tenant_id
|
|
553
|
+
|
|
554
|
+
# Serialize data
|
|
555
|
+
properties_json = json.dumps(relation.properties)
|
|
556
|
+
|
|
557
|
+
# Insert relation
|
|
558
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
559
|
+
await self.conn.execute(
|
|
560
|
+
f"""
|
|
561
|
+
INSERT INTO {table_name} (id, relation_type, source_id, target_id, properties, weight)
|
|
562
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
563
|
+
""",
|
|
564
|
+
(
|
|
565
|
+
relation.id,
|
|
566
|
+
relation.relation_type,
|
|
567
|
+
relation.source_id,
|
|
568
|
+
relation.target_id,
|
|
569
|
+
properties_json,
|
|
570
|
+
relation.weight,
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
await self.conn.execute(
|
|
575
|
+
f"""
|
|
576
|
+
INSERT INTO {table_name} (id, tenant_id, relation_type, source_id, target_id, properties, weight)
|
|
577
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
578
|
+
""",
|
|
579
|
+
(
|
|
580
|
+
relation.id,
|
|
581
|
+
tenant_id,
|
|
582
|
+
relation.relation_type,
|
|
583
|
+
relation.source_id,
|
|
584
|
+
relation.target_id,
|
|
585
|
+
properties_json,
|
|
586
|
+
relation.weight,
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if not self._in_transaction:
|
|
591
|
+
await self.conn.commit()
|
|
592
|
+
|
|
593
|
+
async def get_relation(self, relation_id: str, context: Optional[TenantContext] = None) -> Optional[Relation]:
|
|
594
|
+
"""
|
|
595
|
+
Get relation from SQLite database
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
relation_id: Relation ID to retrieve
|
|
599
|
+
context: Optional tenant context for multi-tenant isolation
|
|
600
|
+
"""
|
|
601
|
+
if not self._is_initialized:
|
|
602
|
+
raise RuntimeError("GraphStore not initialized")
|
|
603
|
+
if self.conn is None:
|
|
604
|
+
raise RuntimeError("Database connection not initialized")
|
|
605
|
+
|
|
606
|
+
tenant_id = self._get_tenant_id(context)
|
|
607
|
+
table_name = self._get_table_name("relations", tenant_id)
|
|
608
|
+
|
|
609
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
610
|
+
cursor = await self.conn.execute(
|
|
611
|
+
f"""
|
|
612
|
+
SELECT id, relation_type, source_id, target_id, properties, weight
|
|
613
|
+
FROM {table_name}
|
|
614
|
+
WHERE id = ?
|
|
615
|
+
""",
|
|
616
|
+
(relation_id,),
|
|
617
|
+
)
|
|
618
|
+
row = await cursor.fetchone()
|
|
619
|
+
if not row:
|
|
620
|
+
return None
|
|
621
|
+
return self._row_to_relation(tuple(row), tenant_id=tenant_id)
|
|
622
|
+
else:
|
|
623
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
624
|
+
cursor = await self.conn.execute(
|
|
625
|
+
f"""
|
|
626
|
+
SELECT id, tenant_id, relation_type, source_id, target_id, properties, weight
|
|
627
|
+
FROM {table_name}
|
|
628
|
+
WHERE id = ? AND {tenant_filter}
|
|
629
|
+
""",
|
|
630
|
+
[relation_id] + params,
|
|
631
|
+
)
|
|
632
|
+
row = await cursor.fetchone()
|
|
633
|
+
if not row:
|
|
634
|
+
return None
|
|
635
|
+
return self._row_to_relation_with_tenant(tuple(row))
|
|
636
|
+
|
|
637
|
+
async def update_relation(self, relation: Relation, context: Optional[TenantContext] = None) -> Relation:
|
|
638
|
+
"""
|
|
639
|
+
Update relation in SQLite database
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
relation: Relation to update
|
|
643
|
+
context: Optional tenant context for multi-tenant isolation
|
|
644
|
+
"""
|
|
645
|
+
if not self._is_initialized:
|
|
646
|
+
raise RuntimeError("GraphStore not initialized")
|
|
647
|
+
if self.conn is None:
|
|
648
|
+
raise RuntimeError("Database connection not initialized")
|
|
649
|
+
|
|
650
|
+
tenant_id = self._get_tenant_id(context)
|
|
651
|
+
table_name = self._get_table_name("relations", tenant_id)
|
|
652
|
+
|
|
653
|
+
# Check if relation exists
|
|
654
|
+
existing = await self.get_relation(relation.id, context=context)
|
|
655
|
+
if not existing:
|
|
656
|
+
raise ValueError(f"Relation with ID '{relation.id}' does not exist")
|
|
657
|
+
|
|
658
|
+
# Serialize data
|
|
659
|
+
properties_json = json.dumps(relation.properties)
|
|
660
|
+
|
|
661
|
+
# Update relation
|
|
662
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
663
|
+
await self.conn.execute(
|
|
664
|
+
f"""
|
|
665
|
+
UPDATE {table_name}
|
|
666
|
+
SET relation_type = ?, source_id = ?, target_id = ?, properties = ?,
|
|
667
|
+
weight = ?, updated_at = CURRENT_TIMESTAMP
|
|
668
|
+
WHERE id = ?
|
|
669
|
+
""",
|
|
670
|
+
(
|
|
671
|
+
relation.relation_type,
|
|
672
|
+
relation.source_id,
|
|
673
|
+
relation.target_id,
|
|
674
|
+
properties_json,
|
|
675
|
+
relation.weight,
|
|
676
|
+
relation.id,
|
|
677
|
+
),
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
681
|
+
await self.conn.execute(
|
|
682
|
+
f"""
|
|
683
|
+
UPDATE {table_name}
|
|
684
|
+
SET relation_type = ?, source_id = ?, target_id = ?, properties = ?,
|
|
685
|
+
weight = ?, updated_at = CURRENT_TIMESTAMP
|
|
686
|
+
WHERE id = ? AND {tenant_filter}
|
|
687
|
+
""",
|
|
688
|
+
[
|
|
689
|
+
relation.relation_type,
|
|
690
|
+
relation.source_id,
|
|
691
|
+
relation.target_id,
|
|
692
|
+
properties_json,
|
|
693
|
+
relation.weight,
|
|
694
|
+
relation.id,
|
|
695
|
+
] + params,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
if not self._in_transaction:
|
|
699
|
+
await self.conn.commit()
|
|
700
|
+
|
|
701
|
+
return relation
|
|
702
|
+
|
|
703
|
+
async def delete_relation(self, relation_id: str, context: Optional[TenantContext] = None):
|
|
704
|
+
"""
|
|
705
|
+
Delete relation from SQLite database
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
relation_id: Relation ID to delete
|
|
709
|
+
context: Optional tenant context for multi-tenant isolation
|
|
710
|
+
"""
|
|
711
|
+
if not self._is_initialized:
|
|
712
|
+
raise RuntimeError("GraphStore not initialized")
|
|
713
|
+
if self.conn is None:
|
|
714
|
+
raise RuntimeError("Database connection not initialized")
|
|
715
|
+
|
|
716
|
+
tenant_id = self._get_tenant_id(context)
|
|
717
|
+
table_name = self._get_table_name("relations", tenant_id)
|
|
718
|
+
|
|
719
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
720
|
+
await self.conn.execute(f"DELETE FROM {table_name} WHERE id = ?", (relation_id,))
|
|
721
|
+
else:
|
|
722
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
723
|
+
await self.conn.execute(
|
|
724
|
+
f"DELETE FROM {table_name} WHERE id = ? AND {tenant_filter}",
|
|
725
|
+
[relation_id] + params
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
if not self._in_transaction:
|
|
729
|
+
await self.conn.commit()
|
|
730
|
+
|
|
731
|
+
async def get_neighbors(
|
|
732
|
+
self,
|
|
733
|
+
entity_id: str,
|
|
734
|
+
relation_type: Optional[str] = None,
|
|
735
|
+
direction: str = "outgoing",
|
|
736
|
+
context: Optional[TenantContext] = None,
|
|
737
|
+
) -> List[Entity]:
|
|
738
|
+
"""
|
|
739
|
+
Get neighboring entities connected by relations
|
|
740
|
+
|
|
741
|
+
Implements the base GraphStore interface.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
entity_id: ID of entity to get neighbors for
|
|
745
|
+
relation_type: Optional filter by relation type
|
|
746
|
+
direction: "outgoing", "incoming", or "both"
|
|
747
|
+
context: Optional tenant context for multi-tenant isolation
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
List of neighboring entities
|
|
751
|
+
"""
|
|
752
|
+
if not self._is_initialized:
|
|
753
|
+
raise RuntimeError("GraphStore not initialized")
|
|
754
|
+
if self.conn is None:
|
|
755
|
+
raise RuntimeError("Database connection not initialized")
|
|
756
|
+
|
|
757
|
+
tenant_id = self._get_tenant_id(context)
|
|
758
|
+
entities_table = self._get_table_name("entities", tenant_id)
|
|
759
|
+
relations_table = self._get_table_name("relations", tenant_id)
|
|
760
|
+
|
|
761
|
+
neighbors = []
|
|
762
|
+
|
|
763
|
+
# Build WHERE clause for relation type
|
|
764
|
+
type_filter = ""
|
|
765
|
+
if relation_type:
|
|
766
|
+
type_filter = "AND r.relation_type = ?"
|
|
767
|
+
|
|
768
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
769
|
+
# SEPARATE_SCHEMA: No tenant filtering needed (table is tenant-specific)
|
|
770
|
+
params = [entity_id]
|
|
771
|
+
if relation_type:
|
|
772
|
+
params.append(relation_type)
|
|
773
|
+
|
|
774
|
+
# Outgoing relations
|
|
775
|
+
if direction in ["outgoing", "both"]:
|
|
776
|
+
query = f"""
|
|
777
|
+
SELECT e.id, e.entity_type, e.properties, e.embedding
|
|
778
|
+
FROM {relations_table} r
|
|
779
|
+
JOIN {entities_table} e ON r.target_id = e.id
|
|
780
|
+
WHERE r.source_id = ? {type_filter}
|
|
781
|
+
"""
|
|
782
|
+
|
|
783
|
+
cursor = await self.conn.execute(query, params)
|
|
784
|
+
rows = await cursor.fetchall()
|
|
785
|
+
|
|
786
|
+
for row in rows:
|
|
787
|
+
entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
|
|
788
|
+
neighbors.append(entity)
|
|
789
|
+
|
|
790
|
+
# Incoming relations
|
|
791
|
+
if direction in ["incoming", "both"]:
|
|
792
|
+
params_incoming = [entity_id]
|
|
793
|
+
if relation_type:
|
|
794
|
+
params_incoming.append(relation_type)
|
|
795
|
+
|
|
796
|
+
query = f"""
|
|
797
|
+
SELECT e.id, e.entity_type, e.properties, e.embedding
|
|
798
|
+
FROM {relations_table} r
|
|
799
|
+
JOIN {entities_table} e ON r.source_id = e.id
|
|
800
|
+
WHERE r.target_id = ? {type_filter}
|
|
801
|
+
"""
|
|
802
|
+
|
|
803
|
+
cursor = await self.conn.execute(query, params_incoming)
|
|
804
|
+
rows = await cursor.fetchall()
|
|
805
|
+
|
|
806
|
+
for row in rows:
|
|
807
|
+
entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
|
|
808
|
+
neighbors.append(entity)
|
|
809
|
+
else:
|
|
810
|
+
# SHARED_SCHEMA: Filter by tenant_id
|
|
811
|
+
tenant_filter_r, tenant_params = self._build_tenant_filter(tenant_id, "r")
|
|
812
|
+
tenant_filter_e, tenant_params_e = self._build_tenant_filter(tenant_id, "e")
|
|
813
|
+
|
|
814
|
+
# Outgoing relations
|
|
815
|
+
if direction in ["outgoing", "both"]:
|
|
816
|
+
# Parameter order must match query: JOIN condition (e.tenant), WHERE source_id, [type], r.tenant
|
|
817
|
+
params = tenant_params_e + [entity_id]
|
|
818
|
+
if relation_type:
|
|
819
|
+
params.append(relation_type)
|
|
820
|
+
params.extend(tenant_params)
|
|
821
|
+
|
|
822
|
+
query = f"""
|
|
823
|
+
SELECT e.id, e.tenant_id, e.entity_type, e.properties, e.embedding
|
|
824
|
+
FROM {relations_table} r
|
|
825
|
+
JOIN {entities_table} e ON r.target_id = e.id AND {tenant_filter_e}
|
|
826
|
+
WHERE r.source_id = ? {type_filter} AND {tenant_filter_r}
|
|
827
|
+
"""
|
|
828
|
+
|
|
829
|
+
cursor = await self.conn.execute(query, params)
|
|
830
|
+
rows = await cursor.fetchall()
|
|
831
|
+
|
|
832
|
+
for row in rows:
|
|
833
|
+
entity = self._row_to_entity_with_tenant(tuple(row))
|
|
834
|
+
neighbors.append(entity)
|
|
835
|
+
|
|
836
|
+
# Incoming relations
|
|
837
|
+
if direction in ["incoming", "both"]:
|
|
838
|
+
# Parameter order must match query: JOIN condition (e.tenant), WHERE target_id, [type], r.tenant
|
|
839
|
+
params_incoming = tenant_params_e + [entity_id]
|
|
840
|
+
if relation_type:
|
|
841
|
+
params_incoming.append(relation_type)
|
|
842
|
+
params_incoming.extend(tenant_params)
|
|
843
|
+
|
|
844
|
+
query = f"""
|
|
845
|
+
SELECT e.id, e.tenant_id, e.entity_type, e.properties, e.embedding
|
|
846
|
+
FROM {relations_table} r
|
|
847
|
+
JOIN {entities_table} e ON r.source_id = e.id AND {tenant_filter_e}
|
|
848
|
+
WHERE r.target_id = ? {type_filter} AND {tenant_filter_r}
|
|
849
|
+
"""
|
|
850
|
+
|
|
851
|
+
cursor = await self.conn.execute(query, params_incoming)
|
|
852
|
+
rows = await cursor.fetchall()
|
|
853
|
+
|
|
854
|
+
for row in rows:
|
|
855
|
+
entity = self._row_to_entity_with_tenant(tuple(row))
|
|
856
|
+
neighbors.append(entity)
|
|
857
|
+
|
|
858
|
+
return neighbors
|
|
859
|
+
|
|
860
|
+
# =========================================================================
|
|
861
|
+
# Tier 2: Advanced Interface (SQL-optimized overrides)
|
|
862
|
+
# =========================================================================
|
|
863
|
+
|
|
864
|
+
async def get_all_entities(
|
|
865
|
+
self,
|
|
866
|
+
entity_type: Optional[str] = None,
|
|
867
|
+
limit: Optional[int] = None,
|
|
868
|
+
offset: int = 0,
|
|
869
|
+
context: Optional[TenantContext] = None,
|
|
870
|
+
) -> List[Entity]:
|
|
871
|
+
"""
|
|
872
|
+
Get all entities in the graph store
|
|
873
|
+
|
|
874
|
+
SQL-optimized implementation that uses efficient queries with filtering
|
|
875
|
+
and pagination.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
entity_type: Optional filter by entity type
|
|
879
|
+
limit: Optional maximum number of entities to return
|
|
880
|
+
offset: Number of entities to skip (for pagination)
|
|
881
|
+
context: Optional tenant context for multi-tenant isolation
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
List of entities matching the criteria
|
|
885
|
+
"""
|
|
886
|
+
if not self._is_initialized:
|
|
887
|
+
raise RuntimeError("GraphStore not initialized")
|
|
888
|
+
if self.conn is None:
|
|
889
|
+
raise RuntimeError("Database connection not initialized")
|
|
890
|
+
|
|
891
|
+
tenant_id = self._get_tenant_id(context)
|
|
892
|
+
table_name = self._get_table_name("entities", tenant_id)
|
|
893
|
+
|
|
894
|
+
# Build query with filters
|
|
895
|
+
conditions = []
|
|
896
|
+
params = []
|
|
897
|
+
|
|
898
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
899
|
+
# SEPARATE_SCHEMA: No tenant_id column, tenant filtering via table_name
|
|
900
|
+
if entity_type:
|
|
901
|
+
conditions.append("entity_type = ?")
|
|
902
|
+
params.append(entity_type)
|
|
903
|
+
|
|
904
|
+
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
|
|
905
|
+
|
|
906
|
+
# Build LIMIT and OFFSET clauses
|
|
907
|
+
limit_clause = ""
|
|
908
|
+
if limit is not None and offset > 0:
|
|
909
|
+
limit_clause = f"LIMIT {limit} OFFSET {offset}"
|
|
910
|
+
elif limit is not None:
|
|
911
|
+
limit_clause = f"LIMIT {limit}"
|
|
912
|
+
elif offset > 0:
|
|
913
|
+
limit_clause = f"OFFSET {offset}"
|
|
914
|
+
|
|
915
|
+
# Execute query
|
|
916
|
+
query = f"""
|
|
917
|
+
SELECT id, entity_type, properties, embedding
|
|
918
|
+
FROM {table_name}
|
|
919
|
+
{where_clause}
|
|
920
|
+
{limit_clause}
|
|
921
|
+
"""
|
|
922
|
+
|
|
923
|
+
cursor = await self.conn.execute(query, params)
|
|
924
|
+
rows = await cursor.fetchall()
|
|
925
|
+
|
|
926
|
+
# Convert rows to entities
|
|
927
|
+
entities = []
|
|
928
|
+
for row in rows:
|
|
929
|
+
entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
|
|
930
|
+
entities.append(entity)
|
|
931
|
+
else:
|
|
932
|
+
# SHARED_SCHEMA: Filter by tenant_id column
|
|
933
|
+
tenant_filter, tenant_params = self._build_tenant_filter(tenant_id)
|
|
934
|
+
if tenant_filter:
|
|
935
|
+
conditions.append(tenant_filter)
|
|
936
|
+
params.extend(tenant_params)
|
|
937
|
+
|
|
938
|
+
# Entity type filtering
|
|
939
|
+
if entity_type:
|
|
940
|
+
conditions.append("entity_type = ?")
|
|
941
|
+
params.append(entity_type)
|
|
942
|
+
|
|
943
|
+
# Build WHERE clause
|
|
944
|
+
where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
|
|
945
|
+
|
|
946
|
+
# Build LIMIT and OFFSET clauses
|
|
947
|
+
limit_clause = ""
|
|
948
|
+
if limit is not None and offset > 0:
|
|
949
|
+
limit_clause = f"LIMIT {limit} OFFSET {offset}"
|
|
950
|
+
elif limit is not None:
|
|
951
|
+
limit_clause = f"LIMIT {limit}"
|
|
952
|
+
elif offset > 0:
|
|
953
|
+
limit_clause = f"OFFSET {offset}"
|
|
954
|
+
|
|
955
|
+
# Execute query
|
|
956
|
+
query = f"""
|
|
957
|
+
SELECT id, tenant_id, entity_type, properties, embedding
|
|
958
|
+
FROM {table_name}
|
|
959
|
+
{where_clause}
|
|
960
|
+
{limit_clause}
|
|
961
|
+
"""
|
|
962
|
+
|
|
963
|
+
cursor = await self.conn.execute(query, params)
|
|
964
|
+
rows = await cursor.fetchall()
|
|
965
|
+
|
|
966
|
+
# Convert rows to entities
|
|
967
|
+
entities = []
|
|
968
|
+
for row in rows:
|
|
969
|
+
entity = self._row_to_entity_with_tenant(tuple(row))
|
|
970
|
+
entities.append(entity)
|
|
971
|
+
|
|
972
|
+
return entities
|
|
973
|
+
|
|
974
|
+
async def vector_search(
|
|
975
|
+
self,
|
|
976
|
+
query_embedding: List[float],
|
|
977
|
+
entity_type: Optional[str] = None,
|
|
978
|
+
max_results: int = 10,
|
|
979
|
+
score_threshold: float = 0.0,
|
|
980
|
+
context: Optional[TenantContext] = None,
|
|
981
|
+
) -> List[Tuple[Entity, float]]:
|
|
982
|
+
"""
|
|
983
|
+
SQL-optimized vector similarity search
|
|
984
|
+
|
|
985
|
+
Performs cosine similarity search over entity embeddings stored in SQLite.
|
|
986
|
+
This implementation fetches all candidates and computes similarity in Python.
|
|
987
|
+
|
|
988
|
+
For production scale, consider:
|
|
989
|
+
- pgvector extension (PostgreSQL)
|
|
990
|
+
- Dedicated vector database (Qdrant, Milvus)
|
|
991
|
+
- Pre-computed ANN indexes
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
query_embedding: Query vector
|
|
995
|
+
entity_type: Optional filter by entity type
|
|
996
|
+
max_results: Maximum number of results to return
|
|
997
|
+
score_threshold: Minimum similarity score (0.0-1.0)
|
|
998
|
+
context: Optional tenant context for multi-tenant isolation
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
List of (entity, similarity_score) tuples, sorted descending
|
|
1002
|
+
"""
|
|
1003
|
+
if not self._is_initialized:
|
|
1004
|
+
raise RuntimeError("GraphStore not initialized")
|
|
1005
|
+
if self.conn is None:
|
|
1006
|
+
raise RuntimeError("Database connection not initialized")
|
|
1007
|
+
|
|
1008
|
+
if not query_embedding:
|
|
1009
|
+
raise ValueError("Query embedding cannot be empty")
|
|
1010
|
+
|
|
1011
|
+
tenant_id = self._get_tenant_id(context)
|
|
1012
|
+
table_name = self._get_table_name("entities", tenant_id)
|
|
1013
|
+
|
|
1014
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
1015
|
+
# SEPARATE_SCHEMA: No tenant filtering needed
|
|
1016
|
+
type_filter = "WHERE entity_type = ?" if entity_type else ""
|
|
1017
|
+
params = [entity_type] if entity_type else []
|
|
1018
|
+
|
|
1019
|
+
query = f"""
|
|
1020
|
+
SELECT id, entity_type, properties, embedding
|
|
1021
|
+
FROM {table_name}
|
|
1022
|
+
{type_filter}
|
|
1023
|
+
"""
|
|
1024
|
+
|
|
1025
|
+
cursor = await self.conn.execute(query, params)
|
|
1026
|
+
rows = await cursor.fetchall()
|
|
1027
|
+
|
|
1028
|
+
# Compute similarities
|
|
1029
|
+
scored_entities = []
|
|
1030
|
+
for row in rows:
|
|
1031
|
+
entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
|
|
1032
|
+
|
|
1033
|
+
if not entity.embedding:
|
|
1034
|
+
continue
|
|
1035
|
+
|
|
1036
|
+
similarity = self._cosine_similarity(query_embedding, entity.embedding)
|
|
1037
|
+
if similarity >= score_threshold:
|
|
1038
|
+
scored_entities.append((entity, similarity))
|
|
1039
|
+
else:
|
|
1040
|
+
# SHARED_SCHEMA: Filter by tenant_id
|
|
1041
|
+
tenant_filter, tenant_params = self._build_tenant_filter(tenant_id)
|
|
1042
|
+
|
|
1043
|
+
if entity_type:
|
|
1044
|
+
where_clause = f"WHERE {tenant_filter} AND entity_type = ?"
|
|
1045
|
+
params = tenant_params + [entity_type]
|
|
1046
|
+
else:
|
|
1047
|
+
where_clause = f"WHERE {tenant_filter}"
|
|
1048
|
+
params = tenant_params
|
|
1049
|
+
|
|
1050
|
+
query = f"""
|
|
1051
|
+
SELECT id, tenant_id, entity_type, properties, embedding
|
|
1052
|
+
FROM {table_name}
|
|
1053
|
+
{where_clause}
|
|
1054
|
+
"""
|
|
1055
|
+
|
|
1056
|
+
cursor = await self.conn.execute(query, params)
|
|
1057
|
+
rows = await cursor.fetchall()
|
|
1058
|
+
|
|
1059
|
+
# Compute similarities
|
|
1060
|
+
scored_entities = []
|
|
1061
|
+
for row in rows:
|
|
1062
|
+
entity = self._row_to_entity_with_tenant(tuple(row))
|
|
1063
|
+
|
|
1064
|
+
if not entity.embedding:
|
|
1065
|
+
continue
|
|
1066
|
+
|
|
1067
|
+
similarity = self._cosine_similarity(query_embedding, entity.embedding)
|
|
1068
|
+
if similarity >= score_threshold:
|
|
1069
|
+
scored_entities.append((entity, similarity))
|
|
1070
|
+
|
|
1071
|
+
# Sort by score descending and return top max_results
|
|
1072
|
+
scored_entities.sort(key=lambda x: x[1], reverse=True)
|
|
1073
|
+
return scored_entities[:max_results]
|
|
1074
|
+
|
|
1075
|
+
async def traverse(
|
|
1076
|
+
self,
|
|
1077
|
+
start_entity_id: str,
|
|
1078
|
+
relation_type: Optional[str] = None,
|
|
1079
|
+
max_depth: int = 3,
|
|
1080
|
+
max_results: int = 100,
|
|
1081
|
+
context: Optional[TenantContext] = None,
|
|
1082
|
+
) -> List[Path]:
|
|
1083
|
+
"""
|
|
1084
|
+
SQL-optimized traversal using recursive CTE
|
|
1085
|
+
|
|
1086
|
+
This overrides the default Tier 2 implementation for better performance.
|
|
1087
|
+
Uses recursive CTEs in SQLite for efficient graph traversal.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
start_entity_id: Starting entity ID
|
|
1091
|
+
relation_type: Optional filter by relation type
|
|
1092
|
+
max_depth: Maximum traversal depth
|
|
1093
|
+
max_results: Maximum number of paths to return
|
|
1094
|
+
context: Optional tenant context for multi-tenant isolation
|
|
1095
|
+
"""
|
|
1096
|
+
if not self._is_initialized:
|
|
1097
|
+
raise RuntimeError("GraphStore not initialized")
|
|
1098
|
+
|
|
1099
|
+
# For SQLite, we'll use the default implementation from base class
|
|
1100
|
+
# which uses BFS with get_neighbors(). While recursive CTEs are powerful,
|
|
1101
|
+
# building full Path objects with them is complex. The default is sufficient.
|
|
1102
|
+
# Backends with native graph query languages (e.g., Neo4j with Cypher)
|
|
1103
|
+
# should override this for better performance.
|
|
1104
|
+
return await self._default_traverse_bfs(start_entity_id, relation_type, max_depth, max_results, context)
|
|
1105
|
+
|
|
1106
|
+
# =========================================================================
|
|
1107
|
+
# Helper Methods
|
|
1108
|
+
# =========================================================================
|
|
1109
|
+
|
|
1110
|
+
def _row_to_entity(self, row: tuple, tenant_id: Optional[str] = None) -> Entity:
|
|
1111
|
+
"""Convert database row to Entity object (for SEPARATE_SCHEMA without tenant_id column)"""
|
|
1112
|
+
entity_id, entity_type, properties_json, embedding_blob = row
|
|
1113
|
+
|
|
1114
|
+
properties = json.loads(properties_json)
|
|
1115
|
+
embedding = self._deserialize_embedding(embedding_blob) if embedding_blob else None
|
|
1116
|
+
|
|
1117
|
+
return Entity(
|
|
1118
|
+
id=entity_id,
|
|
1119
|
+
entity_type=entity_type,
|
|
1120
|
+
properties=properties,
|
|
1121
|
+
embedding=embedding,
|
|
1122
|
+
tenant_id=tenant_id,
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
def _row_to_entity_with_tenant(self, row: tuple) -> Entity:
|
|
1126
|
+
"""Convert database row to Entity object (for SHARED_SCHEMA with tenant_id column)"""
|
|
1127
|
+
entity_id, tenant_id, entity_type, properties_json, embedding_blob = row
|
|
1128
|
+
|
|
1129
|
+
properties = json.loads(properties_json)
|
|
1130
|
+
embedding = self._deserialize_embedding(embedding_blob) if embedding_blob else None
|
|
1131
|
+
|
|
1132
|
+
return Entity(
|
|
1133
|
+
id=entity_id,
|
|
1134
|
+
entity_type=entity_type,
|
|
1135
|
+
properties=properties,
|
|
1136
|
+
embedding=embedding,
|
|
1137
|
+
tenant_id=tenant_id,
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
def _row_to_relation(self, row: tuple, tenant_id: Optional[str] = None) -> Relation:
|
|
1141
|
+
"""Convert database row to Relation object (for SEPARATE_SCHEMA without tenant_id column)"""
|
|
1142
|
+
rel_id, rel_type, source_id, target_id, properties_json, weight = row
|
|
1143
|
+
|
|
1144
|
+
properties = json.loads(properties_json)
|
|
1145
|
+
|
|
1146
|
+
return Relation(
|
|
1147
|
+
id=rel_id,
|
|
1148
|
+
relation_type=rel_type,
|
|
1149
|
+
source_id=source_id,
|
|
1150
|
+
target_id=target_id,
|
|
1151
|
+
properties=properties,
|
|
1152
|
+
weight=weight,
|
|
1153
|
+
tenant_id=tenant_id,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
def _row_to_relation_with_tenant(self, row: tuple) -> Relation:
|
|
1157
|
+
"""Convert database row to Relation object (for SHARED_SCHEMA with tenant_id column)"""
|
|
1158
|
+
rel_id, tenant_id, rel_type, source_id, target_id, properties_json, weight = row
|
|
1159
|
+
|
|
1160
|
+
properties = json.loads(properties_json)
|
|
1161
|
+
|
|
1162
|
+
return Relation(
|
|
1163
|
+
id=rel_id,
|
|
1164
|
+
relation_type=rel_type,
|
|
1165
|
+
source_id=source_id,
|
|
1166
|
+
target_id=target_id,
|
|
1167
|
+
properties=properties,
|
|
1168
|
+
weight=weight,
|
|
1169
|
+
tenant_id=tenant_id,
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
def _serialize_embedding(self, embedding: List[float]) -> bytes:
|
|
1173
|
+
"""Serialize embedding vector to bytes"""
|
|
1174
|
+
import struct
|
|
1175
|
+
|
|
1176
|
+
return struct.pack(f"{len(embedding)}f", *embedding)
|
|
1177
|
+
|
|
1178
|
+
def _deserialize_embedding(self, blob: bytes) -> List[float]:
|
|
1179
|
+
"""Deserialize embedding vector from bytes"""
|
|
1180
|
+
import struct
|
|
1181
|
+
|
|
1182
|
+
count = len(blob) // 4 # 4 bytes per float
|
|
1183
|
+
return list(struct.unpack(f"{count}f", blob))
|
|
1184
|
+
|
|
1185
|
+
def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
|
|
1186
|
+
"""
|
|
1187
|
+
Compute cosine similarity between two vectors
|
|
1188
|
+
|
|
1189
|
+
Returns value between -1 and 1, where 1 means identical direction.
|
|
1190
|
+
Normalized to 0-1 range for consistency.
|
|
1191
|
+
|
|
1192
|
+
Args:
|
|
1193
|
+
vec1: First vector
|
|
1194
|
+
vec2: Second vector
|
|
1195
|
+
|
|
1196
|
+
Returns:
|
|
1197
|
+
Cosine similarity (0.0-1.0)
|
|
1198
|
+
"""
|
|
1199
|
+
if len(vec1) != len(vec2):
|
|
1200
|
+
return 0.0
|
|
1201
|
+
|
|
1202
|
+
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
|
1203
|
+
magnitude1 = sum(a * a for a in vec1) ** 0.5
|
|
1204
|
+
magnitude2 = sum(b * b for b in vec2) ** 0.5
|
|
1205
|
+
|
|
1206
|
+
if magnitude1 == 0 or magnitude2 == 0:
|
|
1207
|
+
return 0.0
|
|
1208
|
+
|
|
1209
|
+
# Cosine similarity ranges from -1 to 1, normalize to 0 to 1
|
|
1210
|
+
similarity = dot_product / (magnitude1 * magnitude2)
|
|
1211
|
+
return (similarity + 1) / 2
|
|
1212
|
+
|
|
1213
|
+
async def get_stats(self, context: Optional[TenantContext] = None) -> Dict[str, Any]:
|
|
1214
|
+
"""
|
|
1215
|
+
Get statistics about the SQLite graph store
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
context: Optional tenant context for tenant-scoped stats
|
|
1219
|
+
"""
|
|
1220
|
+
if not self._is_initialized:
|
|
1221
|
+
raise RuntimeError("GraphStore not initialized")
|
|
1222
|
+
if self.conn is None:
|
|
1223
|
+
raise RuntimeError("Database connection not initialized")
|
|
1224
|
+
|
|
1225
|
+
tenant_id = self._get_tenant_id(context)
|
|
1226
|
+
entities_table = self._get_table_name("entities", tenant_id)
|
|
1227
|
+
relations_table = self._get_table_name("relations", tenant_id)
|
|
1228
|
+
|
|
1229
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
|
|
1230
|
+
# Check if tenant tables exist
|
|
1231
|
+
cursor = await self.conn.execute(
|
|
1232
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
|
1233
|
+
(entities_table,)
|
|
1234
|
+
)
|
|
1235
|
+
table_exists = await cursor.fetchone()
|
|
1236
|
+
|
|
1237
|
+
if not table_exists:
|
|
1238
|
+
entity_count = 0
|
|
1239
|
+
relation_count = 0
|
|
1240
|
+
else:
|
|
1241
|
+
cursor = await self.conn.execute(f"SELECT COUNT(*) FROM {entities_table}")
|
|
1242
|
+
entity_row = await cursor.fetchone()
|
|
1243
|
+
entity_count = entity_row[0] if entity_row else 0
|
|
1244
|
+
|
|
1245
|
+
cursor = await self.conn.execute(f"SELECT COUNT(*) FROM {relations_table}")
|
|
1246
|
+
relation_row = await cursor.fetchone()
|
|
1247
|
+
relation_count = relation_row[0] if relation_row else 0
|
|
1248
|
+
else:
|
|
1249
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
1250
|
+
|
|
1251
|
+
cursor = await self.conn.execute(
|
|
1252
|
+
f"SELECT COUNT(*) FROM {entities_table} WHERE {tenant_filter}",
|
|
1253
|
+
params
|
|
1254
|
+
)
|
|
1255
|
+
entity_row = await cursor.fetchone()
|
|
1256
|
+
entity_count = entity_row[0] if entity_row else 0
|
|
1257
|
+
|
|
1258
|
+
cursor = await self.conn.execute(
|
|
1259
|
+
f"SELECT COUNT(*) FROM {relations_table} WHERE {tenant_filter}",
|
|
1260
|
+
params
|
|
1261
|
+
)
|
|
1262
|
+
relation_row = await cursor.fetchone()
|
|
1263
|
+
relation_count = relation_row[0] if relation_row else 0
|
|
1264
|
+
|
|
1265
|
+
# Database file size
|
|
1266
|
+
file_size = 0
|
|
1267
|
+
if self.db_path != ":memory:":
|
|
1268
|
+
try:
|
|
1269
|
+
file_size = PathLibPath(self.db_path).stat().st_size
|
|
1270
|
+
except (OSError, ValueError):
|
|
1271
|
+
pass
|
|
1272
|
+
|
|
1273
|
+
return {
|
|
1274
|
+
"entity_count": entity_count,
|
|
1275
|
+
"relation_count": relation_count,
|
|
1276
|
+
"storage_type": "sqlite",
|
|
1277
|
+
"db_path": self.db_path,
|
|
1278
|
+
"db_size_bytes": file_size,
|
|
1279
|
+
"is_initialized": self._is_initialized,
|
|
1280
|
+
"isolation_mode": self.isolation_mode.value,
|
|
1281
|
+
"tenant_id": tenant_id,
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async def clear(self, context: Optional[TenantContext] = None):
|
|
1285
|
+
"""
|
|
1286
|
+
Clear data from SQLite database
|
|
1287
|
+
|
|
1288
|
+
Args:
|
|
1289
|
+
context: Optional tenant context for multi-tenant isolation.
|
|
1290
|
+
If provided, clears only data for the specified tenant.
|
|
1291
|
+
If None, clears all data.
|
|
1292
|
+
"""
|
|
1293
|
+
if not self._is_initialized:
|
|
1294
|
+
raise RuntimeError("GraphStore not initialized")
|
|
1295
|
+
if self.conn is None:
|
|
1296
|
+
raise RuntimeError("Database connection not initialized")
|
|
1297
|
+
|
|
1298
|
+
tenant_id = self._get_tenant_id(context)
|
|
1299
|
+
|
|
1300
|
+
if tenant_id is None:
|
|
1301
|
+
# Clear all data (global and all tenants)
|
|
1302
|
+
await self.conn.execute("DELETE FROM relations")
|
|
1303
|
+
await self.conn.execute("DELETE FROM entities")
|
|
1304
|
+
|
|
1305
|
+
# Drop all tenant-specific tables for SEPARATE_SCHEMA
|
|
1306
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
|
|
1307
|
+
cursor = await self.conn.execute(
|
|
1308
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'tenant_%'"
|
|
1309
|
+
)
|
|
1310
|
+
tables = await cursor.fetchall()
|
|
1311
|
+
for (table_name,) in tables:
|
|
1312
|
+
await self.conn.execute(f"DROP TABLE IF EXISTS {table_name}")
|
|
1313
|
+
self._initialized_tenant_tables.clear()
|
|
1314
|
+
else:
|
|
1315
|
+
# Clear tenant-specific data
|
|
1316
|
+
if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
|
|
1317
|
+
entities_table = self._get_table_name("entities", tenant_id)
|
|
1318
|
+
relations_table = self._get_table_name("relations", tenant_id)
|
|
1319
|
+
|
|
1320
|
+
# Drop tenant tables
|
|
1321
|
+
await self.conn.execute(f"DROP TABLE IF EXISTS {relations_table}")
|
|
1322
|
+
await self.conn.execute(f"DROP TABLE IF EXISTS {entities_table}")
|
|
1323
|
+
self._initialized_tenant_tables.discard(tenant_id)
|
|
1324
|
+
else:
|
|
1325
|
+
# Delete from shared tables with tenant filter
|
|
1326
|
+
tenant_filter, params = self._build_tenant_filter(tenant_id)
|
|
1327
|
+
await self.conn.execute(
|
|
1328
|
+
f"DELETE FROM relations WHERE {tenant_filter}",
|
|
1329
|
+
params
|
|
1330
|
+
)
|
|
1331
|
+
await self.conn.execute(
|
|
1332
|
+
f"DELETE FROM entities WHERE {tenant_filter}",
|
|
1333
|
+
params
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
if not self._in_transaction:
|
|
1337
|
+
await self.conn.commit()
|
|
1338
|
+
|
|
1339
|
+
async def migrate_add_tenant_id(self):
|
|
1340
|
+
"""
|
|
1341
|
+
Migration script to add tenant_id column to existing databases.
|
|
1342
|
+
|
|
1343
|
+
This should be run once when upgrading an existing database to support multi-tenancy.
|
|
1344
|
+
"""
|
|
1345
|
+
if not self._is_initialized:
|
|
1346
|
+
raise RuntimeError("GraphStore not initialized")
|
|
1347
|
+
if self.conn is None:
|
|
1348
|
+
raise RuntimeError("Database connection not initialized")
|
|
1349
|
+
|
|
1350
|
+
# Check if tenant_id column already exists
|
|
1351
|
+
cursor = await self.conn.execute("PRAGMA table_info(entities)")
|
|
1352
|
+
columns = await cursor.fetchall()
|
|
1353
|
+
column_names = [col[1] for col in columns]
|
|
1354
|
+
|
|
1355
|
+
if "tenant_id" in column_names:
|
|
1356
|
+
return # Migration already applied
|
|
1357
|
+
|
|
1358
|
+
# Apply migration
|
|
1359
|
+
try:
|
|
1360
|
+
await self.conn.execute("ALTER TABLE entities ADD COLUMN tenant_id TEXT")
|
|
1361
|
+
await self.conn.execute("ALTER TABLE relations ADD COLUMN tenant_id TEXT")
|
|
1362
|
+
|
|
1363
|
+
# Create tenant indexes
|
|
1364
|
+
await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)")
|
|
1365
|
+
await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_tenant_type ON entities(tenant_id, entity_type)")
|
|
1366
|
+
await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)")
|
|
1367
|
+
await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_relations_tenant_source ON relations(tenant_id, source_id)")
|
|
1368
|
+
await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_relations_tenant_target ON relations(tenant_id, target_id)")
|
|
1369
|
+
|
|
1370
|
+
await self.conn.commit()
|
|
1371
|
+
except Exception as e:
|
|
1372
|
+
await self.conn.rollback()
|
|
1373
|
+
raise RuntimeError(f"Migration failed: {e}")
|