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.
Files changed (201) hide show
  1. blq_cli-1.0.2/CHANGELOG.md +6 -0
  2. {blq_cli-1.0.1 → blq_cli-1.0.2}/PKG-INFO +1 -1
  3. {blq_cli-1.0.1 → blq_cli-1.0.2}/pyproject.toml +1 -1
  4. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/bird.py +111 -39
  5. blq_cli-1.0.2/tests/test_schema_repair.py +95 -0
  6. {blq_cli-1.0.1 → blq_cli-1.0.2}/.claude/hooks/blq-suggest.sh +0 -0
  7. {blq_cli-1.0.1 → blq_cli-1.0.2}/.github/workflows/ci.yml +0 -0
  8. {blq_cli-1.0.1 → blq_cli-1.0.2}/.github/workflows/docs.yml +0 -0
  9. {blq_cli-1.0.1 → blq_cli-1.0.2}/.github/workflows/publish.yml +0 -0
  10. {blq_cli-1.0.1 → blq_cli-1.0.2}/.gitignore +0 -0
  11. {blq_cli-1.0.1 → blq_cli-1.0.2}/.mcp.json +0 -0
  12. {blq_cli-1.0.1 → blq_cli-1.0.2}/.readthedocs.yml +0 -0
  13. {blq_cli-1.0.1 → blq_cli-1.0.2}/AGENT.md +0 -0
  14. {blq_cli-1.0.1 → blq_cli-1.0.2}/AGENTS.md +0 -0
  15. {blq_cli-1.0.1 → blq_cli-1.0.2}/CLAUDE.md +0 -0
  16. {blq_cli-1.0.1 → blq_cli-1.0.2}/CONTRIBUTING.md +0 -0
  17. {blq_cli-1.0.1 → blq_cli-1.0.2}/README.md +0 -0
  18. {blq_cli-1.0.1 → blq_cli-1.0.2}/SKILL.md +0 -0
  19. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/api-reference.md +0 -0
  20. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/capture.md +0 -0
  21. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/ci.md +0 -0
  22. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/completions.md +0 -0
  23. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/errors.md +0 -0
  24. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/exec.md +0 -0
  25. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/filter.md +0 -0
  26. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/index.md +0 -0
  27. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/init.md +0 -0
  28. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/maintenance.md +0 -0
  29. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/query.md +0 -0
  30. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/registry.md +0 -0
  31. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/report.md +0 -0
  32. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/run.md +0 -0
  33. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/sql.md +0 -0
  34. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/status.md +0 -0
  35. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/sync.md +0 -0
  36. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/commands/watch.md +0 -0
  37. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-bird-migration.md +0 -0
  38. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-commands.md +0 -0
  39. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-config-command.md +0 -0
  40. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-extensions.md +0 -0
  41. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-git-integration.md +0 -0
  42. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-hooks-v2.md +0 -0
  43. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-live-inspection.md +0 -0
  44. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-parameterized-commands.md +0 -0
  45. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-run-args.md +0 -0
  46. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-sandbox-specs.md +0 -0
  47. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-sync.md +0 -0
  48. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/design-track-save.md +0 -0
  49. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/duck-hunt-v2-migration.md +0 -0
  50. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/duck-hunt-v3-migration.md +0 -0
  51. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/proposal-bird-v5.md +0 -0
  52. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/design/skill-inspect-enrichment.md +0 -0
  53. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/getting-started.md +0 -0
  54. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/index.md +0 -0
  55. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/integration.md +0 -0
  56. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/mcp.md +0 -0
  57. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/explore-bird-sqlite-backend.md +0 -0
  58. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
  59. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/explore-nsjail-spack-package.md +0 -0
  60. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/patterns-integration-prompt.md +0 -0
  61. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plans/roadmap-to-1.0.md +0 -0
  62. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/plugin-guide.md +0 -0
  63. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/python-api.md +0 -0
  64. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/query-guide.md +0 -0
  65. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/requirements.txt +0 -0
  66. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/sandbox.md +0 -0
  67. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/schema-stability.md +0 -0
  68. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/schema.md +0 -0
  69. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
  70. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
  71. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
  72. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
  73. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
  74. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
  75. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
  76. {blq_cli-1.0.1 → blq_cli-1.0.2}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
  77. {blq_cli-1.0.1 → blq_cli-1.0.2}/experiments/agent-build-test.md +0 -0
  78. {blq_cli-1.0.1 → blq_cli-1.0.2}/mkdocs.yml +0 -0
  79. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/__init__.py +0 -0
  80. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/__main__.py +0 -0
  81. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/bird_schema.sql +0 -0
  82. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/cli.py +0 -0
  83. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/README.md +0 -0
  84. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/__init__.py +0 -0
  85. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/ci_cmd.py +0 -0
  86. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/clean_cmd.py +0 -0
  87. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/config_cmd.py +0 -0
  88. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/core.py +0 -0
  89. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/events.py +0 -0
  90. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/execution.py +0 -0
  91. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/hooks_cmd.py +0 -0
  92. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/hooks_gen.py +0 -0
  93. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/init_cmd.py +0 -0
  94. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/management.py +0 -0
  95. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/management_cmd.py +0 -0
  96. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/mcp_cmd.py +0 -0
  97. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/migrate.py +0 -0
  98. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/query_cmd.py +0 -0
  99. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/record_cmd.py +0 -0
  100. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/registry.py +0 -0
  101. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/report_cmd.py +0 -0
  102. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/sandbox_cmd.py +0 -0
  103. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/serve_cmd.py +0 -0
  104. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/sync_cmd.py +0 -0
  105. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/commands/watch_cmd.py +0 -0
  106. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/config_format.py +0 -0
  107. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/__init__.py +0 -0
  108. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/annotator.py +0 -0
  109. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/discovery.py +0 -0
  110. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/local_executor.py +0 -0
  111. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/ext/pipeline.py +0 -0
  112. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/git.py +0 -0
  113. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/github.py +0 -0
  114. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/locks.py +0 -0
  115. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/output.py +0 -0
  116. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/query.py +0 -0
  117. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/runtime.py +0 -0
  118. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/schema.sql +0 -0
  119. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/serve.py +0 -0
  120. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/__init__.py +0 -0
  121. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/execution.py +0 -0
  122. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/inspect.py +0 -0
  123. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/query.py +0 -0
  124. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/services/refs.py +0 -0
  125. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/storage.py +0 -0
  126. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/drone.yml.j2 +0 -0
  127. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/git_hook.sh.j2 +0 -0
  128. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/github_workflow.yml.j2 +0 -0
  129. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
  130. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/templates/hook_script.sh.j2 +0 -0
  131. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq/user_config.py +0 -0
  132. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/__init__.py +0 -0
  133. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/engines.py +0 -0
  134. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/profile.py +0 -0
  135. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/source_annotator.py +0 -0
  136. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/spec.py +0 -0
  137. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/strace_parser.py +0 -0
  138. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/tighten.py +0 -0
  139. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox/violations.py +0 -0
  140. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox_bwrap/__init__.py +0 -0
  141. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox_bwrap/args.py +0 -0
  142. {blq_cli-1.0.1 → blq_cli-1.0.2}/src/blq_sandbox_systemd/__init__.py +0 -0
  143. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/__init__.py +0 -0
  144. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/conftest.py +0 -0
  145. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
  146. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_annotator.py +0 -0
  147. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_attempts_outcomes.py +0 -0
  148. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_auto_init.py +0 -0
  149. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_autoprune.py +0 -0
  150. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_bird.py +0 -0
  151. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_bwrap_args.py +0 -0
  152. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_bwrap_engine.py +0 -0
  153. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ci.py +0 -0
  154. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ci_generators.py +0 -0
  155. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_command_args.py +0 -0
  156. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_command_lock_field.py +0 -0
  157. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_config_consumption.py +0 -0
  158. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_core.py +0 -0
  159. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_execution_locks.py +0 -0
  160. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_exit_code_reason.py +0 -0
  161. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_discovery.py +0 -0
  162. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_integration.py +0 -0
  163. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_local_executor.py +0 -0
  164. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_pipeline.py +0 -0
  165. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_ext_types.py +0 -0
  166. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_git.py +0 -0
  167. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_hooks.py +0 -0
  168. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_hooks_gen.py +0 -0
  169. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_inspect.py +0 -0
  170. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_json_null_filter.py +0 -0
  171. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_locks.py +0 -0
  172. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_mcp_ci_tools.py +0 -0
  173. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_mcp_merge.py +0 -0
  174. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_mcp_server.py +0 -0
  175. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_phase1_structured_output.py +0 -0
  176. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_phase2_command_registry.py +0 -0
  177. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_query_api.py +0 -0
  178. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_query_filter.py +0 -0
  179. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_record_invocation.py +0 -0
  180. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_report.py +0 -0
  181. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_runtime_config.py +0 -0
  182. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox.py +0 -0
  183. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_cmd.py +0 -0
  184. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_events.py +0 -0
  185. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_ext.py +0 -0
  186. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_profile.py +0 -0
  187. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_register.py +0 -0
  188. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_systemd.py +0 -0
  189. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_tighten.py +0 -0
  190. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_sandbox_violations.py +0 -0
  191. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_execution.py +0 -0
  192. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_inspect.py +0 -0
  193. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_query.py +0 -0
  194. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_services_refs.py +0 -0
  195. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_source_annotator.py +0 -0
  196. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_storage.py +0 -0
  197. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_storage_prune.py +0 -0
  198. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_strace_parser.py +0 -0
  199. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_user_config.py +0 -0
  200. {blq_cli-1.0.1 → blq_cli-1.0.2}/tests/test_watch.py +0 -0
  201. {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.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blq-cli"
7
- version = "1.0.1"
7
+ version = "1.0.2"
8
8
  description = "Build Log Query - capture and query build/test logs with DuckDB"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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
- # Load schema from SQL file
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 (rename sandbox to extension_data)
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
- migration_ok = True
563
- for table in ("attempts", "invocations"):
564
- try:
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
File without changes
File without changes
File without changes