dirsql 0.4.2__tar.gz → 0.4.4__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 (213) hide show
  1. {dirsql-0.4.2 → dirsql-0.4.4}/Cargo.lock +1 -1
  2. {dirsql-0.4.2 → dirsql-0.4.4}/Cargo.toml +12 -0
  3. {dirsql-0.4.2 → dirsql-0.4.4}/PKG-INFO +1 -1
  4. {dirsql-0.4.2/packages/rust → dirsql-0.4.4}/docs/reference/hooks.md +1 -1
  5. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/Cargo.toml +1 -1
  6. dirsql-0.4.4/packages/python/changelog.d/2026-08-02-release-profile.md +5 -0
  7. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/hooks.md +1 -1
  8. dirsql-0.4.4/packages/python/e2e-attestations/claude-714-unify-file-failures.json +7 -0
  9. dirsql-0.4.4/packages/rust/changelog.d/2026-08-02-per-file-scan-failures.md +35 -0
  10. dirsql-0.4.4/packages/rust/changelog.d/2026-08-02-release-profile.md +7 -0
  11. {dirsql-0.4.2 → dirsql-0.4.4/packages/rust}/docs/reference/hooks.md +1 -1
  12. dirsql-0.4.4/packages/rust/migrations.d/2026-08-02-per-file-scan-failures.md +100 -0
  13. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/bin/dirsql.rs +42 -1
  14. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/lib.rs +86 -75
  15. {dirsql-0.4.2 → dirsql-0.4.4}/README.md +0 -0
  16. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/__init__.py +0 -0
  17. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/_async.py +0 -0
  18. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/_dirsql.pyi +0 -0
  19. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/__init__.py +0 -0
  20. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/binary_path.py +0 -0
  21. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/discover_plugins/__init__.py +0 -0
  22. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  23. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  24. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  25. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  26. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  27. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/is_windows.py +0 -0
  28. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/main.py +0 -0
  29. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/cli/resolve_config_extensions.py +0 -0
  30. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/py.typed +0 -0
  31. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/resolve_config_extensions.py +0 -0
  32. {dirsql-0.4.2 → dirsql-0.4.4}/dirsql/resolve_extension.py +0 -0
  33. {dirsql-0.4.2 → dirsql-0.4.4}/docs/.claude/CLAUDE.md +0 -0
  34. {dirsql-0.4.2 → dirsql-0.4.4}/docs/.vitepress/config.ts +0 -0
  35. {dirsql-0.4.2 → dirsql-0.4.4}/docs/.vitepress/theme/index.ts +0 -0
  36. {dirsql-0.4.2 → dirsql-0.4.4}/docs/.vitepress/theme/lang.ts +0 -0
  37. {dirsql-0.4.2 → dirsql-0.4.4}/docs/AGENTS.md +0 -0
  38. {dirsql-0.4.2 → dirsql-0.4.4}/docs/explanation.md +0 -0
  39. {dirsql-0.4.2 → dirsql-0.4.4}/docs/getting-started.md +0 -0
  40. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/columns-from-paths.md +0 -0
  41. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/define-tables.md +0 -0
  42. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/embed.md +0 -0
  43. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/extract-from-contents.md +0 -0
  44. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/load-extension.md +0 -0
  45. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/parse-files-into-columns.md +0 -0
  46. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/persist.md +0 -0
  47. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/query-without-config.md +0 -0
  48. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/react-to-changes.md +0 -0
  49. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/search-by-meaning.md +0 -0
  50. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/skip-files.md +0 -0
  51. {dirsql-0.4.2 → dirsql-0.4.4}/docs/howto/write-a-plugin.md +0 -0
  52. {dirsql-0.4.2 → dirsql-0.4.4}/docs/index.md +0 -0
  53. {dirsql-0.4.2 → dirsql-0.4.4}/docs/migrations.md +0 -0
  54. {dirsql-0.4.2 → dirsql-0.4.4}/docs/package.json +0 -0
  55. {dirsql-0.4.2 → dirsql-0.4.4}/docs/pnpm-lock.yaml +0 -0
  56. {dirsql-0.4.2 → dirsql-0.4.4}/docs/pnpm-workspace.yaml +0 -0
  57. {dirsql-0.4.2 → dirsql-0.4.4}/docs/reference/cli.md +0 -0
  58. {dirsql-0.4.2 → dirsql-0.4.4}/docs/reference/columns.md +0 -0
  59. {dirsql-0.4.2 → dirsql-0.4.4}/docs/reference/config.md +0 -0
  60. {dirsql-0.4.2 → dirsql-0.4.4}/docs/reference/http-api.md +0 -0
  61. {dirsql-0.4.2 → dirsql-0.4.4}/docs/reference/path-tables.md +0 -0
  62. {dirsql-0.4.2 → dirsql-0.4.4}/docs/reference/sdk.md +0 -0
  63. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/CHANGELOG.md +0 -0
  64. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/MIGRATIONS.md +0 -0
  65. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/README.md +0 -0
  66. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  67. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  68. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  69. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  70. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  71. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  72. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  73. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  74. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  75. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  76. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/changelog.d/README.md +0 -0
  77. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/conftest.py +0 -0
  78. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/.claude/CLAUDE.md +0 -0
  79. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/.vitepress/config.ts +0 -0
  80. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  81. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  82. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/AGENTS.md +0 -0
  83. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/explanation.md +0 -0
  84. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/getting-started.md +0 -0
  85. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/columns-from-paths.md +0 -0
  86. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/define-tables.md +0 -0
  87. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/embed.md +0 -0
  88. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/extract-from-contents.md +0 -0
  89. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/load-extension.md +0 -0
  90. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  91. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/persist.md +0 -0
  92. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/query-without-config.md +0 -0
  93. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/react-to-changes.md +0 -0
  94. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/search-by-meaning.md +0 -0
  95. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/skip-files.md +0 -0
  96. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/howto/write-a-plugin.md +0 -0
  97. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/index.md +0 -0
  98. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/migrations.md +0 -0
  99. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/package.json +0 -0
  100. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/pnpm-lock.yaml +0 -0
  101. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/pnpm-workspace.yaml +0 -0
  102. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/cli.md +0 -0
  103. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/columns.md +0 -0
  104. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/config.md +0 -0
  105. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/http-api.md +0 -0
  106. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/path-tables.md +0 -0
  107. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/docs/reference/sdk.md +0 -0
  108. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/e2e-attestations/claude-710-accumulate-onfile.json +0 -0
  109. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  110. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  111. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  112. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  113. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  114. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  115. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  116. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  117. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/migrations.d/README.md +0 -0
  118. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/src/lib.rs +0 -0
  119. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/__init__.py +0 -0
  120. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/conftest.py +0 -0
  121. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/e2e/__init__.py +0 -0
  122. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  123. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  124. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/integration/__init__.py +0 -0
  125. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/integration/binding/__init__.py +0 -0
  126. {dirsql-0.4.2 → dirsql-0.4.4}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  127. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/CHANGELOG.md +0 -0
  128. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/Cargo.toml +0 -0
  129. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/MIGRATIONS.md +0 -0
  130. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/README.md +0 -0
  131. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/benches/db_bench.rs +0 -0
  132. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/benches/differ_bench.rs +0 -0
  133. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/benches/matcher_bench.rs +0 -0
  134. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/benches/scanner_bench.rs +0 -0
  135. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-batch-ingest-transaction.md +0 -0
  136. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-chained-pre-post-query-hooks.md +0 -0
  137. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  138. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-fan-out-file-table-matching.md +0 -0
  139. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-persist-wal.md +0 -0
  140. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  141. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-repeatable-builder-config.md +0 -0
  142. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-repeatable-cli-config.md +0 -0
  143. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  144. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-13-subcommand-local-config-flags.md +0 -0
  145. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-19-path-table-query-fallback.md +0 -0
  146. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-19-path-table-vtab.md +0 -0
  147. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  148. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-20-path-table-glob-semantics.md +0 -0
  149. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-20-schema-inference.md +0 -0
  150. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-21-on-file-flag.md +0 -0
  151. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  152. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-23-init-escalation-example.md +0 -0
  153. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-23-query-default-cli.md +0 -0
  154. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  155. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  156. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/2026-07-29-on-file-failures-accumulate.md +0 -0
  157. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/changelog.d/README.md +0 -0
  158. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/explanation.md +0 -0
  159. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/getting-started.md +0 -0
  160. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  161. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/define-tables.md +0 -0
  162. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/embed.md +0 -0
  163. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  164. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/load-extension.md +0 -0
  165. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  166. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/persist.md +0 -0
  167. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/query-without-config.md +0 -0
  168. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/react-to-changes.md +0 -0
  169. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  170. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/skip-files.md +0 -0
  171. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  172. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/index.md +0 -0
  173. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/migrations.md +0 -0
  174. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/reference/cli.md +0 -0
  175. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/reference/columns.md +0 -0
  176. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/reference/config.md +0 -0
  177. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/reference/http-api.md +0 -0
  178. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/reference/path-tables.md +0 -0
  179. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/docs/reference/sdk.md +0 -0
  180. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  181. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-13-fan-out-file-table-matching.md +0 -0
  182. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-13-persist-wal.md +0 -0
  183. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  184. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  185. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-13-subcommand-local-config-flags.md +0 -0
  186. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  187. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-20-path-table-glob-semantics.md +0 -0
  188. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  189. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-23-init-escalation-example.md +0 -0
  190. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-23-query-default-cli.md +0 -0
  191. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  192. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  193. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/migrations.d/README.md +0 -0
  194. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/cli/execute.rs +0 -0
  195. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/cli/init.rs +0 -0
  196. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/cli/mod.rs +0 -0
  197. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/cli/router.rs +0 -0
  198. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/cli/serialize.rs +0 -0
  199. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/cli/server.rs +0 -0
  200. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/command.rs +0 -0
  201. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/config.rs +0 -0
  202. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/db.rs +0 -0
  203. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/default_config.toml +0 -0
  204. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/differ.rs +0 -0
  205. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/infer.rs +0 -0
  206. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/matcher.rs +0 -0
  207. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/parsed_vtab.rs +0 -0
  208. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/path_table.rs +0 -0
  209. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/persist.rs +0 -0
  210. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/scanner.rs +0 -0
  211. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/vtab.rs +0 -0
  212. {dirsql-0.4.2 → dirsql-0.4.4}/packages/rust/src/watcher.rs +0 -0
  213. {dirsql-0.4.2 → dirsql-0.4.4}/pyproject.toml +0 -0
@@ -500,7 +500,7 @@ dependencies = [
500
500
 
501
501
  [[package]]
502
502
  name = "dirsql-py-ext"
503
- version = "0.4.2"
503
+ version = "0.4.4"
504
504
  dependencies = [
505
505
  "dirsql",
506
506
  "pyo3",
@@ -22,6 +22,18 @@ thiserror = "2"
22
22
  tokio = { version = "1", features = ["rt", "sync"] }
23
23
  futures-channel = "0.3"
24
24
 
25
+ # Cargo's default release profile ships every binary unstripped, without
26
+ # cross-crate optimization. The three published artifacts (CLI bin, pyo3
27
+ # extension, napi addon) are each a statically linked copy of the whole core,
28
+ # so the defaults cost ~31% of every download (#717).
29
+ #
30
+ # `panic` stays at the default `unwind`: pyo3 and napi turn Rust panics into
31
+ # Python/JS exceptions, which requires unwinding.
32
+ [profile.release]
33
+ strip = "symbols"
34
+ lto = "fat"
35
+ codegen-units = 1
36
+
25
37
  [workspace.lints.rust]
26
38
  unsafe_code = "forbid"
27
39
  rust_2018_idioms = { level = "warn", priority = -1 }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.2
3
+ Version: 0.4.4
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.2"
7
+ version = "0.4.4"
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
@@ -0,0 +1,5 @@
1
+ **Changed** — the wheel's compiled artifacts are now stripped and built with
2
+ fat LTO, shrinking the `_dirsql.abi3.so` extension ~28% (6.94 MB → 4.99 MB) and
3
+ the bundled CLI binary ~33% (9.17 MB → 6.14 MB).
4
+
5
+ Build-only: no API or behavior change.
@@ -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": 1785673957,
4
+ "exit_code": 0,
5
+ "commit": "484c89828d5a512f7fd51dc8e5995d498e857c0b",
6
+ "branch": "claude/714-unify-file-failures"
7
+ }
@@ -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.
@@ -0,0 +1,7 @@
1
+ **Changed** — release builds now strip symbols, run fat LTO, and use a single
2
+ codegen unit, so the published `dirsql` binary is ~33% smaller (linux-x64:
3
+ 9.17 MB → 6.14 MB; 3.49 MB → 2.80 MB gzipped).
4
+
5
+ Build-only: no API, CLI or behavior change. `panic` stays at the default
6
+ `unwind`, which the pyo3 and napi bindings need to turn Rust panics into
7
+ Python/JS exceptions. Release build time rises ~2m10s → ~2m38s.
@@ -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.
@@ -129,23 +129,6 @@ pub enum DirSqlError {
129
129
  #[error("duplicate table name: {0}")]
130
130
  DuplicateTable(String),
131
131
 
132
- #[error("on-file error for {path}: {message}")]
133
- OnFile { path: String, message: String },
134
-
135
- /// Several files' on-file hooks failed during one scan. A scan attempts
136
- /// every matched file, so more than one can fail; reporting only the first
137
- /// would make the rest invisible until it was fixed and the scan re-run.
138
- #[error(
139
- "{} files failed their on-file hook:\n{}",
140
- .failures.len(),
141
- .failures
142
- .iter()
143
- .map(|OnFileFailure { path, message }| format!(" {path}: {message}"))
144
- .collect::<Vec<_>>()
145
- .join("\n")
146
- )]
147
- OnFileMany { failures: Vec<OnFileFailure> },
148
-
149
132
  #[error("config error: {message}")]
150
133
  Config {
151
134
  message: String,
@@ -273,6 +256,8 @@ struct DirSqlInner {
273
256
  matcher: TableMatcher,
274
257
  on_file_map: HashMap<String, Arc<OnFileFn>>,
275
258
  strict_map: HashMap<String, bool>,
259
+ /// Files the initial scan could not index. Empty for a clean scan.
260
+ scan_failures: Vec<OnFileFailure>,
276
261
  watcher: Mutex<Option<Watcher>>,
277
262
  /// Locks out [`DirSQL::watch`] once [`DirSQL::poll_events`] has run:
278
263
  /// both would drain the same underlying watcher.
@@ -735,6 +720,18 @@ impl DirSQL {
735
720
  })
736
721
  }
737
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
+
738
735
  /// Split-phase construction — part 2. Consumes the intermediate state from
739
736
  /// [`prepare_resolved`](Self::prepare_resolved): creates the SQLite
740
737
  /// database (or wires up the persistent on-disk one), runs each table's
@@ -873,9 +870,29 @@ impl DirSQL {
873
870
  db.delete_rows_by_file(&table_name, &rel_path)
874
871
  .map_err(map_db_error)?;
875
872
  }
876
- for (row_index, raw_row) in raw_rows.iter().enumerate() {
877
- let row = db.normalize_row(&table_name, raw_row, strict)?;
878
- 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)
879
896
  .map_err(map_db_error)?;
880
897
  }
881
898
 
@@ -904,26 +921,12 @@ impl DirSQL {
904
921
  write_meta(db.conn(), meta).map_err(DirSqlError::sqlite)?;
905
922
  }
906
923
 
907
- // Returning before the commit drops `_tx`, so a scan with failures
908
- // still leaves the cache exactly as it was -- the pre-existing
909
- // behavior. The single-failure case keeps its original error verbatim;
910
- // only the multi-failure case reports more than it used to. Whether a
911
- // partial index should commit instead is dirsql#697.
912
- match on_file_failures.len() {
913
- 0 => {}
914
- 1 => {
915
- let only = on_file_failures.remove(0);
916
- return Err(DirSqlError::OnFile {
917
- path: only.path,
918
- message: only.message,
919
- });
920
- }
921
- _ => {
922
- return Err(DirSqlError::OnFileMany {
923
- failures: on_file_failures,
924
- });
925
- }
926
- }
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.
927
930
 
928
931
  // Commit the ingest transaction.
929
932
  _tx.commit().map_err(DirSqlError::sqlite)?;
@@ -940,6 +943,7 @@ impl DirSQL {
940
943
  matcher,
941
944
  on_file_map,
942
945
  strict_map,
946
+ scan_failures: on_file_failures,
943
947
  watcher: Mutex::new(None),
944
948
  poll_used: AtomicBool::new(false),
945
949
  watch_thread_started: AtomicBool::new(false),
@@ -1548,10 +1552,9 @@ fn build_tables_from_config(
1548
1552
  let command = table_cfg.on_file.clone();
1549
1553
  let config_dir = config_dir.to_path_buf();
1550
1554
  let root = root.to_path_buf();
1551
- // `Table::new` (infallible): `run_on_file` isolates its own errors to an
1552
- // empty row set so one bad file never aborts the scan (the scan aborts
1553
- // on an on_file `Err`).
1554
- 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(
1555
1558
  table_cfg.ddl.clone(),
1556
1559
  table_cfg.glob.clone(),
1557
1560
  move |abs_path: &str| run_on_file(&command, abs_path, &config_dir, &root, timeout),
@@ -1575,38 +1578,33 @@ fn build_tables_from_config(
1575
1578
  /// config lives outside the index still resolves it. A template that omits a
1576
1579
  /// placeholder simply never receives its value — nothing is appended.
1577
1580
  ///
1578
- /// Per-file isolation: any failure — a spawn/exit/timeout error from
1579
- /// [`command::run_command`], or output that is not a JSON array of objects —
1580
- /// is logged to stderr and yields no rows (`vec![]`). Returning `Err` here
1581
- /// would abort the whole scan, so it never does. `timeout` bounds the run
1582
- /// 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.
1583
1587
  fn run_on_file(
1584
1588
  command: &str,
1585
1589
  abs_path: &str,
1586
1590
  config_dir: &Path,
1587
1591
  root: &Path,
1588
1592
  timeout: Duration,
1589
- ) -> Vec<Row> {
1593
+ ) -> std::result::Result<Vec<Row>, BoxError> {
1590
1594
  let placeholders = [
1591
1595
  Placeholder::new("path", abs_path),
1592
1596
  Placeholder::new("root", root.to_string_lossy().into_owned()),
1593
1597
  ];
1594
1598
 
1595
- match command::run_command(command, &placeholders, config_dir, timeout, None) {
1596
- Ok(output) => match parse_command_rows(&output.payload) {
1597
- Ok(rows) => rows,
1598
- Err(message) => {
1599
- eprintln!(
1600
- "dirsql: skipping `{abs_path}`: on-file output was not a JSON array of rows: {message}"
1601
- );
1602
- Vec::new()
1603
- }
1604
- },
1605
- Err(error) => {
1606
- eprintln!("dirsql: skipping `{abs_path}`: on-file command failed: {error}");
1607
- Vec::new()
1608
- }
1609
- }
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
+ })
1610
1608
  }
1611
1609
 
1612
1610
  /// Parse an `on-file` command's stdout payload — a JSON array of row objects —
@@ -3206,7 +3204,8 @@ mod internal_tests {
3206
3204
  dir.path(),
3207
3205
  dir.path(),
3208
3206
  command::DEFAULT_COMMAND_TIMEOUT,
3209
- );
3207
+ )
3208
+ .expect("a well-formed payload parses");
3210
3209
  assert_eq!(rows.len(), 1);
3211
3210
  assert_eq!(rows[0]["n"], Value::Integer(1));
3212
3211
  }
@@ -3225,7 +3224,8 @@ mod internal_tests {
3225
3224
  dir.path(),
3226
3225
  dir.path(),
3227
3226
  command::DEFAULT_COMMAND_TIMEOUT,
3228
- );
3227
+ )
3228
+ .expect("a well-formed payload parses");
3229
3229
  assert_eq!(rows.len(), 1);
3230
3230
  assert_eq!(rows[0]["q"], Value::Text("{abspath}".into()));
3231
3231
  }
@@ -3245,7 +3245,8 @@ mod internal_tests {
3245
3245
  dir.path(),
3246
3246
  dir.path(),
3247
3247
  command::DEFAULT_COMMAND_TIMEOUT,
3248
- );
3248
+ )
3249
+ .expect("a well-formed payload parses");
3249
3250
  assert_eq!(rows.len(), 1);
3250
3251
  assert_eq!(
3251
3252
  rows[0]["p"],
@@ -3253,31 +3254,41 @@ mod internal_tests {
3253
3254
  );
3254
3255
  }
3255
3256
 
3256
- /// 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.
3257
3260
  #[test]
3258
- fn run_on_file_returns_no_rows_on_spawn_failure() {
3261
+ fn run_on_file_errors_on_spawn_failure() {
3259
3262
  let dir = TempDir::new().unwrap();
3260
- let rows = run_on_file(
3263
+ let error = run_on_file(
3261
3264
  "definitely-not-a-real-binary-xyzzy",
3262
3265
  "/outside/f.txt",
3263
3266
  dir.path(),
3264
3267
  dir.path(),
3265
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}"
3266
3274
  );
3267
- assert!(rows.is_empty());
3268
3275
  }
3269
3276
 
3270
3277
  #[test]
3271
- fn run_on_file_returns_no_rows_on_non_json_output() {
3278
+ fn run_on_file_errors_on_non_json_output() {
3272
3279
  let dir = TempDir::new().unwrap();
3273
- let rows = run_on_file(
3280
+ let error = run_on_file(
3274
3281
  "echo not-json",
3275
3282
  "/outside/f.txt",
3276
3283
  dir.path(),
3277
3284
  dir.path(),
3278
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}"
3279
3291
  );
3280
- assert!(rows.is_empty());
3281
3292
  }
3282
3293
 
3283
3294
  #[tokio::test]
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