dirsql 0.4.1__tar.gz → 0.4.3__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 (212) hide show
  1. {dirsql-0.4.1 → dirsql-0.4.3}/Cargo.lock +1 -1
  2. {dirsql-0.4.1 → dirsql-0.4.3}/PKG-INFO +1 -1
  3. {dirsql-0.4.1/packages/rust → dirsql-0.4.3}/docs/reference/hooks.md +1 -1
  4. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/Cargo.toml +1 -1
  5. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/hooks.md +1 -1
  6. dirsql-0.4.3/packages/python/e2e-attestations/claude-710-accumulate-onfile.json +7 -0
  7. dirsql-0.4.3/packages/python/e2e-attestations/claude-714-unify-file-failures.json +7 -0
  8. dirsql-0.4.3/packages/rust/changelog.d/2026-07-29-on-file-failures-accumulate.md +8 -0
  9. dirsql-0.4.3/packages/rust/changelog.d/2026-08-02-per-file-scan-failures.md +35 -0
  10. {dirsql-0.4.1 → dirsql-0.4.3/packages/rust}/docs/reference/hooks.md +1 -1
  11. dirsql-0.4.3/packages/rust/migrations.d/2026-08-02-per-file-scan-failures.md +100 -0
  12. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/bin/dirsql.rs +42 -1
  13. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/lib.rs +112 -46
  14. dirsql-0.4.1/packages/python/e2e-attestations/claude-tackle-698-bz6ere.json +0 -7
  15. {dirsql-0.4.1 → dirsql-0.4.3}/Cargo.toml +0 -0
  16. {dirsql-0.4.1 → dirsql-0.4.3}/README.md +0 -0
  17. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/__init__.py +0 -0
  18. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/_async.py +0 -0
  19. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/_dirsql.pyi +0 -0
  20. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/__init__.py +0 -0
  21. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/binary_path.py +0 -0
  22. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/discover_plugins/__init__.py +0 -0
  23. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  24. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  25. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  26. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  27. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  28. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/is_windows.py +0 -0
  29. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/main.py +0 -0
  30. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/cli/resolve_config_extensions.py +0 -0
  31. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/py.typed +0 -0
  32. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/resolve_config_extensions.py +0 -0
  33. {dirsql-0.4.1 → dirsql-0.4.3}/dirsql/resolve_extension.py +0 -0
  34. {dirsql-0.4.1 → dirsql-0.4.3}/docs/.claude/CLAUDE.md +0 -0
  35. {dirsql-0.4.1 → dirsql-0.4.3}/docs/.vitepress/config.ts +0 -0
  36. {dirsql-0.4.1 → dirsql-0.4.3}/docs/.vitepress/theme/index.ts +0 -0
  37. {dirsql-0.4.1 → dirsql-0.4.3}/docs/.vitepress/theme/lang.ts +0 -0
  38. {dirsql-0.4.1 → dirsql-0.4.3}/docs/AGENTS.md +0 -0
  39. {dirsql-0.4.1 → dirsql-0.4.3}/docs/explanation.md +0 -0
  40. {dirsql-0.4.1 → dirsql-0.4.3}/docs/getting-started.md +0 -0
  41. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/columns-from-paths.md +0 -0
  42. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/define-tables.md +0 -0
  43. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/embed.md +0 -0
  44. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/extract-from-contents.md +0 -0
  45. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/load-extension.md +0 -0
  46. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/parse-files-into-columns.md +0 -0
  47. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/persist.md +0 -0
  48. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/query-without-config.md +0 -0
  49. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/react-to-changes.md +0 -0
  50. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/search-by-meaning.md +0 -0
  51. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/skip-files.md +0 -0
  52. {dirsql-0.4.1 → dirsql-0.4.3}/docs/howto/write-a-plugin.md +0 -0
  53. {dirsql-0.4.1 → dirsql-0.4.3}/docs/index.md +0 -0
  54. {dirsql-0.4.1 → dirsql-0.4.3}/docs/migrations.md +0 -0
  55. {dirsql-0.4.1 → dirsql-0.4.3}/docs/package.json +0 -0
  56. {dirsql-0.4.1 → dirsql-0.4.3}/docs/pnpm-lock.yaml +0 -0
  57. {dirsql-0.4.1 → dirsql-0.4.3}/docs/pnpm-workspace.yaml +0 -0
  58. {dirsql-0.4.1 → dirsql-0.4.3}/docs/reference/cli.md +0 -0
  59. {dirsql-0.4.1 → dirsql-0.4.3}/docs/reference/columns.md +0 -0
  60. {dirsql-0.4.1 → dirsql-0.4.3}/docs/reference/config.md +0 -0
  61. {dirsql-0.4.1 → dirsql-0.4.3}/docs/reference/http-api.md +0 -0
  62. {dirsql-0.4.1 → dirsql-0.4.3}/docs/reference/path-tables.md +0 -0
  63. {dirsql-0.4.1 → dirsql-0.4.3}/docs/reference/sdk.md +0 -0
  64. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/CHANGELOG.md +0 -0
  65. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/MIGRATIONS.md +0 -0
  66. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/README.md +0 -0
  67. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  68. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  69. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  70. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  71. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  72. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  73. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  74. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  75. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  76. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  77. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/changelog.d/README.md +0 -0
  78. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/conftest.py +0 -0
  79. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/.claude/CLAUDE.md +0 -0
  80. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/.vitepress/config.ts +0 -0
  81. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  82. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  83. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/AGENTS.md +0 -0
  84. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/explanation.md +0 -0
  85. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/getting-started.md +0 -0
  86. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/columns-from-paths.md +0 -0
  87. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/define-tables.md +0 -0
  88. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/embed.md +0 -0
  89. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/extract-from-contents.md +0 -0
  90. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/load-extension.md +0 -0
  91. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  92. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/persist.md +0 -0
  93. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/query-without-config.md +0 -0
  94. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/react-to-changes.md +0 -0
  95. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/search-by-meaning.md +0 -0
  96. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/skip-files.md +0 -0
  97. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/howto/write-a-plugin.md +0 -0
  98. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/index.md +0 -0
  99. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/migrations.md +0 -0
  100. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/package.json +0 -0
  101. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/pnpm-lock.yaml +0 -0
  102. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/pnpm-workspace.yaml +0 -0
  103. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/cli.md +0 -0
  104. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/columns.md +0 -0
  105. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/config.md +0 -0
  106. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/http-api.md +0 -0
  107. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/path-tables.md +0 -0
  108. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/docs/reference/sdk.md +0 -0
  109. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  110. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  111. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  112. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  113. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  114. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  115. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  116. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  117. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/migrations.d/README.md +0 -0
  118. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/src/lib.rs +0 -0
  119. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/__init__.py +0 -0
  120. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/conftest.py +0 -0
  121. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/e2e/__init__.py +0 -0
  122. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  123. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  124. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/integration/__init__.py +0 -0
  125. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/integration/binding/__init__.py +0 -0
  126. {dirsql-0.4.1 → dirsql-0.4.3}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  127. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/CHANGELOG.md +0 -0
  128. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/Cargo.toml +0 -0
  129. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/MIGRATIONS.md +0 -0
  130. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/README.md +0 -0
  131. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/benches/db_bench.rs +0 -0
  132. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/benches/differ_bench.rs +0 -0
  133. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/benches/matcher_bench.rs +0 -0
  134. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/benches/scanner_bench.rs +0 -0
  135. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-batch-ingest-transaction.md +0 -0
  136. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-chained-pre-post-query-hooks.md +0 -0
  137. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  138. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-fan-out-file-table-matching.md +0 -0
  139. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-persist-wal.md +0 -0
  140. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  141. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-repeatable-builder-config.md +0 -0
  142. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-repeatable-cli-config.md +0 -0
  143. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  144. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-13-subcommand-local-config-flags.md +0 -0
  145. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-19-path-table-query-fallback.md +0 -0
  146. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-19-path-table-vtab.md +0 -0
  147. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  148. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-20-path-table-glob-semantics.md +0 -0
  149. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-20-schema-inference.md +0 -0
  150. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-21-on-file-flag.md +0 -0
  151. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  152. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-23-init-escalation-example.md +0 -0
  153. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-23-query-default-cli.md +0 -0
  154. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  155. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  156. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/changelog.d/README.md +0 -0
  157. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/explanation.md +0 -0
  158. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/getting-started.md +0 -0
  159. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  160. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/define-tables.md +0 -0
  161. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/embed.md +0 -0
  162. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  163. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/load-extension.md +0 -0
  164. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  165. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/persist.md +0 -0
  166. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/query-without-config.md +0 -0
  167. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/react-to-changes.md +0 -0
  168. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  169. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/skip-files.md +0 -0
  170. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  171. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/index.md +0 -0
  172. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/migrations.md +0 -0
  173. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/reference/cli.md +0 -0
  174. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/reference/columns.md +0 -0
  175. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/reference/config.md +0 -0
  176. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/reference/http-api.md +0 -0
  177. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/reference/path-tables.md +0 -0
  178. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/docs/reference/sdk.md +0 -0
  179. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  180. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-13-fan-out-file-table-matching.md +0 -0
  181. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-13-persist-wal.md +0 -0
  182. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  183. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  184. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-13-subcommand-local-config-flags.md +0 -0
  185. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  186. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-20-path-table-glob-semantics.md +0 -0
  187. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  188. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-23-init-escalation-example.md +0 -0
  189. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-23-query-default-cli.md +0 -0
  190. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  191. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  192. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/migrations.d/README.md +0 -0
  193. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/cli/execute.rs +0 -0
  194. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/cli/init.rs +0 -0
  195. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/cli/mod.rs +0 -0
  196. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/cli/router.rs +0 -0
  197. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/cli/serialize.rs +0 -0
  198. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/cli/server.rs +0 -0
  199. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/command.rs +0 -0
  200. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/config.rs +0 -0
  201. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/db.rs +0 -0
  202. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/default_config.toml +0 -0
  203. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/differ.rs +0 -0
  204. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/infer.rs +0 -0
  205. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/matcher.rs +0 -0
  206. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/parsed_vtab.rs +0 -0
  207. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/path_table.rs +0 -0
  208. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/persist.rs +0 -0
  209. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/scanner.rs +0 -0
  210. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/vtab.rs +0 -0
  211. {dirsql-0.4.1 → dirsql-0.4.3}/packages/rust/src/watcher.rs +0 -0
  212. {dirsql-0.4.1 → dirsql-0.4.3}/pyproject.toml +0 -0
@@ -500,7 +500,7 @@ dependencies = [
500
500
 
501
501
  [[package]]
502
502
  name = "dirsql-py-ext"
503
- version = "0.4.1"
503
+ version = "0.4.3"
504
504
  dependencies = [
505
505
  "dirsql",
506
506
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Requires-Dist: tomli>=2 ; python_full_version < '3.11'
5
5
  Summary: Ephemeral SQL index over a local directory
6
6
  Keywords: sql,filesystem,directory,sqlite,index
@@ -86,7 +86,7 @@ What a failure *means* differs per hook:
86
86
 
87
87
  | Hook | On failure |
88
88
  |---|---|
89
- | `on-file` | **Per-file isolation.** The file contributes no rows; a one-line warning naming the file and error goes to stderr; the scan continues. One bad file never aborts the scan. |
89
+ | `on-file` | **Per-file isolation.** The file contributes no rows and is reported as skipped; the scan indexes every other file and commits. The CLI names up to ten skipped files on stderr, then `... and N more`, and exits `23` — distinct from `0` (clean) and `1` (the run failed), so a caller can tell a partial index from a complete one. A row the table rejects under `strict` counts as the same kind of failure. |
90
90
  | `pre-query` | The request returns `500 Internal Server Error` with the command's stderr tail in the JSON `error` body. |
91
91
  | `post-query` | The request returns `500 Internal Server Error` with the command's stderr tail (or, for unparseable output, `post-query did not return valid JSON: <err>`) in the JSON `error` body. |
92
92
 
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
4
4
  # pypi/maturin handler can rewrite it via `write-version` before
5
5
  # `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
6
6
  # and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
7
- version = "0.4.1"
7
+ version = "0.4.3"
8
8
  # abi3 note: the `abi3-py310` pyo3 feature (below) builds ONE stable-ABI
9
9
  # `cp310-abi3` wheel per platform that loads on every CPython >= 3.10
10
10
  # (matching `pyproject.toml`'s `requires-python = ">=3.10"`), instead of
@@ -86,7 +86,7 @@ What a failure *means* differs per hook:
86
86
 
87
87
  | Hook | On failure |
88
88
  |---|---|
89
- | `on-file` | **Per-file isolation.** The file contributes no rows; a one-line warning naming the file and error goes to stderr; the scan continues. One bad file never aborts the scan. |
89
+ | `on-file` | **Per-file isolation.** The file contributes no rows and is reported as skipped; the scan indexes every other file and commits. The CLI names up to ten skipped files on stderr, then `... and N more`, and exits `23` — distinct from `0` (clean) and `1` (the run failed), so a caller can tell a partial index from a complete one. A row the table rejects under `strict` counts as the same kind of failure. |
90
90
  | `pre-query` | The request returns `500 Internal Server Error` with the command's stderr tail in the JSON `error` body. |
91
91
  | `post-query` | The request returns `500 Internal Server Error` with the command's stderr tail (or, for unparseable output, `post-query did not return valid JSON: <err>`) in the JSON `error` body. |
92
92
 
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "uv run python -m pytest tests/e2e/ -x -q",
3
+ "ran_at": 1785444370,
4
+ "exit_code": 0,
5
+ "commit": "287fb980b2380a894543a652a0439f6ab8ea71cf",
6
+ "branch": "claude/710-accumulate-onfile"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "uv run python -m pytest tests/e2e/ -x -q",
3
+ "ran_at": 1785673957,
4
+ "exit_code": 0,
5
+ "commit": "484c89828d5a512f7fd51dc8e5995d498e857c0b",
6
+ "branch": "claude/714-unify-file-failures"
7
+ }
@@ -0,0 +1,8 @@
1
+ **Changed** — a scan now attempts every matched file instead of stopping at the
2
+ first whose `on-file` hook fails, and reports every failure rather than only the
3
+ first. A single failure still raises `DirSqlError::OnFile` unchanged; several
4
+ now raise a new `DirSqlError::OnFileMany` carrying one `OnFileFailure { path,
5
+ message }` per file.
6
+
7
+ A scan with failures still fails and still leaves the cache exactly as it was —
8
+ only the reporting improved. Committing a partial index is tracked in #697.
@@ -0,0 +1,35 @@
1
+ **Fixed**
2
+
3
+ A file the scan cannot index no longer costs the files around it, and a partial
4
+ index is now visible at the exit code instead of passing for a complete one
5
+ (#714).
6
+
7
+ Two failure modes behaved inconsistently. A hook that exited non-zero or emitted
8
+ malformed output was already skipped — `run_on_file` logged it and returned an
9
+ empty row set — but a row that failed **strict** normalization aborted the whole
10
+ scan, so one bad column cost every other file's rows. Both are now the same
11
+ thing: the file is skipped, the scan commits what it could index, and the
12
+ failure is recorded.
13
+
14
+ - `DirSQL::scan_failures()` returns the files the scan could not index, each
15
+ with its root-relative path and the hook's own message. Empty for a clean
16
+ scan.
17
+ - The CLI reports skips on **stderr**, at most ten by name followed by
18
+ `... and N more`, and exits **23** — rsync's "partial transfer due to error" —
19
+ when any file was skipped. stdout still carries only the query result, so
20
+ `dirsql "SELECT …" | jq` keeps working and can now tell a partial index from a
21
+ complete one. A clean scan still exits `0`; a failed run still exits `1`.
22
+ - `run_on_file` returns `Result` rather than logging and flattening to an empty
23
+ row set, so a skip and a file that legitimately produced no rows are no longer
24
+ indistinguishable.
25
+
26
+ SQLite, DDL and extension errors are unchanged: they still abort the scan and
27
+ roll back. Only per-file hook outcomes became recoverable.
28
+
29
+ A skipped file never reaches the persistent file index, so it is retried on the
30
+ next scan — the cache is incomplete, never wrong.
31
+
32
+ **Removed**
33
+
34
+ `DirSqlError::OnFile` and `DirSqlError::OnFileMany`. Nothing can produce them
35
+ now that a per-file failure is not a scan error.
@@ -86,7 +86,7 @@ What a failure *means* differs per hook:
86
86
 
87
87
  | Hook | On failure |
88
88
  |---|---|
89
- | `on-file` | **Per-file isolation.** The file contributes no rows; a one-line warning naming the file and error goes to stderr; the scan continues. One bad file never aborts the scan. |
89
+ | `on-file` | **Per-file isolation.** The file contributes no rows and is reported as skipped; the scan indexes every other file and commits. The CLI names up to ten skipped files on stderr, then `... and N more`, and exits `23` — distinct from `0` (clean) and `1` (the run failed), so a caller can tell a partial index from a complete one. A row the table rejects under `strict` counts as the same kind of failure. |
90
90
  | `pre-query` | The request returns `500 Internal Server Error` with the command's stderr tail in the JSON `error` body. |
91
91
  | `post-query` | The request returns `500 Internal Server Error` with the command's stderr tail (or, for unparseable output, `post-query did not return valid JSON: <err>`) in the JSON `error` body. |
92
92
 
@@ -0,0 +1,100 @@
1
+ ## Summary
2
+
3
+ A per-file failure during the initial scan is no longer fatal (#714). A hook
4
+ that fails, or that returns a row the table rejects, costs that file alone: the
5
+ scan indexes everything else, commits, and records the failure. Previously a
6
+ strict-mode violation aborted the scan outright — one bad column cost every
7
+ other file's rows — while a non-zero hook exit was already silently skipped. The
8
+ two are now consistent.
9
+
10
+ The visible consequence is an exit code. A run that skipped files exits **23**
11
+ (rsync's "partial transfer due to error") instead of `0`, so a caller can tell a
12
+ partial index from a complete one. That distinction did not exist before, which
13
+ is why it is a breaking change for scripts rather than a pure improvement.
14
+
15
+ ## Required changes
16
+
17
+ **`DirSqlError::OnFile` and `DirSqlError::OnFileMany` are removed.** Nothing can
18
+ produce them now. An exhaustive `match` on `DirSqlError` drops those arms:
19
+
20
+ ```rust
21
+ // before
22
+ match err {
23
+ DirSqlError::OnFile { path, message } => report(path, message),
24
+ DirSqlError::OnFileMany { failures } => failures.iter().for_each(report_one),
25
+ other => return Err(other),
26
+ }
27
+
28
+ // after — the build no longer fails for this reason; ask the database instead
29
+ let db = DirSQL::new(root, tables)?;
30
+ for failure in db.scan_failures() {
31
+ report(&failure.path, &failure.message);
32
+ }
33
+ ```
34
+
35
+ **A script that treats any non-zero exit as failure now sees `23`.** If a
36
+ partial index should be acceptable, accept it explicitly:
37
+
38
+ ```sh
39
+ # before: relied on `dirsql` exiting 0 while quietly skipping files
40
+ dirsql "SELECT * FROM docs" | jq .
41
+
42
+ # after: accept a partial index, still failing on a real error
43
+ dirsql "SELECT * FROM docs" | jq . || [ $? -eq 23 ]
44
+ ```
45
+
46
+ Under `set -e` and *without* that guard, a scan with skipped files now stops the
47
+ script — which is the point: it previously continued against a silently
48
+ incomplete index.
49
+
50
+ ## Deprecations removed
51
+
52
+ _None._
53
+
54
+ ## Behavior changes without code changes
55
+
56
+ **A strict-mode violation skips its file instead of aborting the scan.** A
57
+ `strict` table whose hook returns an unexpected or missing column used to fail
58
+ the build; it now yields a skipped file and a built database containing every
59
+ other file's rows. The rejected row is still never inserted, and the column name
60
+ still never reaches SQL — only the blast radius changed.
61
+
62
+ **Skipped files are reported by the CLI, capped.** At most ten are named on
63
+ stderr, followed by `... and N more`. Previously `run_on_file` printed one line
64
+ per failing file with no cap, and printed nothing at all for strict violations
65
+ (they aborted instead). Paths in these lines are now root-relative, where the
66
+ old `run_on_file` line printed the absolute path.
67
+
68
+ **A library consumer no longer gets stderr noise from the core.** `run_on_file`
69
+ returned `Vec<Row>` and logged its own failures; it now returns `Result` and the
70
+ scan records them. Read `DirSQL::scan_failures()` instead of scraping stderr.
71
+
72
+ **The persistent cache commits partially.** A scan with failures used to roll
73
+ back entirely; it now commits the files that parsed. The failed file never
74
+ reaches the file index, so the next scan treats it as unknown and retries it —
75
+ the cache is incomplete, never wrong. A caller that relied on "a failed build
76
+ leaves the cache untouched" no longer gets that.
77
+
78
+ **Watch-mode gains an error event it did not emit.** With `run_on_file` now
79
+ returning `Result`, a command hook that fails during live watching produces an
80
+ `error` row event, where it previously produced silence.
81
+
82
+ ## Verification
83
+
84
+ ```sh
85
+ # a directory with one good file and two whose on-file hook exits non-zero
86
+ dirsql "SELECT name FROM items" -c .dirsql.toml; echo "EXIT=$?"
87
+ ```
88
+
89
+ Expected: the good file's rows on stdout as JSON, one `dirsql: skipping …` line
90
+ per failed file on stderr, and `EXIT=23`. Before this change the same command
91
+ printed the rows and `EXIT=0`.
92
+
93
+ ```sh
94
+ # a `strict = true` table where one file's hook emits an unexpected column
95
+ dirsql "SELECT name FROM items" -c strict.toml; echo "EXIT=$?"
96
+ ```
97
+
98
+ Expected: every well-formed file's rows on stdout, one skip line naming the bad
99
+ file, and `EXIT=23`. Before this change: no rows at all, a
100
+ `Schema mismatch: extra columns` error on stderr, and `EXIT=1`.
@@ -280,6 +280,7 @@ async fn run_query(args: QueryArgs) -> ExitCode {
280
280
  }
281
281
  };
282
282
  let state = load_state(&args.common, parser);
283
+ let skipped = report_scan_failures(&state);
283
284
  let pre_query = load_pre_queries(&args.common);
284
285
  let post_query = load_post_queries(&args.common);
285
286
  // Same default the server binds with; the pipeline enforces it.
@@ -296,7 +297,15 @@ async fn run_query(args: QueryArgs) -> ExitCode {
296
297
  {
297
298
  Ok(value) => {
298
299
  println!("{value}");
299
- ExitCode::SUCCESS
300
+ // The query ran and its rows are on stdout, so this is not a
301
+ // failure -- but the index behind them is missing files, and a
302
+ // caller piping into `jq` under `set -e` has no other way to find
303
+ // that out.
304
+ if skipped {
305
+ ExitCode::from(PARTIAL_SCAN_EXIT)
306
+ } else {
307
+ ExitCode::SUCCESS
308
+ }
300
309
  }
301
310
  Err(failure) => {
302
311
  eprintln!("dirsql query: {}", failure.message());
@@ -305,6 +314,38 @@ async fn run_query(args: QueryArgs) -> ExitCode {
305
314
  }
306
315
  }
307
316
 
317
+ /// Exit code for "the scan completed, but some files were skipped" -- distinct
318
+ /// from `1` so a script can tell a partial index from a run that failed.
319
+ /// Follows rsync's `23` ("partial transfer due to error").
320
+ const PARTIAL_SCAN_EXIT: u8 = 23;
321
+
322
+ /// How many skipped files to name before collapsing the rest into a count. A
323
+ /// directory of unreadable files should not bury the terminal.
324
+ const MAX_REPORTED_FAILURES: usize = 10;
325
+
326
+ /// Print the scan's skipped files to stderr, capped, and report whether there
327
+ /// were any. stdout is left for the query result alone.
328
+ fn report_scan_failures(state: &AppState) -> bool {
329
+ let AppState::Ready(db) = state else {
330
+ return false;
331
+ };
332
+ let failures = db.scan_failures();
333
+ if failures.is_empty() {
334
+ return false;
335
+ }
336
+ for failure in failures.iter().take(MAX_REPORTED_FAILURES) {
337
+ eprintln!("dirsql: skipping `{}`: {}", failure.path, failure.message);
338
+ }
339
+ if let Some(rest) = failures
340
+ .len()
341
+ .checked_sub(MAX_REPORTED_FAILURES)
342
+ .filter(|n| *n > 0)
343
+ {
344
+ eprintln!("dirsql: ... and {rest} more");
345
+ }
346
+ true
347
+ }
348
+
308
349
  /// Synthesize the exact `POST /query` body for a positional SQL argument,
309
350
  /// so the shared pipeline's intake validation and `pre-query` hook see
310
351
  /// byte-for-byte what an HTTP client would send.
@@ -78,6 +78,16 @@ pub const DEFAULT_CONFIG_TOML: &str = include_str!("default_config.toml");
78
78
  type BoxError = Box<dyn StdError + Send + Sync + 'static>;
79
79
  type OnFileFn = dyn Fn(&str) -> std::result::Result<Vec<Row>, BoxError> + Send + Sync + 'static;
80
80
 
81
+ /// One file whose on-file hook failed, carried so a scan can report every
82
+ /// failure rather than only whichever came first.
83
+ #[derive(Debug, Clone, PartialEq, Eq)]
84
+ pub struct OnFileFailure {
85
+ /// Path relative to the scan root.
86
+ pub path: String,
87
+ /// The hook's error, as rendered by its `Display`.
88
+ pub message: String,
89
+ }
90
+
81
91
  #[derive(Debug, Error)]
82
92
  pub enum DirSqlError {
83
93
  #[error(transparent)]
@@ -119,9 +129,6 @@ pub enum DirSqlError {
119
129
  #[error("duplicate table name: {0}")]
120
130
  DuplicateTable(String),
121
131
 
122
- #[error("on-file error for {path}: {message}")]
123
- OnFile { path: String, message: String },
124
-
125
132
  #[error("config error: {message}")]
126
133
  Config {
127
134
  message: String,
@@ -249,6 +256,8 @@ struct DirSqlInner {
249
256
  matcher: TableMatcher,
250
257
  on_file_map: HashMap<String, Arc<OnFileFn>>,
251
258
  strict_map: HashMap<String, bool>,
259
+ /// Files the initial scan could not index. Empty for a clean scan.
260
+ scan_failures: Vec<OnFileFailure>,
252
261
  watcher: Mutex<Option<Watcher>>,
253
262
  /// Locks out [`DirSQL::watch`] once [`DirSQL::poll_events`] has run:
254
263
  /// both would drain the same underlying watcher.
@@ -711,6 +720,18 @@ impl DirSQL {
711
720
  })
712
721
  }
713
722
 
723
+ /// The files the initial scan could not index, in scan order — each with
724
+ /// the path relative to the root and the hook's own error.
725
+ ///
726
+ /// A hook that fails, or returns a row the table rejects, costs that file
727
+ /// and no other: the scan indexes everything else and records the failure
728
+ /// here. Empty for a clean scan, which is the signal a caller checks —
729
+ /// non-empty means the index is *incomplete*, not wrong, and the missing
730
+ /// files are retried on the next scan.
731
+ pub fn scan_failures(&self) -> &[OnFileFailure] {
732
+ &self.inner.scan_failures
733
+ }
734
+
714
735
  /// Split-phase construction — part 2. Consumes the intermediate state from
715
736
  /// [`prepare_resolved`](Self::prepare_resolved): creates the SQLite
716
737
  /// database (or wires up the persistent on-disk one), runs each table's
@@ -816,6 +837,7 @@ impl DirSQL {
816
837
  }
817
838
 
818
839
  let snapshot_ns = now_ns();
840
+ let mut on_file_failures: Vec<OnFileFailure> = Vec::new();
819
841
  for ScannedFile {
820
842
  rel_path,
821
843
  table_name,
@@ -827,11 +849,20 @@ impl DirSQL {
827
849
  })?;
828
850
  let strict = *strict_map.get(&table_name).unwrap_or(&false);
829
851
  let abs_path = root.join(&rel_path);
830
- let raw_rows =
831
- on_file(&abs_path.to_string_lossy()).map_err(|e| DirSqlError::OnFile {
832
- path: rel_path.clone(),
833
- message: e.to_string(),
834
- })?;
852
+ // A hook failure is this file's problem, not the scan's: record it
853
+ // and keep going, so one unreadable file cannot hide the state of
854
+ // every file after it. The collected failures still fail the build
855
+ // below -- whether a partial index commits is dirsql#697.
856
+ let raw_rows = match on_file(&abs_path.to_string_lossy()) {
857
+ Ok(rows) => rows,
858
+ Err(e) => {
859
+ on_file_failures.push(OnFileFailure {
860
+ path: rel_path.clone(),
861
+ message: e.to_string(),
862
+ });
863
+ continue;
864
+ }
865
+ };
835
866
 
836
867
  // When updating an existing file in the persistent cache, drop
837
868
  // its old rows before inserting the new ones.
@@ -839,9 +870,29 @@ impl DirSQL {
839
870
  db.delete_rows_by_file(&table_name, &rel_path)
840
871
  .map_err(map_db_error)?;
841
872
  }
842
- for (row_index, raw_row) in raw_rows.iter().enumerate() {
843
- let row = db.normalize_row(&table_name, raw_row, strict)?;
844
- db.insert_row(&table_name, &row, &rel_path, row_index)
873
+ // Normalize every row before inserting any: a row the hook got
874
+ // wrong is that file's failure, and half-inserting a file would
875
+ // leave rows behind for a file the scan reports as skipped.
876
+ let mut normalized = Vec::with_capacity(raw_rows.len());
877
+ let mut rejected = None;
878
+ for raw_row in &raw_rows {
879
+ match db.normalize_row(&table_name, raw_row, strict) {
880
+ Ok(row) => normalized.push(row),
881
+ Err(e) => {
882
+ rejected = Some(e.to_string());
883
+ break;
884
+ }
885
+ }
886
+ }
887
+ if let Some(message) = rejected {
888
+ on_file_failures.push(OnFileFailure {
889
+ path: rel_path.clone(),
890
+ message,
891
+ });
892
+ continue;
893
+ }
894
+ for (row_index, row) in normalized.iter().enumerate() {
895
+ db.insert_row(&table_name, row, &rel_path, row_index)
845
896
  .map_err(map_db_error)?;
846
897
  }
847
898
 
@@ -870,6 +921,13 @@ impl DirSQL {
870
921
  write_meta(db.conn(), meta).map_err(DirSqlError::sqlite)?;
871
922
  }
872
923
 
924
+ // A per-file failure no longer cancels the scan: what could be indexed
925
+ // is committed, and the failures ride on the built database for the
926
+ // caller to report (`scan_failures`). A file that failed never reached
927
+ // `upsert_file`, so it is absent from the cache index and is retried on
928
+ // the next scan -- the cache is incomplete, never wrong. SQLite, DDL
929
+ // and extension errors still propagate above, unchanged.
930
+
873
931
  // Commit the ingest transaction.
874
932
  _tx.commit().map_err(DirSqlError::sqlite)?;
875
933
 
@@ -885,6 +943,7 @@ impl DirSQL {
885
943
  matcher,
886
944
  on_file_map,
887
945
  strict_map,
946
+ scan_failures: on_file_failures,
888
947
  watcher: Mutex::new(None),
889
948
  poll_used: AtomicBool::new(false),
890
949
  watch_thread_started: AtomicBool::new(false),
@@ -1493,10 +1552,9 @@ fn build_tables_from_config(
1493
1552
  let command = table_cfg.on_file.clone();
1494
1553
  let config_dir = config_dir.to_path_buf();
1495
1554
  let root = root.to_path_buf();
1496
- // `Table::new` (infallible): `run_on_file` isolates its own errors to an
1497
- // empty row set so one bad file never aborts the scan (the scan aborts
1498
- // on an on_file `Err`).
1499
- let mut table = Table::new(
1555
+ // `Table::try_new`: a hook failure is the scan's to record, not this
1556
+ // closure's to hide. The scan skips the file and reports it.
1557
+ let mut table = Table::try_new(
1500
1558
  table_cfg.ddl.clone(),
1501
1559
  table_cfg.glob.clone(),
1502
1560
  move |abs_path: &str| run_on_file(&command, abs_path, &config_dir, &root, timeout),
@@ -1520,38 +1578,33 @@ fn build_tables_from_config(
1520
1578
  /// config lives outside the index still resolves it. A template that omits a
1521
1579
  /// placeholder simply never receives its value — nothing is appended.
1522
1580
  ///
1523
- /// Per-file isolation: any failure — a spawn/exit/timeout error from
1524
- /// [`command::run_command`], or output that is not a JSON array of objects —
1525
- /// is logged to stderr and yields no rows (`vec![]`). Returning `Err` here
1526
- /// would abort the whole scan, so it never does. `timeout` bounds the run
1527
- /// the table's configured value, or the shared default.
1581
+ /// Any failure — a spawn/exit/timeout error from [`command::run_command`], or
1582
+ /// output that is not a JSON array of objects — is returned to the caller. The
1583
+ /// scan turns it into a skipped file rather than a scan error, and needs the
1584
+ /// `Err` to tell a skip apart from a file that legitimately produced no rows.
1585
+ /// `timeout` bounds the run — the table's configured value, or the shared
1586
+ /// default.
1528
1587
  fn run_on_file(
1529
1588
  command: &str,
1530
1589
  abs_path: &str,
1531
1590
  config_dir: &Path,
1532
1591
  root: &Path,
1533
1592
  timeout: Duration,
1534
- ) -> Vec<Row> {
1593
+ ) -> std::result::Result<Vec<Row>, BoxError> {
1535
1594
  let placeholders = [
1536
1595
  Placeholder::new("path", abs_path),
1537
1596
  Placeholder::new("root", root.to_string_lossy().into_owned()),
1538
1597
  ];
1539
1598
 
1540
- match command::run_command(command, &placeholders, config_dir, timeout, None) {
1541
- Ok(output) => match parse_command_rows(&output.payload) {
1542
- Ok(rows) => rows,
1543
- Err(message) => {
1544
- eprintln!(
1545
- "dirsql: skipping `{abs_path}`: on-file output was not a JSON array of rows: {message}"
1546
- );
1547
- Vec::new()
1548
- }
1549
- },
1550
- Err(error) => {
1551
- eprintln!("dirsql: skipping `{abs_path}`: on-file command failed: {error}");
1552
- Vec::new()
1553
- }
1554
- }
1599
+ // Errors travel to the caller rather than being logged and flattened to an
1600
+ // empty row set here. Swallowing them made a skip indistinguishable from a
1601
+ // file that legitimately produced no rows, so the scan could not count what
1602
+ // it dropped, cap the report, or exit differently.
1603
+ let output = command::run_command(command, &placeholders, config_dir, timeout, None)
1604
+ .map_err(|error| -> BoxError { format!("on-file command failed: {error}").into() })?;
1605
+ parse_command_rows(&output.payload).map_err(|message| -> BoxError {
1606
+ format!("on-file output was not a JSON array of rows: {message}").into()
1607
+ })
1555
1608
  }
1556
1609
 
1557
1610
  /// Parse an `on-file` command's stdout payload — a JSON array of row objects —
@@ -3151,7 +3204,8 @@ mod internal_tests {
3151
3204
  dir.path(),
3152
3205
  dir.path(),
3153
3206
  command::DEFAULT_COMMAND_TIMEOUT,
3154
- );
3207
+ )
3208
+ .expect("a well-formed payload parses");
3155
3209
  assert_eq!(rows.len(), 1);
3156
3210
  assert_eq!(rows[0]["n"], Value::Integer(1));
3157
3211
  }
@@ -3170,7 +3224,8 @@ mod internal_tests {
3170
3224
  dir.path(),
3171
3225
  dir.path(),
3172
3226
  command::DEFAULT_COMMAND_TIMEOUT,
3173
- );
3227
+ )
3228
+ .expect("a well-formed payload parses");
3174
3229
  assert_eq!(rows.len(), 1);
3175
3230
  assert_eq!(rows[0]["q"], Value::Text("{abspath}".into()));
3176
3231
  }
@@ -3190,7 +3245,8 @@ mod internal_tests {
3190
3245
  dir.path(),
3191
3246
  dir.path(),
3192
3247
  command::DEFAULT_COMMAND_TIMEOUT,
3193
- );
3248
+ )
3249
+ .expect("a well-formed payload parses");
3194
3250
  assert_eq!(rows.len(), 1);
3195
3251
  assert_eq!(
3196
3252
  rows[0]["p"],
@@ -3198,31 +3254,41 @@ mod internal_tests {
3198
3254
  );
3199
3255
  }
3200
3256
 
3201
- /// A command that cannot be spawned yields no rows (per-file isolation).
3257
+ /// A command that cannot be spawned is an error the caller records, not an
3258
+ /// empty row set: a skip and a file that legitimately produced no rows must
3259
+ /// stay distinguishable.
3202
3260
  #[test]
3203
- fn run_on_file_returns_no_rows_on_spawn_failure() {
3261
+ fn run_on_file_errors_on_spawn_failure() {
3204
3262
  let dir = TempDir::new().unwrap();
3205
- let rows = run_on_file(
3263
+ let error = run_on_file(
3206
3264
  "definitely-not-a-real-binary-xyzzy",
3207
3265
  "/outside/f.txt",
3208
3266
  dir.path(),
3209
3267
  dir.path(),
3210
3268
  command::DEFAULT_COMMAND_TIMEOUT,
3269
+ )
3270
+ .expect_err("an unspawnable command is a failure");
3271
+ assert!(
3272
+ error.to_string().contains("on-file command failed"),
3273
+ "the error names the stage: {error}"
3211
3274
  );
3212
- assert!(rows.is_empty());
3213
3275
  }
3214
3276
 
3215
3277
  #[test]
3216
- fn run_on_file_returns_no_rows_on_non_json_output() {
3278
+ fn run_on_file_errors_on_non_json_output() {
3217
3279
  let dir = TempDir::new().unwrap();
3218
- let rows = run_on_file(
3280
+ let error = run_on_file(
3219
3281
  "echo not-json",
3220
3282
  "/outside/f.txt",
3221
3283
  dir.path(),
3222
3284
  dir.path(),
3223
3285
  command::DEFAULT_COMMAND_TIMEOUT,
3286
+ )
3287
+ .expect_err("a non-JSON payload is a failure");
3288
+ assert!(
3289
+ error.to_string().contains("not a JSON array of rows"),
3290
+ "the error names the stage: {error}"
3224
3291
  );
3225
- assert!(rows.is_empty());
3226
3292
  }
3227
3293
 
3228
3294
  #[tokio::test]
@@ -1,7 +0,0 @@
1
- {
2
- "command": "uv run python -m pytest tests/e2e/ -x -q",
3
- "ran_at": 1785328821,
4
- "exit_code": 0,
5
- "commit": "a133d0f16d5e9d1b3892b3854f52eac535a9012f",
6
- "branch": "claude/tackle-698-bz6ere"
7
- }
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