blq-cli 1.0.1__tar.gz → 1.0.2__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.
- blq_cli-1.0.2/CHANGELOG.md +6 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/PKG-INFO +1 -1
- {blq_cli-1.0.1 → blq_cli-1.0.2}/pyproject.toml +1 -1
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/bird.py +111 -39
- blq_cli-1.0.2/tests/test_schema_repair.py +95 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.gitignore +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.mcp.json +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/.readthedocs.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/AGENT.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/AGENTS.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/CLAUDE.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/CONTRIBUTING.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/README.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/SKILL.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/api-reference.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/capture.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/ci.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/completions.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/errors.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/exec.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/filter.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/index.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/init.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/query.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/registry.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/report.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/run.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/sql.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/status.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/sync.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/watch.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-commands.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-sync.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/duck-hunt-v3-migration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/getting-started.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/index.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/integration.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/mcp.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/explore-bird-sqlite-backend.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plugin-guide.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/python-api.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/query-guide.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/requirements.txt +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/sandbox.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/schema-stability.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/schema.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/mkdocs.yml +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/__main__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/bird_schema.sql +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/cli.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/README.md +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/core.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/events.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/execution.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/management.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/migrate.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/registry.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/config_format.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/discovery.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/local_executor.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/git.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/github.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/locks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/output.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/query.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/runtime.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/schema.sql +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/serve.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/execution.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/inspect.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/query.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/refs.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/storage.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/user_config.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/profile.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/__init__.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/conftest.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_auto_init.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_autoprune.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_bird.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_bwrap_args.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ci.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_command_args.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_command_lock_field.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_config_consumption.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_core.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_execution_locks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_discovery.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_integration.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_types.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_git.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_hooks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_inspect.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_locks.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_mcp_ci_tools.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_mcp_server.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_query_api.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_query_filter.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_report.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_runtime_config.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_events.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_register.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_execution.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_inspect.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_query.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_refs.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_source_annotator.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_storage.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_strace_parser.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_user_config.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_watch.py +0 -0
- {blq_cli-1.0.1 → blq_cli-1.0.2}/uv.lock +0 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v1.0.2
|
|
4
|
+
|
|
5
|
+
### Bug fixes
|
|
6
|
+
- **Self-heal DBs stuck without `extension_data`.** The 2.3->2.4 migration renamed `sandbox` -> `extension_data` via `RENAME COLUMN`, which DuckDB blocks when non-view dependencies (FKs/constraints) exist on `attempts`/`invocations` — even after dropping views. The rename failed silently but the schema version advanced anyway, leaving DBs permanently stuck without `extension_data` and crashing every `write_attempt` ("Table attempts does not have a column extension_data"). `_ensure_schema` now self-heals version-independently: it gates on a missing column, adds `extension_data` via `ADD COLUMN` (never blocked), copies any existing `sandbox` data across (wrapped as `{"sandbox": ...}`, no data loss), and re-applies the idempotent schema to recreate views. Healthy/fresh DBs stay a fast no-op. Repairs on next connect (the CLI heals on the next `blq run`; a running MCP server heals after restart).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blq-cli
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Build Log Query - capture and query build/test logs with DuckDB
|
|
5
5
|
Project-URL: Homepage, https://blq-cli.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/teaguesterling/blq-cli
|
|
@@ -420,13 +420,26 @@ class BirdStore:
|
|
|
420
420
|
).fetchone()
|
|
421
421
|
if result:
|
|
422
422
|
current_version = result[0]
|
|
423
|
-
# Apply any pending migrations
|
|
424
|
-
cls._apply_migrations(conn, current_version)
|
|
425
|
-
return
|
|
426
423
|
except duckdb.Error:
|
|
427
424
|
pass # Table doesn't exist, need to create
|
|
428
425
|
|
|
429
|
-
|
|
426
|
+
if current_version is not None and not force:
|
|
427
|
+
# Existing DB. Only touch it when there's pending migration work or a
|
|
428
|
+
# column a past (possibly silently-failed) migration never added;
|
|
429
|
+
# otherwise this is a fast no-op.
|
|
430
|
+
if not cls._needs_repair(conn, current_version):
|
|
431
|
+
return
|
|
432
|
+
# DuckDB blocks column ALTER/RENAME while views depend on the table
|
|
433
|
+
# (this is what left old DBs stuck without extension_data). Drop all
|
|
434
|
+
# views first so migrations + the reconcile can alter the base tables;
|
|
435
|
+
# re-applying the schema below recreates every view.
|
|
436
|
+
cls._drop_all_views(conn)
|
|
437
|
+
cls._apply_migrations(conn, current_version)
|
|
438
|
+
cls._reconcile_schema(conn)
|
|
439
|
+
|
|
440
|
+
# Load schema from SQL file (idempotent: CREATE TABLE IF NOT EXISTS,
|
|
441
|
+
# INSERT OR IGNORE, CREATE OR REPLACE VIEW — safe to re-apply, and it
|
|
442
|
+
# recreates any views dropped above).
|
|
430
443
|
schema_path = Path(__file__).parent / "bird_schema.sql"
|
|
431
444
|
if schema_path.exists():
|
|
432
445
|
schema_sql = schema_path.read_text()
|
|
@@ -504,6 +517,92 @@ class BirdStore:
|
|
|
504
517
|
|
|
505
518
|
return statements
|
|
506
519
|
|
|
520
|
+
@classmethod
|
|
521
|
+
def _needs_repair(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> bool:
|
|
522
|
+
"""True if the DB needs migration/repair — pending version bump OR a
|
|
523
|
+
required column a past (possibly silently-failed) migration never added.
|
|
524
|
+
|
|
525
|
+
Keeps the common path a fast no-op: a fully current DB returns False, so
|
|
526
|
+
_ensure_schema doesn't drop/recreate views on every connect.
|
|
527
|
+
"""
|
|
528
|
+
try:
|
|
529
|
+
parts = [int(p) for p in current_version.split(".")]
|
|
530
|
+
major, minor = parts[0], parts[1] if len(parts) > 1 else 0
|
|
531
|
+
if (major, minor) < (3, 0):
|
|
532
|
+
return True
|
|
533
|
+
except (ValueError, IndexError):
|
|
534
|
+
return True # unparseable version — reconcile to be safe
|
|
535
|
+
# Self-heal for migrations that failed silently on dependent views and
|
|
536
|
+
# then let the version advance past themselves (leaving no column).
|
|
537
|
+
for table in ("attempts", "invocations"):
|
|
538
|
+
try:
|
|
539
|
+
has = conn.execute(
|
|
540
|
+
"SELECT 1 FROM information_schema.columns "
|
|
541
|
+
f"WHERE table_name = '{table}' AND column_name = 'extension_data'"
|
|
542
|
+
).fetchone()
|
|
543
|
+
if not has:
|
|
544
|
+
return True
|
|
545
|
+
except duckdb.Error:
|
|
546
|
+
return True
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
@staticmethod
|
|
550
|
+
def _drop_all_views(conn: duckdb.DuckDBPyConnection) -> None:
|
|
551
|
+
"""Drop all views so base-table column ALTER/RENAME isn't blocked by
|
|
552
|
+
dependents. The schema is re-applied afterward (CREATE OR REPLACE VIEW)
|
|
553
|
+
to recreate them."""
|
|
554
|
+
try:
|
|
555
|
+
views = conn.execute(
|
|
556
|
+
"SELECT table_name FROM information_schema.tables "
|
|
557
|
+
"WHERE table_type = 'VIEW'"
|
|
558
|
+
).fetchall()
|
|
559
|
+
except duckdb.Error:
|
|
560
|
+
return
|
|
561
|
+
for (name,) in views:
|
|
562
|
+
try:
|
|
563
|
+
conn.execute(f'DROP VIEW IF EXISTS "{name}" CASCADE')
|
|
564
|
+
except duckdb.Error:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
@classmethod
|
|
568
|
+
def _reconcile_schema(cls, conn: duckdb.DuckDBPyConnection) -> None:
|
|
569
|
+
"""Version-independent self-heal: ensure `extension_data` exists on
|
|
570
|
+
attempts and invocations even if a past migration failed silently (blocked
|
|
571
|
+
by dependent views) and the schema version advanced past it. Views must
|
|
572
|
+
already be dropped by the caller so the ALTER/RENAME can proceed.
|
|
573
|
+
|
|
574
|
+
Uses ADD COLUMN + data copy, NOT rename: RENAME COLUMN on these tables is
|
|
575
|
+
blocked by non-view dependencies (FKs/constraints) that persist even after
|
|
576
|
+
dropping views, whereas ADD COLUMN is never blocked. Existing `sandbox`
|
|
577
|
+
data (from the 2.3 migration) is copied into the new column wrapped as
|
|
578
|
+
{"sandbox": ...}; the now-redundant `sandbox` column is left in place
|
|
579
|
+
(harmless, and DROP COLUMN hits the same dependency wall).
|
|
580
|
+
"""
|
|
581
|
+
for table in ("attempts", "invocations"):
|
|
582
|
+
try:
|
|
583
|
+
cols = {
|
|
584
|
+
r[0]
|
|
585
|
+
for r in conn.execute(
|
|
586
|
+
"SELECT column_name FROM information_schema.columns "
|
|
587
|
+
f"WHERE table_name = '{table}'"
|
|
588
|
+
).fetchall()
|
|
589
|
+
}
|
|
590
|
+
if "extension_data" in cols:
|
|
591
|
+
continue
|
|
592
|
+
conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
|
|
593
|
+
if "sandbox" in cols:
|
|
594
|
+
conn.execute(
|
|
595
|
+
f"UPDATE {table} SET extension_data = "
|
|
596
|
+
f"json_object('sandbox', sandbox) WHERE sandbox IS NOT NULL"
|
|
597
|
+
)
|
|
598
|
+
logger.info(
|
|
599
|
+
f"Repaired {table}: added extension_data (copied sandbox data)"
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
logger.info(f"Repaired {table}: added extension_data")
|
|
603
|
+
except duckdb.Error as e:
|
|
604
|
+
logger.warning(f"Schema reconcile warning ({table}): {e}")
|
|
605
|
+
|
|
507
606
|
@classmethod
|
|
508
607
|
def _apply_migrations(cls, conn: duckdb.DuckDBPyConnection, current_version: str) -> None:
|
|
509
608
|
"""Apply schema migrations from current version to latest.
|
|
@@ -557,42 +656,15 @@ class BirdStore:
|
|
|
557
656
|
# Update schema version
|
|
558
657
|
conn.execute("UPDATE blq_metadata SET value = '2.3.0' WHERE key = 'schema_version'")
|
|
559
658
|
|
|
560
|
-
# Migration: 2.3.0 -> 2.4.0 (
|
|
659
|
+
# Migration: 2.3.0 -> 2.4.0 (sandbox -> extension_data).
|
|
660
|
+
# RENAME COLUMN is blocked by non-view dependencies on these tables, so
|
|
661
|
+
# this is done as ADD + copy via _reconcile_schema (which is idempotent
|
|
662
|
+
# and always succeeds), rather than the RENAME that silently failed and
|
|
663
|
+
# left old DBs stuck without extension_data.
|
|
561
664
|
if (major, minor) < (2, 4):
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
# Check if old column exists
|
|
566
|
-
result = conn.execute(
|
|
567
|
-
"SELECT column_name FROM information_schema.columns "
|
|
568
|
-
f"WHERE table_name = '{table}' AND column_name = 'sandbox'"
|
|
569
|
-
).fetchone()
|
|
570
|
-
if result:
|
|
571
|
-
conn.execute(f"ALTER TABLE {table} RENAME COLUMN sandbox TO extension_data")
|
|
572
|
-
# Wrap existing sandbox data in {"sandbox": ...} envelope
|
|
573
|
-
conn.execute(
|
|
574
|
-
f"UPDATE {table} SET extension_data = "
|
|
575
|
-
f"json_object('sandbox', extension_data) "
|
|
576
|
-
f"WHERE extension_data IS NOT NULL"
|
|
577
|
-
)
|
|
578
|
-
logger.info(f"Migration: Renamed sandbox to extension_data in {table}")
|
|
579
|
-
migrations_applied = True
|
|
580
|
-
else:
|
|
581
|
-
# Check if new column exists, add if not
|
|
582
|
-
result = conn.execute(
|
|
583
|
-
"SELECT column_name FROM information_schema.columns "
|
|
584
|
-
f"WHERE table_name = '{table}' AND column_name = 'extension_data'"
|
|
585
|
-
).fetchone()
|
|
586
|
-
if not result:
|
|
587
|
-
conn.execute(f"ALTER TABLE {table} ADD COLUMN extension_data JSON")
|
|
588
|
-
logger.info(f"Migration: Added extension_data column to {table}")
|
|
589
|
-
migrations_applied = True
|
|
590
|
-
except duckdb.Error as e:
|
|
591
|
-
logger.warning(f"Migration warning: {e}")
|
|
592
|
-
migration_ok = False
|
|
593
|
-
|
|
594
|
-
if migration_ok:
|
|
595
|
-
conn.execute("UPDATE blq_metadata SET value = '2.4.0' WHERE key = 'schema_version'")
|
|
665
|
+
cls._reconcile_schema(conn)
|
|
666
|
+
migrations_applied = True
|
|
667
|
+
conn.execute("UPDATE blq_metadata SET value = '2.4.0' WHERE key = 'schema_version'")
|
|
596
668
|
|
|
597
669
|
# Migration: 2.4.0 -> 3.0.0 (directory rename .lq -> .bird, no table changes)
|
|
598
670
|
if (major, minor) < (3, 0):
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Regression tests for schema self-heal.
|
|
2
|
+
|
|
3
|
+
An old migration renamed `sandbox` -> `extension_data`, but RENAME COLUMN is
|
|
4
|
+
blocked by dependencies on `attempts`/`invocations`, so it failed silently and
|
|
5
|
+
the schema version advanced past it — leaving DBs stuck without `extension_data`
|
|
6
|
+
and crashing `write_attempt`. `_ensure_schema` now self-heals (ADD + copy),
|
|
7
|
+
version-independently, without losing the old `sandbox` data.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import duckdb
|
|
13
|
+
|
|
14
|
+
import blq.bird as _bird_mod
|
|
15
|
+
from blq.bird import BirdStore
|
|
16
|
+
|
|
17
|
+
SCHEMA = (Path(_bird_mod.__file__).parent / "bird_schema.sql").read_text()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _apply(conn, sql_text):
|
|
21
|
+
for stmt in BirdStore._split_sql_statements(sql_text):
|
|
22
|
+
try:
|
|
23
|
+
conn.execute(stmt)
|
|
24
|
+
except duckdb.Error as e:
|
|
25
|
+
if "already exists" not in str(e).lower():
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _cols(conn, table):
|
|
30
|
+
return {
|
|
31
|
+
r[0]
|
|
32
|
+
for r in conn.execute(
|
|
33
|
+
f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}'"
|
|
34
|
+
).fetchall()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_stuck_db(path):
|
|
39
|
+
"""A DB from the era when the column was `sandbox` (tables + views all on
|
|
40
|
+
`sandbox`), version 3.0.0 — exactly what a silently-failed 2.4 RENAME left."""
|
|
41
|
+
c = duckdb.connect(str(path))
|
|
42
|
+
_apply(c, SCHEMA.replace("extension_data", "sandbox"))
|
|
43
|
+
c.execute(
|
|
44
|
+
"INSERT INTO attempts (session_id, cwd, cmd, client_id, sandbox) "
|
|
45
|
+
"VALUES ('s1', '/tmp', 'build', 'blq-test', '{\"network\":\"none\"}'::JSON)"
|
|
46
|
+
)
|
|
47
|
+
c.execute("UPDATE blq_metadata SET value='3.0.0' WHERE key='schema_version'")
|
|
48
|
+
c.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_stuck_db_self_heals_and_preserves_data(tmp_path):
|
|
52
|
+
bird = tmp_path / ".bird"
|
|
53
|
+
bird.mkdir()
|
|
54
|
+
db = bird / "blq.duckdb"
|
|
55
|
+
_build_stuck_db(db)
|
|
56
|
+
|
|
57
|
+
c = duckdb.connect(str(db))
|
|
58
|
+
assert "sandbox" in _cols(c, "attempts")
|
|
59
|
+
assert "extension_data" not in _cols(c, "attempts")
|
|
60
|
+
assert BirdStore._needs_repair(c, "3.0.0") is True
|
|
61
|
+
|
|
62
|
+
BirdStore._ensure_schema(c, bird)
|
|
63
|
+
|
|
64
|
+
# extension_data added on both tables, sandbox data copied + wrapped
|
|
65
|
+
assert "extension_data" in _cols(c, "attempts")
|
|
66
|
+
assert "extension_data" in _cols(c, "invocations")
|
|
67
|
+
val = c.execute("SELECT extension_data FROM attempts WHERE session_id='s1'").fetchone()[0]
|
|
68
|
+
assert "network" in str(val) and "sandbox" in str(val)
|
|
69
|
+
|
|
70
|
+
# views recreated, and the write that used to crash now succeeds
|
|
71
|
+
nviews = c.execute(
|
|
72
|
+
"SELECT count(*) FROM information_schema.tables WHERE table_type='VIEW'"
|
|
73
|
+
).fetchone()[0]
|
|
74
|
+
assert nviews >= 4
|
|
75
|
+
c.execute(
|
|
76
|
+
"INSERT INTO attempts (session_id, cwd, cmd, client_id, extension_data) "
|
|
77
|
+
"VALUES ('s2', '/tmp', 'x', 'blq-test', '{\"a\":1}'::JSON)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# converges: a repaired DB is a fast no-op on the next open
|
|
81
|
+
assert BirdStore._needs_repair(c, "3.0.0") is False
|
|
82
|
+
c.close()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_fresh_db_is_healthy_and_no_op(tmp_path):
|
|
86
|
+
bird = tmp_path / ".bird"
|
|
87
|
+
bird.mkdir()
|
|
88
|
+
c = duckdb.connect(str(bird / "blq.duckdb"))
|
|
89
|
+
BirdStore._ensure_schema(c, bird) # fresh init
|
|
90
|
+
assert "extension_data" in _cols(c, "attempts")
|
|
91
|
+
assert BirdStore._needs_repair(c, "3.0.0") is False
|
|
92
|
+
# re-open is a no-op and stays healthy
|
|
93
|
+
BirdStore._ensure_schema(c, bird)
|
|
94
|
+
assert "extension_data" in _cols(c, "attempts")
|
|
95
|
+
c.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-22-extension-system-design.md
RENAMED
|
File without changes
|
{blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|