quantum-framework 0.18.0__tar.gz → 0.19.0__tar.gz
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.
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/PKG-INFO +1 -1
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/pyproject.toml +1 -1
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/runner.py +15 -1
- quantum_framework-0.19.0/quantum/cli/schema_plan.py +273 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/ast_nodes.py +19 -1
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/parser.py +17 -1
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/action_handler.py +3 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/database_service.py +12 -0
- quantum_framework-0.19.0/quantum/runtime/history.py +203 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_page_renderer.py +33 -2
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_view.py +11 -1
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/web_server.py +11 -5
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/PKG-INFO +1 -1
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/SOURCES.txt +2 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/LICENSE +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/README.md +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/admin.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/check.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/build.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/dev.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/docs.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/lint.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/migrate.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/new.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/serve.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/commands/test.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/deploy.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/hot_reload.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/jobs.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/mq.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/pkg.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/server_process.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/cli/utils.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/base_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/cli.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/expression_transformer.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/javascript/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/javascript/generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/optimizer.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/python/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/python/generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/python/runtime.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/compiler/transpiler.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/ast_nodes.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/config_env.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/docs_links.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/expression_diagnostics.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/expression_stdlib.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/expressions.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/agents/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/agents/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/conditionals/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/conditionals/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/data_import/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/data_import/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/data_import/src/runtime.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/dump/src/runtime.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/functions/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/functions/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/game_engine_2d/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/game_engine_2d/src/ast_nodes.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/game_engine_2d/src/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/invocation/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/invocation/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/invocation/src/runtime.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/knowledge_base/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/knowledge_base/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/knowledge_base/src/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/logging/src/runtime.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/loops/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/loops/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/query/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/query/src/query_validators.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/state_management/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/state_management/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/terminal_engine/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/terminal_engine/src/ast_nodes.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/terminal_engine/src/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/testing_engine/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/testing_engine/src/ast_nodes.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/testing_engine/src/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/theming/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/theming/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/theming/src/presets.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/websocket/src/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/websocket/src/ast_node.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/guards.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/html_compat.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parser_registry.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/agent_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/knowledge_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/llm_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/ai/team_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/base.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/composition/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/composition/import_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/composition/slot_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/if_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/loop_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/control_flow/set_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/data_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/invoke_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/query_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/data/transaction_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/events/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/events/dispatch_event_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/action_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/flash_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/forms/redirect_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/function_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/param_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/functions/return_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/html/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/html/component_call_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/html/html_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/job_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/schedule_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/jobs/thread_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/message_ack_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/message_nack_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/message_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/queue_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/messaging/websocket_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/routing/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/routing/route_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/pyclass_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/pydecorator_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/pyimport_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/scripting/python_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/dump_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/file_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/log_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/parsers/services/mail_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/tiers.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/xml_lines.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/manager.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/manifest.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/registry.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/packages/resolver.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/hooks.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/loader.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/manifest.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/plugins/registry.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/memory_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/rabbitmq_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/redis_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/adapters/sqlite_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/agent_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ast_cache.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/auth_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/component.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/component_composer.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/component_resolver.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/conditions.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/db_schema.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/dev_panel.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/email_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/error_handler.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/execution_context.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executor_registry.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/agent_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/knowledge_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/llm_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/ai/team_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/base.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/if_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/loop_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/redirect_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/control_flow/set_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/data_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/invoke_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/query_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/data/transaction_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/job_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/schedule_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/jobs/thread_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/message_ack_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/message_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/queue_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/messaging/websocket_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/pyclass_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/pyimport_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/scripting/python_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/dump_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/file_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/log_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/executors/services/mail_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/expression_cache.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/file_upload_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/function_registry.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/game_builder.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/game_code_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/game_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/godot_code_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/godot_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/job_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/knowledge_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/llm_cache.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/llm_providers.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/llm_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/local_server.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/logging_setup.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/message_broker.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/message_queue_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/param_validation.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/pypy_compat.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/python_bridge.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/query_validators.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/renderer.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/service_container.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/table_params.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/terminal_builder.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/terminal_code_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/terminal_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/testing_builder.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/testing_code_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/testing_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_builder.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_console.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_desktop.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_form_rules.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_html_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_html_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_mobile_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_mobile_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_pager.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_table_edit.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_textual_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_textual_templates.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_tokens.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/ui_validator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/validators.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/websocket_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/websocket_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/websocket_transport.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/runtime/wsgi.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/services.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/utils/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/assets/quantum-admin-connectors.js +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/assets/quantum-admin.css +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/assets/quantum-source-highlight.js +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/audit_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/auth_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/component_discovery.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/config_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/connector_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/crud.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/database.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/db_setup_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/deploy_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/docker_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/environment_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/git_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/health_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/job_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/log_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/main.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/models.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/pipeline_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/resource_manager.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/schemas.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/secret_manager.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/settings_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/template_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/test_execution_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/test_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/webhook_service.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/backend/websocket_manager.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/_layout/AdminShell.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/agents.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/app/[name].q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/applications.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/component/[...path].q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/components.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/connectors.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/dashboard.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/database.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/features.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/index.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/jobs.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/login.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/logout.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/settings.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/source.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/components/admin/tests.q +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/run.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/__init__.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/_base.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/_component_test_generator.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/_yaml_editor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/apps.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/auth.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/components.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/connectors.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/projects.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/repository.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/settings.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_admin/services/yaml_import.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/dependency_links.txt +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/entry_points.txt +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/requires.txt +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum_framework.egg-info/top_level.txt +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/setup.cfg +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_agent.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_animations.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ast_cache.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_cli_commands.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_component_library.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_expression_cache.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_form_validation.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_codegen.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_e2e.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_engine.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_game_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_godot_codegen.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_job_executor.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_llm_providers.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_message_broker.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_message_queue.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_mobile_target.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_multi_agent.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_packages.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_plugins.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_pypy_compat.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_python_scripting.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_terminal_codegen.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_terminal_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_testing_codegen.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_testing_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_theming.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_html_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_parity.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_parser.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_ui_textual_adapter.py +0 -0
- {quantum_framework-0.18.0 → quantum_framework-0.19.0}/tests/test_websocket.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantum-framework"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.19.0"
|
|
8
8
|
description = "Full-stack declarative framework for building web, desktop, and mobile applications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -399,6 +399,15 @@ Examples:
|
|
|
399
399
|
migrate_create_parser = migrate_subparsers.add_parser('create', help='Create a new migration')
|
|
400
400
|
migrate_create_parser.add_argument('name', help='Migration name')
|
|
401
401
|
|
|
402
|
+
# M6 (DB-10): schema.sql as the source of truth
|
|
403
|
+
migrate_plan_parser = migrate_subparsers.add_parser(
|
|
404
|
+
'plan', help='Compare schema.sql with the migrations and plan the migration')
|
|
405
|
+
migrate_plan_parser.add_argument('--schema', default='schema.sql', help='Schema file (default: schema.sql)')
|
|
406
|
+
migrate_plan_parser.add_argument('--write', metavar='NAME', help='Save the plan as migration V00N_NAME')
|
|
407
|
+
migrate_plan_parser.add_argument('--yes', action='store_true', help='Do not ask before writing')
|
|
408
|
+
migrate_plan_parser.add_argument('--allow-data-loss', action='store_true',
|
|
409
|
+
help='Write a plan that drops tables or columns')
|
|
410
|
+
|
|
402
411
|
# Parse arguments
|
|
403
412
|
args = parser.parse_args()
|
|
404
413
|
|
|
@@ -541,13 +550,18 @@ Examples:
|
|
|
541
550
|
if failed:
|
|
542
551
|
sys.exit(1)
|
|
543
552
|
|
|
553
|
+
elif args.migrate_command == 'plan':
|
|
554
|
+
from quantum.cli.schema_plan import run_plan
|
|
555
|
+
sys.exit(run_plan(Path.cwd(), args.schema, args.write, args.yes, args.allow_data_loss,
|
|
556
|
+
interactive=sys.stdin.isatty()))
|
|
557
|
+
|
|
544
558
|
elif args.migrate_command == 'create':
|
|
545
559
|
filepath = runner.create(args.name)
|
|
546
560
|
print(f"Created migration: {filepath}")
|
|
547
561
|
print(f"Also created rollback: {filepath.with_suffix('.down.sql')}")
|
|
548
562
|
|
|
549
563
|
else:
|
|
550
|
-
print("Usage: quantum migrate <status|up|down|create> [options]")
|
|
564
|
+
print("Usage: quantum migrate <status|up|down|create|plan> [options]")
|
|
551
565
|
sys.exit(1)
|
|
552
566
|
|
|
553
567
|
sys.exit(0)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""`quantum migrate plan`: a declarative schema (M6, DB-10).
|
|
2
|
+
|
|
3
|
+
You edit `schema.sql` — the tables as they should be. `quantum migrate plan`
|
|
4
|
+
compares it with the schema the migrations produce, shows what changes, marks
|
|
5
|
+
what loses data, and `--write` saves the migration (and its rollback) after
|
|
6
|
+
confirmation. `quantum migrate up` applies it as any other.
|
|
7
|
+
|
|
8
|
+
Both sides are built in in-memory SQLite databases — the migrations applied in
|
|
9
|
+
order, and schema.sql — and compared with the schema reader of `quantum check`
|
|
10
|
+
(quantum/runtime/db_schema.py). The development database is not read: the
|
|
11
|
+
plan is the same on any machine and in CI.
|
|
12
|
+
|
|
13
|
+
SQLite can add a column but not change one, so any other change to a table
|
|
14
|
+
rebuilds it: create the new table, copy the columns both have, drop the old,
|
|
15
|
+
rename. Before a plan is written it is checked: applied to the migrations'
|
|
16
|
+
schema it must produce exactly schema.sql's.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import sqlite3
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, List, Optional, Tuple
|
|
24
|
+
|
|
25
|
+
from quantum.runtime.db_schema import Column, read_schema
|
|
26
|
+
|
|
27
|
+
# Tables that belong to Quantum, not to the app's schema.
|
|
28
|
+
INTERNAL_TABLES = {'_migrations', 'quantum_history'}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SchemaPlanError(Exception):
|
|
32
|
+
"""The plan cannot be made (no schema.sql, invalid SQL, a change it cannot express)."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Step:
|
|
37
|
+
kind: str # add-table, drop-table, add-column, rebuild, add-index, drop-index
|
|
38
|
+
target: str # "posts" or "posts.summary"
|
|
39
|
+
detail: str = ''
|
|
40
|
+
sql: List[str] = field(default_factory=list)
|
|
41
|
+
loses_data: bool = False
|
|
42
|
+
warning: str = ''
|
|
43
|
+
|
|
44
|
+
def line(self) -> str:
|
|
45
|
+
mark = {'add-table': '+', 'add-column': '+', 'add-index': '+', 'drop-table': '-',
|
|
46
|
+
'drop-index': '-', 'rebuild': '~'}.get(self.kind, '?')
|
|
47
|
+
text = f'{mark} {self.kind.replace("-", " ")} {self.target}'
|
|
48
|
+
if self.detail:
|
|
49
|
+
text += f' ({self.detail})'
|
|
50
|
+
if self.loses_data:
|
|
51
|
+
text += ' !! loses data'
|
|
52
|
+
if self.warning:
|
|
53
|
+
text += f' !! {self.warning}'
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _database(scripts: List[Tuple[str, str]]) -> sqlite3.Connection:
|
|
58
|
+
conn = sqlite3.connect(':memory:')
|
|
59
|
+
for origin, sql in scripts:
|
|
60
|
+
try:
|
|
61
|
+
conn.executescript(sql)
|
|
62
|
+
except sqlite3.Error as exc:
|
|
63
|
+
raise SchemaPlanError(f'{origin}: {exc}') from exc
|
|
64
|
+
return conn
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _migration_scripts(migrations_dir: Path) -> List[Tuple[str, str]]:
|
|
68
|
+
if not migrations_dir.exists():
|
|
69
|
+
return []
|
|
70
|
+
files = [f for f in sorted(migrations_dir.glob('*.sql')) if not f.name.endswith('.down.sql')]
|
|
71
|
+
return [(f.name, f.read_text(encoding='utf-8')) for f in files if '_' in f.stem]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _tables(conn) -> Dict[str, object]:
|
|
75
|
+
return {name: t for name, t in read_schema(conn).items() if name not in INTERNAL_TABLES}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _create_sql(conn, table: str) -> str:
|
|
79
|
+
return conn.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
80
|
+
(table,)).fetchone()[0]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _indexes(conn) -> Dict[str, Tuple[str, str]]:
|
|
84
|
+
"""Explicit indexes: name -> (table, sql)."""
|
|
85
|
+
return {name: (table, sql) for name, table, sql in conn.execute(
|
|
86
|
+
"SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index' AND sql IS NOT NULL")
|
|
87
|
+
if table not in INTERNAL_TABLES}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _column_sql(column: Column) -> str:
|
|
91
|
+
parts = [f'"{column.name}"']
|
|
92
|
+
if column.type:
|
|
93
|
+
parts.append(column.type)
|
|
94
|
+
if column.not_null:
|
|
95
|
+
parts.append('NOT NULL')
|
|
96
|
+
if column.default is not None:
|
|
97
|
+
parts.append(f'DEFAULT {column.default}')
|
|
98
|
+
if column.options:
|
|
99
|
+
options = ', '.join("'" + o.replace("'", "''") + "'" for o in column.options)
|
|
100
|
+
parts.append(f'CHECK ("{column.name}" IN ({options}))')
|
|
101
|
+
if column.references:
|
|
102
|
+
parts.append(f'REFERENCES "{column.references[0]}"("{column.references[1]}")')
|
|
103
|
+
return ' '.join(parts)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _differences(a: Column, b: Column) -> List[str]:
|
|
107
|
+
changed = []
|
|
108
|
+
for label, x, y in (('type', (a.type or '').upper(), (b.type or '').upper()),
|
|
109
|
+
('NOT NULL', a.not_null, b.not_null), ('default', a.default, b.default),
|
|
110
|
+
('primary key', a.primary_key, b.primary_key), ('CHECK', a.options, b.options),
|
|
111
|
+
('references', a.references, b.references)):
|
|
112
|
+
if x != y:
|
|
113
|
+
changed.append(label)
|
|
114
|
+
return changed
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def diff(current_db, desired_db) -> List[Step]:
|
|
118
|
+
"""The steps that turn the first database's schema into the second's."""
|
|
119
|
+
current, desired = _tables(current_db), _tables(desired_db)
|
|
120
|
+
steps: List[Step] = []
|
|
121
|
+
rebuilt = set()
|
|
122
|
+
|
|
123
|
+
for name in sorted(set(desired) - set(current)):
|
|
124
|
+
steps.append(Step('add-table', name, sql=[_create_sql(desired_db, name) + ';']))
|
|
125
|
+
|
|
126
|
+
for name in sorted(set(current) & set(desired)):
|
|
127
|
+
before, after = current[name].columns, desired[name].columns
|
|
128
|
+
added = [c for c in after if c not in before]
|
|
129
|
+
removed = [c for c in before if c not in after]
|
|
130
|
+
changed = {c: _differences(before[c], after[c]) for c in before
|
|
131
|
+
if c in after and _differences(before[c], after[c])}
|
|
132
|
+
addable = all(not after[c].primary_key and (not after[c].not_null or after[c].default is not None)
|
|
133
|
+
for c in added)
|
|
134
|
+
if not removed and not changed and addable:
|
|
135
|
+
for c in added:
|
|
136
|
+
steps.append(Step('add-column', f'{name}.{c}', after[c].type or '',
|
|
137
|
+
[f'ALTER TABLE "{name}" ADD COLUMN {_column_sql(after[c])};']))
|
|
138
|
+
continue
|
|
139
|
+
if not (added or removed or changed):
|
|
140
|
+
continue
|
|
141
|
+
common = ', '.join(f'"{c}"' for c in after if c in before)
|
|
142
|
+
create = re.sub(r'^\s*CREATE\s+TABLE\s+("?)' + re.escape(name) + r'\1',
|
|
143
|
+
f'CREATE TABLE "{name}__new"', _create_sql(desired_db, name), count=1, flags=re.I)
|
|
144
|
+
details = [f'+ {c}' for c in added] + [f'- {c}' for c in removed] + \
|
|
145
|
+
[f'{c}: {", ".join(d)}' for c, d in changed.items()]
|
|
146
|
+
warning = ''
|
|
147
|
+
if any('NOT NULL' in d and after[c].not_null for c, d in changed.items()) or \
|
|
148
|
+
any(after[c].not_null and after[c].default is None for c in added):
|
|
149
|
+
warning = 'fails if existing rows have no value for a NOT NULL column'
|
|
150
|
+
steps.append(Step('rebuild', name, '; '.join(details), [
|
|
151
|
+
create + ';',
|
|
152
|
+
f'INSERT INTO "{name}__new" ({common}) SELECT {common} FROM "{name}";',
|
|
153
|
+
f'DROP TABLE "{name}";',
|
|
154
|
+
f'ALTER TABLE "{name}__new" RENAME TO "{name}";',
|
|
155
|
+
], loses_data=bool(removed) or any('type' in d for d in changed.values()), warning=warning))
|
|
156
|
+
rebuilt.add(name)
|
|
157
|
+
|
|
158
|
+
for name in sorted(set(current) - set(desired)):
|
|
159
|
+
steps.append(Step('drop-table', name, sql=[f'DROP TABLE "{name}";'], loses_data=True))
|
|
160
|
+
|
|
161
|
+
# Indexes: a rebuilt table lost its own, so they are created again.
|
|
162
|
+
before_idx, after_idx = _indexes(current_db), _indexes(desired_db)
|
|
163
|
+
for name, (table, sql) in sorted(before_idx.items()):
|
|
164
|
+
if table in desired and table not in rebuilt and after_idx.get(name, (None, None))[1] != sql:
|
|
165
|
+
steps.append(Step('drop-index', name, sql=[f'DROP INDEX "{name}";']))
|
|
166
|
+
for name, (table, sql) in sorted(after_idx.items()):
|
|
167
|
+
if before_idx.get(name, (None, None))[1] != sql or table in rebuilt:
|
|
168
|
+
steps.append(Step('add-index', name, sql=[sql + ';']))
|
|
169
|
+
return steps
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _schema_only(conn) -> str:
|
|
173
|
+
"""The CREATE statements of a database, tables before indexes, without its rows."""
|
|
174
|
+
rows = conn.execute("SELECT type, sql FROM sqlite_master WHERE sql IS NOT NULL "
|
|
175
|
+
"AND name NOT LIKE 'sqlite_%' ORDER BY type = 'index', rowid").fetchall()
|
|
176
|
+
return '\n'.join(sql + ';' for _type, sql in rows)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _signature(conn) -> tuple:
|
|
180
|
+
tables = _tables(conn)
|
|
181
|
+
return (tuple(sorted((n, tuple(sorted((c.name, (c.type or '').upper(), c.not_null, c.default, c.primary_key,
|
|
182
|
+
tuple(c.options or ()), c.references)
|
|
183
|
+
for c in t.columns.values()))) for n, t in tables.items())),
|
|
184
|
+
tuple(sorted((n, s) for n, (_t, s) in _indexes(conn).items())))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class Plan:
|
|
189
|
+
steps: List[Step]
|
|
190
|
+
up_sql: str
|
|
191
|
+
down_sql: str
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def loses_data(self) -> bool:
|
|
195
|
+
return any(s.loses_data for s in self.steps)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def make_plan(schema_file: Path, migrations_dir: Path) -> Plan:
|
|
199
|
+
if not schema_file.exists():
|
|
200
|
+
raise SchemaPlanError(f'{schema_file} does not exist: write the tables as they should be, '
|
|
201
|
+
f'e.g. CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL);')
|
|
202
|
+
migrations = _migration_scripts(migrations_dir)
|
|
203
|
+
current = _database(migrations)
|
|
204
|
+
desired = _database([(schema_file.name, schema_file.read_text(encoding='utf-8'))])
|
|
205
|
+
steps = diff(current, desired)
|
|
206
|
+
up = '\n'.join(s for step in steps for s in step.sql)
|
|
207
|
+
down = '\n'.join(s for step in diff(desired, current) for s in step.sql)
|
|
208
|
+
if steps:
|
|
209
|
+
# The plan must do what it says: applied, it yields schema.sql's schema.
|
|
210
|
+
# Checked on the structure only — what existing rows may break is the
|
|
211
|
+
# step's warning, shown in the plan, not a reason to hide the plan.
|
|
212
|
+
check = _database([('migrations (schema only)', _schema_only(current)), ('plan', up)])
|
|
213
|
+
if _signature(check) != _signature(desired):
|
|
214
|
+
raise SchemaPlanError('the generated migration does not reproduce schema.sql — '
|
|
215
|
+
'write this change as a migration by hand (quantum migrate create)')
|
|
216
|
+
return Plan(steps, up, down)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def next_version(migrations_dir: Path) -> str:
|
|
220
|
+
numbers = [int(m.group(1)) for f in migrations_dir.glob('V*.sql')
|
|
221
|
+
for m in [re.match(r'V(\d+)_', f.name)] if m]
|
|
222
|
+
return f'V{(max(numbers) + 1 if numbers else 1):03d}'
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def write_plan(plan: Plan, migrations_dir: Path, name: str) -> Path:
|
|
226
|
+
migrations_dir.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
slug = re.sub(r'[^a-z0-9_]+', '_', name.lower()).strip('_') or 'schema'
|
|
228
|
+
version = next_version(migrations_dir)
|
|
229
|
+
path = migrations_dir / f'{version}_{slug}.sql'
|
|
230
|
+
header = '-- Generated by `quantum migrate plan` from schema.sql (DB-10)\n'
|
|
231
|
+
summary = '\n'.join(f'-- {s.line()}' for s in plan.steps)
|
|
232
|
+
path.write_text(f'{header}{summary}\n\n{plan.up_sql}\n', encoding='utf-8')
|
|
233
|
+
(migrations_dir / f'{version}_{slug}.down.sql').write_text(
|
|
234
|
+
f'{header}-- rollback\n\n{plan.down_sql}\n', encoding='utf-8')
|
|
235
|
+
return path
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def run_plan(project: Path, schema: Optional[str], write: Optional[str], yes: bool,
|
|
239
|
+
allow_data_loss: bool, ask=input, interactive: bool = True) -> int:
|
|
240
|
+
"""The `quantum migrate plan` command. Returns the exit code."""
|
|
241
|
+
from quantum.cli.commands.migrate import _paths_migrations
|
|
242
|
+
migrations_dir = project / _paths_migrations(project)
|
|
243
|
+
schema_file = project / (schema or 'schema.sql')
|
|
244
|
+
try:
|
|
245
|
+
plan = make_plan(schema_file, migrations_dir)
|
|
246
|
+
except SchemaPlanError as exc:
|
|
247
|
+
print(f'[ERROR] {exc}')
|
|
248
|
+
return 1
|
|
249
|
+
if not plan.steps:
|
|
250
|
+
print(f'Nothing to do: the migrations already produce {schema_file.name}.')
|
|
251
|
+
return 0
|
|
252
|
+
print(f'Plan: {schema_file.name} vs. the migrations in {migrations_dir.name}/\n')
|
|
253
|
+
for step in plan.steps:
|
|
254
|
+
print(' ' + step.line())
|
|
255
|
+
print('\nSQL:\n' + plan.up_sql)
|
|
256
|
+
if not write:
|
|
257
|
+
print('\nNothing written. `quantum migrate plan --write <name>` saves it as a migration.')
|
|
258
|
+
return 0
|
|
259
|
+
if plan.loses_data and not allow_data_loss:
|
|
260
|
+
print('\n[REFUSED] This plan loses data (marked !!). Check it, then run again with '
|
|
261
|
+
'--allow-data-loss.')
|
|
262
|
+
return 1
|
|
263
|
+
if not yes:
|
|
264
|
+
if not interactive:
|
|
265
|
+
print('\n[REFUSED] Confirm with --yes (not a terminal: nobody to ask).')
|
|
266
|
+
return 1
|
|
267
|
+
answer = ask(f'\nWrite migration {next_version(migrations_dir)}_{write}? [y/N] ').strip().lower()
|
|
268
|
+
if answer not in ('y', 'yes'):
|
|
269
|
+
print('Nothing written.')
|
|
270
|
+
return 1
|
|
271
|
+
path = write_plan(plan, migrations_dir, write)
|
|
272
|
+
print(f'Written: {path} (and {path.stem}.down.sql). Apply with `quantum migrate up`.')
|
|
273
|
+
return 0
|
|
@@ -1896,6 +1896,24 @@ class UIPagerNode(QuantumNode, UILayoutMixin):
|
|
|
1896
1896
|
return [] if self.for_query else ["ui:pager needs for="]
|
|
1897
1897
|
|
|
1898
1898
|
|
|
1899
|
+
class UIHistoryNode(QuantumNode, UILayoutMixin):
|
|
1900
|
+
"""<ui:history table="t" key="{row.id}" datasource="db"> — a row's change history (DB-11)."""
|
|
1901
|
+
|
|
1902
|
+
def __init__(self):
|
|
1903
|
+
self.table: Optional[str] = None
|
|
1904
|
+
self.key: Optional[str] = None
|
|
1905
|
+
self.datasource: Optional[str] = None
|
|
1906
|
+
self.limit: int = 50
|
|
1907
|
+
self.children: List[QuantumNode] = []
|
|
1908
|
+
self._init_layout_attrs()
|
|
1909
|
+
|
|
1910
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1911
|
+
return {"type": "ui_history", "table": self.table, "key": self.key}
|
|
1912
|
+
|
|
1913
|
+
def validate(self) -> List[str]:
|
|
1914
|
+
return []
|
|
1915
|
+
|
|
1916
|
+
|
|
1899
1917
|
def is_ui_node(node) -> bool:
|
|
1900
1918
|
"""A ui:* element (UI-1): markup, drawn by a renderer, never executed."""
|
|
1901
1919
|
return type(node).__module__ == __name__
|
|
@@ -1908,6 +1926,6 @@ CORE_TAGS = frozenset({
|
|
|
1908
1926
|
'window', 'hbox', 'vbox', 'grid', 'panel', 'section', 'scrollbox', 'spacer', 'rule',
|
|
1909
1927
|
'header', 'footer', 'card', 'card-header', 'card-body', 'card-footer', 'tabpanel', 'tab',
|
|
1910
1928
|
'text', 'badge', 'alert', 'link', 'image', 'progress',
|
|
1911
|
-
'table', 'column', 'list', 'item', 'pager',
|
|
1929
|
+
'table', 'column', 'list', 'item', 'pager', 'history',
|
|
1912
1930
|
'form', 'formitem', 'input', 'checkbox', 'switch', 'radio', 'select', 'option', 'button',
|
|
1913
1931
|
})
|
{quantum_framework-0.18.0 → quantum_framework-0.19.0}/quantum/core/features/ui_engine/src/parser.py
RENAMED
|
@@ -28,7 +28,7 @@ from .ast_nodes import (
|
|
|
28
28
|
UIToastNode, UIToastContainerNode,
|
|
29
29
|
UICarouselNode, UISlideNode,
|
|
30
30
|
UIStepperNode, UIStepNode,
|
|
31
|
-
UICalendarNode, UIDatePickerNode, UIPagerNode,
|
|
31
|
+
UICalendarNode, UIDatePickerNode, UIPagerNode, UIHistoryNode,
|
|
32
32
|
)
|
|
33
33
|
from quantum.core.features.theming.src import UIThemeNode, UIColorNode
|
|
34
34
|
from quantum.core.ast_nodes import TextNode
|
|
@@ -82,6 +82,7 @@ class UIParser:
|
|
|
82
82
|
'table': '_parse_ui_table',
|
|
83
83
|
'column': '_parse_ui_column',
|
|
84
84
|
'pager': '_parse_ui_pager',
|
|
85
|
+
'history': '_parse_ui_history',
|
|
85
86
|
'list': '_parse_ui_list',
|
|
86
87
|
'item': '_parse_ui_item',
|
|
87
88
|
'image': '_parse_ui_image',
|
|
@@ -550,6 +551,21 @@ class UIParser:
|
|
|
550
551
|
self._apply_layout_attrs(node, element)
|
|
551
552
|
return node
|
|
552
553
|
|
|
554
|
+
def _parse_ui_history(self, element: ET.Element) -> UIHistoryNode:
|
|
555
|
+
node = UIHistoryNode()
|
|
556
|
+
node.table = element.get('table')
|
|
557
|
+
node.key = element.get('key')
|
|
558
|
+
node.datasource = element.get('datasource')
|
|
559
|
+
missing = [a for a in ('table', 'key', 'datasource') if not element.get(a)]
|
|
560
|
+
if missing:
|
|
561
|
+
raise UIParseError(f'<ui:history> needs {", ".join(a + "=" for a in missing)}')
|
|
562
|
+
limit = element.get('limit')
|
|
563
|
+
if limit is not None and not limit.isdigit():
|
|
564
|
+
raise UIParseError(f'<ui:history limit="{limit}">: a number of entries')
|
|
565
|
+
node.limit = int(limit) if limit is not None else 50
|
|
566
|
+
self._apply_layout_attrs(node, element)
|
|
567
|
+
return node
|
|
568
|
+
|
|
553
569
|
def _parse_ui_list(self, element: ET.Element) -> UIListNode:
|
|
554
570
|
node = UIListNode()
|
|
555
571
|
node.source = element.get('source')
|
|
@@ -122,9 +122,12 @@ class ActionHandler:
|
|
|
122
122
|
context.set_variable(key, value)
|
|
123
123
|
|
|
124
124
|
# 4. Execute action body
|
|
125
|
+
from quantum.runtime import history
|
|
126
|
+
history_token = history.enter(getattr(action, 'name', None), context.session_vars) # M22
|
|
125
127
|
try:
|
|
126
128
|
redirect_info = self._execute_action_body(action, context)
|
|
127
129
|
finally:
|
|
130
|
+
history.leave(history_token)
|
|
128
131
|
# M12: the /_dev panel shows the action's variables (no-op off debug).
|
|
129
132
|
from quantum.runtime.dev_panel import note_scopes
|
|
130
133
|
note_scopes(context, 'action')
|
|
@@ -86,6 +86,7 @@ class DatabaseService:
|
|
|
86
86
|
'port': local_cfg.get('port', 5432),
|
|
87
87
|
'username': local_cfg.get('username', ''),
|
|
88
88
|
'password': local_cfg.get('password', ''),
|
|
89
|
+
'history': bool(local_cfg.get('history')), # M22 (DB-11)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
# DB-7: a datasource is declared in quantum.config.yaml, or it does not
|
|
@@ -277,6 +278,13 @@ class DatabaseService:
|
|
|
277
278
|
# Convert :param syntax to database-specific placeholder
|
|
278
279
|
prepared_sql, prepared_params = self._prepare_query(sql, params, db_type)
|
|
279
280
|
|
|
281
|
+
# M22 (DB-11): with history on, an action's write records the rows it touches.
|
|
282
|
+
capture = None
|
|
283
|
+
if config.get('history') and db_type == 'sqlite':
|
|
284
|
+
from quantum.runtime import history
|
|
285
|
+
capture = history.begin(conn, sql, params,
|
|
286
|
+
lambda q, p: self._prepare_query(q, p, db_type))
|
|
287
|
+
|
|
280
288
|
# Execute query
|
|
281
289
|
cursor.execute(prepared_sql, prepared_params)
|
|
282
290
|
|
|
@@ -309,6 +317,10 @@ class DatabaseService:
|
|
|
309
317
|
# Non-SELECT query (INSERT, UPDATE, DELETE). Commit now ONLY
|
|
310
318
|
# when standalone; inside a transaction the commit is deferred
|
|
311
319
|
# to commit_transaction so a later failure can roll it back.
|
|
320
|
+
if capture is not None:
|
|
321
|
+
# Before the commit: the write and its history go together.
|
|
322
|
+
from quantum.runtime import history
|
|
323
|
+
history.finish(conn, capture, getattr(cursor, 'lastrowid', None))
|
|
312
324
|
if not in_transaction:
|
|
313
325
|
conn.commit()
|
|
314
326
|
data = []
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Change history, per datasource (M22, DB-11).
|
|
2
|
+
|
|
3
|
+
With `history: true` on a datasource, every write an action makes to it —
|
|
4
|
+
INSERT, UPDATE, DELETE, including the cells of an editable table (UI-13) — is
|
|
5
|
+
recorded in a `quantum_history` table of that same database: when, who (the
|
|
6
|
+
session's user), which action, which row, and the row before and after, as
|
|
7
|
+
JSON. The record is written on the same connection, before the commit, so a
|
|
8
|
+
write and its history commit or roll back together.
|
|
9
|
+
|
|
10
|
+
Only writes made by actions are recorded: page statements do not write in a
|
|
11
|
+
well-formed app, and migrations are not changes to records. A write whose
|
|
12
|
+
shape is not a single-table INSERT/UPDATE/DELETE is still recorded, with its
|
|
13
|
+
SQL and no before/after — never skipped silently.
|
|
14
|
+
|
|
15
|
+
`<ui:history table="posts" key="{post.id}" datasource="db">` shows a row's
|
|
16
|
+
history on any page.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import contextvars
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
TABLE = 'quantum_history'
|
|
26
|
+
|
|
27
|
+
CREATE_SQL = (f'CREATE TABLE IF NOT EXISTS {TABLE} ('
|
|
28
|
+
'id INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, user TEXT, action TEXT, '
|
|
29
|
+
'table_name TEXT NOT NULL, row_key TEXT, op TEXT NOT NULL, before TEXT, after TEXT, '
|
|
30
|
+
'sql TEXT)')
|
|
31
|
+
|
|
32
|
+
_ACTION = contextvars.ContextVar('quantum_history_action', default=None)
|
|
33
|
+
|
|
34
|
+
_WRITE = re.compile(r'^\s*(INSERT\s+(?:OR\s+\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+"?(\w+)"?', re.I)
|
|
35
|
+
_WHERE = re.compile(r'\bWHERE\b(.*)$', re.I | re.S)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def enter(action: str, session_vars: Optional[dict]):
|
|
39
|
+
"""Mark the writes that follow as made by this action, for this user."""
|
|
40
|
+
session = session_vars or {}
|
|
41
|
+
user = session.get('userName') or session.get('userId')
|
|
42
|
+
return _ACTION.set({'action': action, 'user': None if user is None else str(user)})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def leave(token) -> None:
|
|
46
|
+
_ACTION.reset(token)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def current() -> Optional[dict]:
|
|
50
|
+
return _ACTION.get()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_write(sql: str):
|
|
54
|
+
"""(op, table, where) of a single-table write, or None when it is not one."""
|
|
55
|
+
m = _WRITE.match(sql)
|
|
56
|
+
if not m:
|
|
57
|
+
return None
|
|
58
|
+
op = {'INSERT': 'insert', 'UPDATE': 'update', 'DELETE': 'delete'}[m.group(1).split()[0].upper()]
|
|
59
|
+
where = _WHERE.search(sql)
|
|
60
|
+
return op, m.group(2), (where.group(1).strip().rstrip(';') if where else None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Capture:
|
|
64
|
+
"""One write being recorded: the rows before, then the rows after."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, op: str, table: str, sql: str, key: Optional[str], before: List[dict], shaped: bool):
|
|
67
|
+
self.op, self.table, self.sql, self.key = op, table, sql, key
|
|
68
|
+
self.before, self.shaped = before, shaped
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _rows(cursor) -> List[dict]:
|
|
72
|
+
columns = [d[0] for d in cursor.description or []]
|
|
73
|
+
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _primary_key(conn, table: str) -> Optional[str]:
|
|
77
|
+
keys = [row[1] for row in conn.execute(f'PRAGMA table_info("{table}")') if row[5]]
|
|
78
|
+
return keys[0] if len(keys) == 1 else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def begin(conn, sql: str, params, prepare) -> Optional[Capture]:
|
|
82
|
+
"""Before a write: which rows it will touch, as they are. None when nothing is recorded."""
|
|
83
|
+
state = current()
|
|
84
|
+
if state is None:
|
|
85
|
+
return None
|
|
86
|
+
shape = parse_write(sql)
|
|
87
|
+
if shape is None:
|
|
88
|
+
table = re.search(r'\b(?:INTO|UPDATE|FROM)\s+"?(\w+)"?', sql, re.I)
|
|
89
|
+
if not table or table.group(1) == TABLE:
|
|
90
|
+
return None
|
|
91
|
+
return Capture('write', table.group(1), sql, None, [], shaped=False)
|
|
92
|
+
op, table, where = shape
|
|
93
|
+
if table == TABLE:
|
|
94
|
+
return None
|
|
95
|
+
key = _primary_key(conn, table)
|
|
96
|
+
before: List[dict] = []
|
|
97
|
+
if op in ('update', 'delete'):
|
|
98
|
+
query, args = prepare(f'SELECT * FROM "{table}"' + (f' WHERE {where}' if where else ''), params)
|
|
99
|
+
cursor = conn.cursor()
|
|
100
|
+
cursor.execute(query, args)
|
|
101
|
+
before = _rows(cursor)
|
|
102
|
+
cursor.close()
|
|
103
|
+
return Capture(op, table, sql, key, before, shaped=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def finish(conn, capture: Optional[Capture], last_row_id: Optional[int]) -> None:
|
|
107
|
+
"""After the write, before the commit: record each row it changed."""
|
|
108
|
+
if capture is None:
|
|
109
|
+
return
|
|
110
|
+
state = current() or {}
|
|
111
|
+
conn.execute(CREATE_SQL)
|
|
112
|
+
records = []
|
|
113
|
+
if not capture.shaped:
|
|
114
|
+
records.append((None, None, None))
|
|
115
|
+
elif capture.op == 'insert':
|
|
116
|
+
row = None
|
|
117
|
+
if last_row_id:
|
|
118
|
+
cursor = conn.execute(f'SELECT * FROM "{capture.table}" WHERE rowid = ?', (last_row_id,))
|
|
119
|
+
found = _rows(cursor)
|
|
120
|
+
row = found[0] if found else None
|
|
121
|
+
key = row.get(capture.key) if (row and capture.key) else last_row_id
|
|
122
|
+
records.append((key, None, row))
|
|
123
|
+
else:
|
|
124
|
+
for before in capture.before:
|
|
125
|
+
key = before.get(capture.key) if capture.key else None
|
|
126
|
+
after = None
|
|
127
|
+
if capture.op == 'update' and capture.key:
|
|
128
|
+
cursor = conn.execute(f'SELECT * FROM "{capture.table}" WHERE "{capture.key}" = ?', (key,))
|
|
129
|
+
found = _rows(cursor)
|
|
130
|
+
after = found[0] if found else None
|
|
131
|
+
records.append((key, before, after))
|
|
132
|
+
now = datetime.now().isoformat(timespec='seconds')
|
|
133
|
+
for key, before, after in records:
|
|
134
|
+
conn.execute(
|
|
135
|
+
f'INSERT INTO {TABLE} (at, user, action, table_name, row_key, op, before, after, sql) '
|
|
136
|
+
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
137
|
+
(now, state.get('user'), state.get('action'), capture.table,
|
|
138
|
+
None if key is None else str(key), capture.op,
|
|
139
|
+
None if before is None else json.dumps(before, default=str, ensure_ascii=False),
|
|
140
|
+
None if after is None else json.dumps(after, default=str, ensure_ascii=False),
|
|
141
|
+
None if capture.shaped else capture.sql))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def changes(before: Optional[dict], after: Optional[dict]) -> List[tuple]:
|
|
145
|
+
"""(column, old, new) of what an update changed."""
|
|
146
|
+
if not before or not after:
|
|
147
|
+
return []
|
|
148
|
+
return [(c, before.get(c), after.get(c)) for c in after if before.get(c) != after.get(c)]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def read(conn, table: str, key: Any, limit: int = 50) -> List[dict]:
|
|
152
|
+
"""A row's history, newest first; empty when nothing was ever recorded."""
|
|
153
|
+
exists = conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (TABLE,)).fetchone()
|
|
154
|
+
if not exists:
|
|
155
|
+
return []
|
|
156
|
+
cursor = conn.execute(
|
|
157
|
+
f'SELECT at, user, action, op, before, after, sql FROM {TABLE} '
|
|
158
|
+
'WHERE table_name = ? AND row_key = ? ORDER BY id DESC LIMIT ?', (table, str(key), limit))
|
|
159
|
+
result = []
|
|
160
|
+
for at, user, action, op, before, after, sql in cursor.fetchall():
|
|
161
|
+
result.append({'at': at, 'user': user, 'action': action, 'op': op,
|
|
162
|
+
'before': json.loads(before) if before else None,
|
|
163
|
+
'after': json.loads(after) if after else None, 'sql': sql})
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def entries_for(renderer, node) -> List[Dict[str, str]]:
|
|
168
|
+
"""<ui:history>: the row's entries as text — when, who, action, change (web and console)."""
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
from quantum.runtime.db_schema import SchemaError, connect_readonly
|
|
171
|
+
config = getattr(renderer, 'config', None) or {}
|
|
172
|
+
sources = config.get('datasources') or {}
|
|
173
|
+
if node.datasource not in sources:
|
|
174
|
+
raise ValueError(f'<ui:history datasource="{node.datasource}">: not declared in quantum.config.yaml')
|
|
175
|
+
if not sources[node.datasource].get('history'):
|
|
176
|
+
raise ValueError(f'<ui:history>: datasource "{node.datasource}" does not record history — '
|
|
177
|
+
f'add `history: true` to it (DB-11)')
|
|
178
|
+
key = renderer._apply_databinding(node.key) if '{' in (node.key or '') else node.key
|
|
179
|
+
try:
|
|
180
|
+
conn = connect_readonly(sources[node.datasource], Path.cwd())
|
|
181
|
+
except SchemaError as exc:
|
|
182
|
+
raise ValueError(f'<ui:history>: {exc}') from exc
|
|
183
|
+
try:
|
|
184
|
+
entries = read(conn, node.table, key, node.limit)
|
|
185
|
+
finally:
|
|
186
|
+
conn.close()
|
|
187
|
+
lines = []
|
|
188
|
+
for entry in entries:
|
|
189
|
+
if entry['op'] == 'insert':
|
|
190
|
+
change = 'created'
|
|
191
|
+
elif entry['op'] == 'delete':
|
|
192
|
+
change = 'deleted'
|
|
193
|
+
elif entry['op'] == 'update':
|
|
194
|
+
change = '; '.join(f'{c}: {old} → {new}' for c, old, new in changes(entry['before'], entry['after'])) or 'no change'
|
|
195
|
+
else:
|
|
196
|
+
change = entry.get('sql') or entry['op']
|
|
197
|
+
lines.append({'at': entry['at'].replace('T', ' '), 'user': entry['user'] or '—',
|
|
198
|
+
'action': entry['action'] or '—', 'change': change})
|
|
199
|
+
return lines
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
HEADERS = (('at', 'When'), ('user', 'Who'), ('action', 'Action'), ('change', 'Change'))
|
|
203
|
+
|