dirsql 0.4.59__tar.gz → 0.4.61__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 (259) hide show
  1. {dirsql-0.4.59 → dirsql-0.4.61}/Cargo.lock +2 -3
  2. {dirsql-0.4.59 → dirsql-0.4.61}/PKG-INFO +1 -1
  3. {dirsql-0.4.59/packages/rust → dirsql-0.4.61}/docs/reference/config.md +37 -0
  4. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/Cargo.toml +1 -1
  5. {dirsql-0.4.59 → dirsql-0.4.61/packages/python}/docs/reference/config.md +37 -0
  6. dirsql-0.4.61/packages/python/e2e-attestations/claude-1201-single-walk.json +7 -0
  7. dirsql-0.4.61/packages/python/e2e-attestations/claude-1204-embed-batch.json +7 -0
  8. dirsql-0.4.61/packages/python/e2e-attestations/claude-1207-win-argv-quote.json +7 -0
  9. dirsql-0.4.61/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json +7 -0
  10. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/Cargo.toml +2 -3
  11. {dirsql-0.4.59/packages/python → dirsql-0.4.61/packages/rust}/docs/reference/config.md +37 -0
  12. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/command.rs +254 -2
  13. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/config.rs +58 -0
  14. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/db.rs +242 -10
  15. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/extension_resolution.rs +5 -1
  16. dirsql-0.4.61/packages/rust/src/functions.rs +1887 -0
  17. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/lib.rs +46 -141
  18. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/parsed_vtab.rs +5 -5
  19. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/scanner.rs +223 -98
  20. dirsql-0.4.61/packages/rust/src/vtab.rs +708 -0
  21. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/vtab_scaffold.rs +198 -12
  22. dirsql-0.4.59/packages/rust/src/functions.rs +0 -1040
  23. dirsql-0.4.59/packages/rust/src/vtab.rs +0 -498
  24. {dirsql-0.4.59 → dirsql-0.4.61}/Cargo.toml +0 -0
  25. {dirsql-0.4.59 → dirsql-0.4.61}/README.md +0 -0
  26. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/__init__.py +0 -0
  27. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/_async.py +0 -0
  28. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/_dirsql.pyi +0 -0
  29. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/__init__.py +0 -0
  30. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/discover_plugins/__init__.py +0 -0
  31. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  32. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  33. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  34. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  35. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  36. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/main.py +0 -0
  37. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/cli/resolve_config_extensions.py +0 -0
  38. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/plan_entry_path.py +0 -0
  39. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/py.typed +0 -0
  40. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/read_config.py +0 -0
  41. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/resolve_configs_extension_specs.py +0 -0
  42. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/resolve_extension.py +0 -0
  43. {dirsql-0.4.59 → dirsql-0.4.61}/dirsql/resolve_package.py +0 -0
  44. {dirsql-0.4.59 → dirsql-0.4.61}/docs/.claude/CLAUDE.md +0 -0
  45. {dirsql-0.4.59 → dirsql-0.4.61}/docs/.vitepress/config.ts +0 -0
  46. {dirsql-0.4.59 → dirsql-0.4.61}/docs/.vitepress/theme/index.ts +0 -0
  47. {dirsql-0.4.59 → dirsql-0.4.61}/docs/.vitepress/theme/lang.ts +0 -0
  48. {dirsql-0.4.59 → dirsql-0.4.61}/docs/AGENTS.md +0 -0
  49. {dirsql-0.4.59 → dirsql-0.4.61}/docs/explanation.md +0 -0
  50. {dirsql-0.4.59 → dirsql-0.4.61}/docs/getting-started.md +0 -0
  51. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/columns-from-paths.md +0 -0
  52. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/define-tables.md +0 -0
  53. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/embed.md +0 -0
  54. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/extract-from-contents.md +0 -0
  55. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/load-extension.md +0 -0
  56. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/parse-files-into-columns.md +0 -0
  57. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/persist.md +0 -0
  58. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/query-json.md +0 -0
  59. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/query-without-config.md +0 -0
  60. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/react-to-changes.md +0 -0
  61. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/search-by-meaning.md +0 -0
  62. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/search-indexes.md +0 -0
  63. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/skip-files.md +0 -0
  64. {dirsql-0.4.59 → dirsql-0.4.61}/docs/howto/write-a-plugin.md +0 -0
  65. {dirsql-0.4.59 → dirsql-0.4.61}/docs/index.md +0 -0
  66. {dirsql-0.4.59 → dirsql-0.4.61}/docs/package.json +0 -0
  67. {dirsql-0.4.59 → dirsql-0.4.61}/docs/plugins.md +0 -0
  68. {dirsql-0.4.59 → dirsql-0.4.61}/docs/pnpm-lock.yaml +0 -0
  69. {dirsql-0.4.59 → dirsql-0.4.61}/docs/pnpm-workspace.yaml +0 -0
  70. {dirsql-0.4.59 → dirsql-0.4.61}/docs/reference/cli.md +0 -0
  71. {dirsql-0.4.59 → dirsql-0.4.61}/docs/reference/columns.md +0 -0
  72. {dirsql-0.4.59 → dirsql-0.4.61}/docs/reference/hooks.md +0 -0
  73. {dirsql-0.4.59 → dirsql-0.4.61}/docs/reference/http-api.md +0 -0
  74. {dirsql-0.4.59 → dirsql-0.4.61}/docs/reference/path-tables.md +0 -0
  75. {dirsql-0.4.59 → dirsql-0.4.61}/docs/reference/sdk.md +0 -0
  76. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/CHANGELOG.md +0 -0
  77. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/MIGRATIONS.md +0 -0
  78. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/README.md +0 -0
  79. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  80. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  81. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  82. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  83. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  84. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  85. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  86. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  87. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  88. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  89. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  90. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  91. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  92. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  93. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  94. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  95. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  96. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  97. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  98. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  99. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  100. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  101. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  102. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  103. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
  104. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-18-export-scan-failure.md +0 -0
  105. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +0 -0
  106. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
  107. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  108. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/2026-09-24-query-rows-in-projection-order.md +0 -0
  109. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/changelog.d/README.md +0 -0
  110. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/conftest.py +0 -0
  111. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/.claude/CLAUDE.md +0 -0
  112. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/.vitepress/config.ts +0 -0
  113. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  114. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  115. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/AGENTS.md +0 -0
  116. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/explanation.md +0 -0
  117. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/getting-started.md +0 -0
  118. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/columns-from-paths.md +0 -0
  119. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/define-tables.md +0 -0
  120. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/embed.md +0 -0
  121. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/extract-from-contents.md +0 -0
  122. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/load-extension.md +0 -0
  123. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  124. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/persist.md +0 -0
  125. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/query-json.md +0 -0
  126. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/query-without-config.md +0 -0
  127. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/react-to-changes.md +0 -0
  128. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/search-by-meaning.md +0 -0
  129. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/search-indexes.md +0 -0
  130. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/skip-files.md +0 -0
  131. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/howto/write-a-plugin.md +0 -0
  132. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/index.md +0 -0
  133. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/package.json +0 -0
  134. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/plugins.md +0 -0
  135. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/pnpm-lock.yaml +0 -0
  136. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/pnpm-workspace.yaml +0 -0
  137. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/reference/cli.md +0 -0
  138. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/reference/columns.md +0 -0
  139. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/reference/hooks.md +0 -0
  140. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/reference/http-api.md +0 -0
  141. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/reference/path-tables.md +0 -0
  142. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/docs/reference/sdk.md +0 -0
  143. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  144. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  145. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  146. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  147. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  148. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
  149. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
  150. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  151. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
  152. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
  153. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
  154. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
  155. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1130-codec-crates.json +0 -0
  156. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +0 -0
  157. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1139-sse-flatten.json +0 -0
  158. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1140-scan-failure-export.json +0 -0
  159. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1148-clippy-all-targets.json +0 -0
  160. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1164-sigint-fatal.json +0 -0
  161. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1170-projection-order.json +0 -0
  162. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1172-on-file-path.json +0 -0
  163. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1173-py-projection-order.json +0 -0
  164. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1184-win-rel-paths.json +0 -0
  165. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1185-win-abs-table-names.json +0 -0
  166. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-1186-win-test-harness.json +0 -0
  167. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  168. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  169. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  170. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  171. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  172. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  173. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  174. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  175. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  176. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  177. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  178. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  179. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  180. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  181. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  182. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  183. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  184. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  185. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  186. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  187. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  188. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  189. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  190. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  191. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  192. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  193. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/2026-09-24-query-rows-in-projection-order.md +0 -0
  194. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/migrations.d/README.md +0 -0
  195. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/src/lib.rs +0 -0
  196. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/testing-conventions.toml +0 -0
  197. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/__init__.py +0 -0
  198. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/conftest.py +0 -0
  199. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/e2e/__init__.py +0 -0
  200. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  201. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  202. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/e2e/speed_of_light.py +0 -0
  203. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/integration/__init__.py +0 -0
  204. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/integration/binding/__init__.py +0 -0
  205. {dirsql-0.4.59 → dirsql-0.4.61}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  206. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/CHANGELOG.md +0 -0
  207. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/MIGRATIONS.md +0 -0
  208. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/README.md +0 -0
  209. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/benches/db_bench.rs +0 -0
  210. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/benches/differ_bench.rs +0 -0
  211. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/benches/matcher_bench.rs +0 -0
  212. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/benches/scanner_bench.rs +0 -0
  213. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/explanation.md +0 -0
  214. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/getting-started.md +0 -0
  215. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  216. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/define-tables.md +0 -0
  217. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/embed.md +0 -0
  218. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  219. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/load-extension.md +0 -0
  220. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  221. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/persist.md +0 -0
  222. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/query-json.md +0 -0
  223. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/query-without-config.md +0 -0
  224. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/react-to-changes.md +0 -0
  225. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  226. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/search-indexes.md +0 -0
  227. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/skip-files.md +0 -0
  228. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  229. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/index.md +0 -0
  230. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/plugins.md +0 -0
  231. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/reference/cli.md +0 -0
  232. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/reference/columns.md +0 -0
  233. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/reference/hooks.md +0 -0
  234. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/reference/http-api.md +0 -0
  235. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/reference/path-tables.md +0 -0
  236. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/docs/reference/sdk.md +0 -0
  237. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/bin/dirsql.rs +0 -0
  238. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/execute.rs +0 -0
  239. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/init.rs +0 -0
  240. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/mod.rs +0 -0
  241. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/repl.rs +0 -0
  242. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/router.rs +0 -0
  243. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/run.rs +0 -0
  244. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/serialize.rs +0 -0
  245. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/server.rs +0 -0
  246. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/cli/table.rs +0 -0
  247. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/default_config.toml +0 -0
  248. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/differ.rs +0 -0
  249. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/infer.rs +0 -0
  250. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/launcher.rs +0 -0
  251. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/matcher.rs +0 -0
  252. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/parsed_cache.rs +0 -0
  253. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/path_table.rs +0 -0
  254. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/persist.rs +0 -0
  255. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/progress.rs +0 -0
  256. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/row_event_flat.rs +0 -0
  257. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/sql_literal.rs +0 -0
  258. {dirsql-0.4.59 → dirsql-0.4.61}/packages/rust/src/watcher.rs +0 -0
  259. {dirsql-0.4.59 → dirsql-0.4.61}/pyproject.toml +0 -0
@@ -552,7 +552,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
552
552
 
553
553
  [[package]]
554
554
  name = "dirsql"
555
- version = "0.4.59"
555
+ version = "0.4.61"
556
556
  dependencies = [
557
557
  "assert_cmd",
558
558
  "axum",
@@ -585,7 +585,6 @@ dependencies = [
585
585
  "tokio-stream",
586
586
  "toml",
587
587
  "typed-path",
588
- "walkdir",
589
588
  ]
590
589
 
591
590
  [[package]]
@@ -601,7 +600,7 @@ dependencies = [
601
600
 
602
601
  [[package]]
603
602
  name = "dirsql-py-ext"
604
- version = "0.4.59"
603
+ version = "0.4.61"
605
604
  dependencies = [
606
605
  "dirsql",
607
606
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.59
3
+ Version: 0.4.61
4
4
  Requires-Dist: bin-shim>=0.1
5
5
  Summary: Ephemeral SQL index over a local directory
6
6
  Keywords: sql,filesystem,directory,sqlite,index
@@ -111,6 +111,7 @@ its `embed()` function exactly this way.
111
111
  | `command` | yes (non-empty) | The worker command. Argv-split with the same no-shell quoting rules as [command hooks](./hooks.md#argv-not-a-shell); runs in the config file's directory. |
112
112
  | `deterministic` | no (default `false`) | When `true`, the function is registered with `SQLITE_DETERMINISTIC`, letting SQLite cache and reuse results for identical arguments within a query. Only set it when the worker really is a pure function of its arguments. |
113
113
  | `timeout` | no | Per-**call** time bound: a positive integer is whole seconds (`timeout = 600`), a string is an integer suffixed `s` or `ms` (`"600s"`, `"250ms"`). When absent, the function mechanism's own 30-second default applies. |
114
+ | `batch` | no | A positive integer: the most calls one **batched request** carries. When set, a statement's values are gathered and sent to the worker in bulk (see [Batching](#batching)); the worker must then speak the batched protocol shape as well as the single-call one. When absent, the worker only ever sees single calls. |
114
115
 
115
116
  ```toml
116
117
  [[dirsql.function]]
@@ -173,6 +174,42 @@ request line in, one response line out, per call:
173
174
  - **stderr passes through** to `dirsql`'s stderr, so a worker's progress
174
175
  bars and download logs reach the terminal.
175
176
 
177
+ ### Batching
178
+
179
+ One round trip per row is the wrong shape for a worker whose cost is
180
+ per-request rather than per-value — an embedding model encodes a list of
181
+ texts in little more time than one. A function declaring `batch = N` is sent
182
+ values in bulk instead:
183
+
184
+ - **Request:** `{"calls": [[<arg>, ...], ...]}` — up to `N` calls, each
185
+ encoded exactly as a single call's argument list.
186
+ - **Response:** `{"results": [<response>, ...]}` — one single-call response
187
+ (`{"ok": ...}` with its optional `"meta"`, or `{"err": ...}`) per call, in
188
+ order. A per-call `{"err": ...}` fails the query the way it would on a
189
+ single call. A top-level `{"err": "message"}` instead fails every call in
190
+ the request.
191
+ - The round trip is bounded by the per-call `timeout` **times the number of
192
+ calls** in the request.
193
+
194
+ A batching worker must still answer `{"call": [...]}`: `dirsql` gathers a
195
+ statement's values by running it twice. The first run makes one ordinary
196
+ round trip for the function's first value, binds that reply in place of every
197
+ later distinct argument tuple, and queues those tuples into batched requests
198
+ of at most `N`; the second run is served from the replies. A statement that
199
+ never calls a batched function runs once, exactly as before. Values are
200
+ deduplicated along the way, so the [progress line](./cli.md#progress-reporting)
201
+ counts distinct calls, whichever request shape carried them.
202
+
203
+ ```toml
204
+ [[dirsql.function]]
205
+ name = "embed"
206
+ args = [1, 2]
207
+ command = "dirsql-plugin-embeddings worker"
208
+ deterministic = true
209
+ timeout = "600s"
210
+ batch = 256
211
+ ```
212
+
176
213
  ## `[[table]]`
177
214
 
178
215
  Each entry maps a glob pattern to a SQL table. A table's columns are exactly
@@ -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.59"
7
+ version = "0.4.61"
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
@@ -111,6 +111,7 @@ its `embed()` function exactly this way.
111
111
  | `command` | yes (non-empty) | The worker command. Argv-split with the same no-shell quoting rules as [command hooks](./hooks.md#argv-not-a-shell); runs in the config file's directory. |
112
112
  | `deterministic` | no (default `false`) | When `true`, the function is registered with `SQLITE_DETERMINISTIC`, letting SQLite cache and reuse results for identical arguments within a query. Only set it when the worker really is a pure function of its arguments. |
113
113
  | `timeout` | no | Per-**call** time bound: a positive integer is whole seconds (`timeout = 600`), a string is an integer suffixed `s` or `ms` (`"600s"`, `"250ms"`). When absent, the function mechanism's own 30-second default applies. |
114
+ | `batch` | no | A positive integer: the most calls one **batched request** carries. When set, a statement's values are gathered and sent to the worker in bulk (see [Batching](#batching)); the worker must then speak the batched protocol shape as well as the single-call one. When absent, the worker only ever sees single calls. |
114
115
 
115
116
  ```toml
116
117
  [[dirsql.function]]
@@ -173,6 +174,42 @@ request line in, one response line out, per call:
173
174
  - **stderr passes through** to `dirsql`'s stderr, so a worker's progress
174
175
  bars and download logs reach the terminal.
175
176
 
177
+ ### Batching
178
+
179
+ One round trip per row is the wrong shape for a worker whose cost is
180
+ per-request rather than per-value — an embedding model encodes a list of
181
+ texts in little more time than one. A function declaring `batch = N` is sent
182
+ values in bulk instead:
183
+
184
+ - **Request:** `{"calls": [[<arg>, ...], ...]}` — up to `N` calls, each
185
+ encoded exactly as a single call's argument list.
186
+ - **Response:** `{"results": [<response>, ...]}` — one single-call response
187
+ (`{"ok": ...}` with its optional `"meta"`, or `{"err": ...}`) per call, in
188
+ order. A per-call `{"err": ...}` fails the query the way it would on a
189
+ single call. A top-level `{"err": "message"}` instead fails every call in
190
+ the request.
191
+ - The round trip is bounded by the per-call `timeout` **times the number of
192
+ calls** in the request.
193
+
194
+ A batching worker must still answer `{"call": [...]}`: `dirsql` gathers a
195
+ statement's values by running it twice. The first run makes one ordinary
196
+ round trip for the function's first value, binds that reply in place of every
197
+ later distinct argument tuple, and queues those tuples into batched requests
198
+ of at most `N`; the second run is served from the replies. A statement that
199
+ never calls a batched function runs once, exactly as before. Values are
200
+ deduplicated along the way, so the [progress line](./cli.md#progress-reporting)
201
+ counts distinct calls, whichever request shape carried them.
202
+
203
+ ```toml
204
+ [[dirsql.function]]
205
+ name = "embed"
206
+ args = [1, 2]
207
+ command = "dirsql-plugin-embeddings worker"
208
+ deterministic = true
209
+ timeout = "600s"
210
+ batch = 256
211
+ ```
212
+
176
213
  ## `[[table]]`
177
214
 
178
215
  Each entry maps a glob pattern to a SQL table. A table's columns are exactly
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790945578,
4
+ "exit_code": 0,
5
+ "commit": "c74aacb34e2aa97f26338176180e7e063f83465b",
6
+ "branch": "claude/1201-single-walk"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790942055,
4
+ "exit_code": 0,
5
+ "commit": "b9f0529e785dd54fa49496fb3fa48bbcdb14764d",
6
+ "branch": "claude/1204-embed-batch"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790939617,
4
+ "exit_code": 0,
5
+ "commit": "009c015ffa36453da6d92ddec17b5b655b51372f",
6
+ "branch": "claude/1207-win-argv-quote"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790939958,
4
+ "exit_code": 0,
5
+ "commit": "64a48bb1eff07865a52fc25e1ad50722aced9e45",
6
+ "branch": "claude/1208-win-nonverbatim-paths"
7
+ }
@@ -6,7 +6,7 @@ description = "Ephemeral SQL index over a local directory"
6
6
  # version stays for the binding crates (publish = false). The literal
7
7
  # intentionally lags the published version: putitoutthere rewrites it to
8
8
  # the planned release version at build time and never commits it back.
9
- version = "0.4.59"
9
+ version = "0.4.61"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -69,7 +69,7 @@ globset = "0.4"
69
69
  # library consumer never pulls it.
70
70
  hex = { version = "0.4", optional = true }
71
71
  # Hierarchical `.gitignore` matching for path-table scans (the fd/ripgrep
72
- # semantics), without adopting its walker: the scan keeps `walkdir`.
72
+ # semantics), without adopting its walker: the scan has its own.
73
73
  ignore = "0.4"
74
74
  indicatif = "0.18.6"
75
75
  notify = "7"
@@ -87,7 +87,6 @@ tokio.workspace = true
87
87
  tokio-stream = { version = "0.1", features = ["sync"], optional = true }
88
88
  toml = "0.8"
89
89
  typed-path = "0.12"
90
- walkdir = "2"
91
90
 
92
91
  [lints.rust]
93
92
  # `deny`, not `forbid`: rusqlite's API imposes five unavoidable `unsafe`
@@ -111,6 +111,7 @@ its `embed()` function exactly this way.
111
111
  | `command` | yes (non-empty) | The worker command. Argv-split with the same no-shell quoting rules as [command hooks](./hooks.md#argv-not-a-shell); runs in the config file's directory. |
112
112
  | `deterministic` | no (default `false`) | When `true`, the function is registered with `SQLITE_DETERMINISTIC`, letting SQLite cache and reuse results for identical arguments within a query. Only set it when the worker really is a pure function of its arguments. |
113
113
  | `timeout` | no | Per-**call** time bound: a positive integer is whole seconds (`timeout = 600`), a string is an integer suffixed `s` or `ms` (`"600s"`, `"250ms"`). When absent, the function mechanism's own 30-second default applies. |
114
+ | `batch` | no | A positive integer: the most calls one **batched request** carries. When set, a statement's values are gathered and sent to the worker in bulk (see [Batching](#batching)); the worker must then speak the batched protocol shape as well as the single-call one. When absent, the worker only ever sees single calls. |
114
115
 
115
116
  ```toml
116
117
  [[dirsql.function]]
@@ -173,6 +174,42 @@ request line in, one response line out, per call:
173
174
  - **stderr passes through** to `dirsql`'s stderr, so a worker's progress
174
175
  bars and download logs reach the terminal.
175
176
 
177
+ ### Batching
178
+
179
+ One round trip per row is the wrong shape for a worker whose cost is
180
+ per-request rather than per-value — an embedding model encodes a list of
181
+ texts in little more time than one. A function declaring `batch = N` is sent
182
+ values in bulk instead:
183
+
184
+ - **Request:** `{"calls": [[<arg>, ...], ...]}` — up to `N` calls, each
185
+ encoded exactly as a single call's argument list.
186
+ - **Response:** `{"results": [<response>, ...]}` — one single-call response
187
+ (`{"ok": ...}` with its optional `"meta"`, or `{"err": ...}`) per call, in
188
+ order. A per-call `{"err": ...}` fails the query the way it would on a
189
+ single call. A top-level `{"err": "message"}` instead fails every call in
190
+ the request.
191
+ - The round trip is bounded by the per-call `timeout` **times the number of
192
+ calls** in the request.
193
+
194
+ A batching worker must still answer `{"call": [...]}`: `dirsql` gathers a
195
+ statement's values by running it twice. The first run makes one ordinary
196
+ round trip for the function's first value, binds that reply in place of every
197
+ later distinct argument tuple, and queues those tuples into batched requests
198
+ of at most `N`; the second run is served from the replies. A statement that
199
+ never calls a batched function runs once, exactly as before. Values are
200
+ deduplicated along the way, so the [progress line](./cli.md#progress-reporting)
201
+ counts distinct calls, whichever request shape carried them.
202
+
203
+ ```toml
204
+ [[dirsql.function]]
205
+ name = "embed"
206
+ args = [1, 2]
207
+ command = "dirsql-plugin-embeddings worker"
208
+ deterministic = true
209
+ timeout = "600s"
210
+ batch = 256
211
+ ```
212
+
176
213
  ## `[[table]]`
177
214
 
178
215
  Each entry maps a glob pattern to a SQL table. A table's columns are exactly
@@ -49,6 +49,63 @@ impl Placeholder {
49
49
  value: value.into(),
50
50
  }
51
51
  }
52
+
53
+ /// A placeholder whose value is a filesystem path, handed to the child in
54
+ /// its plain (non-verbatim) form; see [`non_verbatim`].
55
+ pub fn path(name: impl Into<String>, path: &str) -> Self {
56
+ Self::new(name, non_verbatim(path))
57
+ }
58
+ }
59
+
60
+ /// The plain form of a Windows verbatim path: `\\?\C:\x` becomes `C:\x` and
61
+ /// `\\?\UNC\server\share\x` becomes `\\server\share\x`. Anything else, and any
62
+ /// verbatim path whose plain spelling would name a different file, is returned
63
+ /// unchanged.
64
+ pub(crate) fn non_verbatim(path: &str) -> String {
65
+ plain_form(path).unwrap_or_else(|| path.to_string())
66
+ }
67
+
68
+ fn plain_form(path: &str) -> Option<String> {
69
+ let rest = path.strip_prefix(r"\\?\")?;
70
+ let (plain, tail) = if let Some(unc) = rest.strip_prefix(r"UNC\") {
71
+ if unc.split('\\').count() < 2 {
72
+ return None;
73
+ }
74
+ (format!(r"\\{unc}"), unc)
75
+ } else {
76
+ let drive = rest.as_bytes();
77
+ let is_drive = drive.len() >= 3
78
+ && drive[0].is_ascii_alphabetic()
79
+ && drive[1] == b':'
80
+ && drive[2] == b'\\';
81
+ if !is_drive {
82
+ return None;
83
+ }
84
+ (rest.to_string(), &rest[3..])
85
+ };
86
+ // Win32 rejects or rewrites these, so the plain spelling would not name
87
+ // the same file; MAX_PATH counts UTF-16 units including the terminator.
88
+ let same_file = (tail.is_empty() || tail.split('\\').all(is_plain_component))
89
+ && plain.encode_utf16().count() < 260;
90
+ same_file.then_some(plain)
91
+ }
92
+
93
+ fn is_plain_component(name: &str) -> bool {
94
+ let invalid_char = |c: char| c < ' ' || r#"/<>:"|?*"#.contains(c);
95
+ let stem = name
96
+ .split_once('.')
97
+ .map_or(name, |(stem, _)| stem)
98
+ .trim_end_matches(' ');
99
+ let reserved = matches!(
100
+ stem.to_ascii_uppercase().as_bytes(),
101
+ b"CON"
102
+ | b"PRN"
103
+ | b"AUX"
104
+ | b"NUL"
105
+ | [b'C', b'O', b'M', b'1'..=b'9']
106
+ | [b'L', b'P', b'T', b'1'..=b'9']
107
+ );
108
+ !name.is_empty() && !name.ends_with(['.', ' ']) && !name.contains(invalid_char) && !reserved
52
109
  }
53
110
 
54
111
  /// A successful command run.
@@ -119,8 +176,8 @@ pub fn run_command(
119
176
  let argv = build_argv(command, placeholders)?;
120
177
  // `build_argv` guarantees a non-empty argv.
121
178
  let mut cmd = Command::new(&argv[0]);
122
- cmd.args(&argv[1..])
123
- .current_dir(cwd)
179
+ push_args(&mut cmd, &argv[1..]);
180
+ cmd.current_dir(cwd)
124
181
  .stdout(Stdio::piped())
125
182
  .stderr(Stdio::piped())
126
183
  .stdin(if stdin_payload.is_some() {
@@ -193,6 +250,20 @@ pub fn run_command(
193
250
  }
194
251
  }
195
252
 
253
+ // One fn with cfg blocks rather than two cfg'd fns: cargo-mutants mutates
254
+ // the uncompiled twin too, and no test on a Linux gate can kill that mutant.
255
+ fn push_args(cmd: &mut Command, args: &[String]) {
256
+ #[cfg(windows)]
257
+ {
258
+ use std::os::windows::process::CommandExt;
259
+ for arg in args {
260
+ cmd.raw_arg(quote_windows_arg(arg));
261
+ }
262
+ }
263
+ #[cfg(not(windows))]
264
+ cmd.args(args);
265
+ }
266
+
196
267
  fn spawn_error(program: &str, source: std::io::Error, cwd: &Path) -> CommandError {
197
268
  // A missing cwd fails the child's chdir with `NotFound` too.
198
269
  if !searched_path(program, source.kind()) || !cwd.is_dir() {
@@ -244,6 +315,35 @@ pub(crate) fn build_argv(
244
315
  Ok(argv)
245
316
  }
246
317
 
318
+ /// Quote `arg` for a Windows command line by the MS C-runtime rules, always
319
+ /// wrapped in double quotes: an MSYS/Cygwin child globs, brace-expands and
320
+ /// unescapes any unquoted word, while an MSVCRT child parses the quoted form
321
+ /// back to `arg` exactly.
322
+ #[cfg_attr(not(windows), allow(dead_code))]
323
+ pub(crate) fn quote_windows_arg(arg: &str) -> String {
324
+ let mut out = String::with_capacity(arg.len() + 2);
325
+ out.push('"');
326
+ let mut backslashes = 0;
327
+ for ch in arg.chars() {
328
+ match ch {
329
+ '\\' => backslashes += 1,
330
+ '"' => {
331
+ out.extend(std::iter::repeat_n('\\', backslashes * 2 + 1));
332
+ out.push('"');
333
+ backslashes = 0;
334
+ }
335
+ _ => {
336
+ out.extend(std::iter::repeat_n('\\', backslashes));
337
+ out.push(ch);
338
+ backslashes = 0;
339
+ }
340
+ }
341
+ }
342
+ out.extend(std::iter::repeat_n('\\', backslashes * 2));
343
+ out.push('"');
344
+ out
345
+ }
346
+
247
347
  /// Replace every `{name}` in `token` with its placeholder value in a single
248
348
  /// left-to-right pass. Injected values are never re-scanned, so an untrusted
249
349
  /// value containing `{…}` is inert. Unknown `{…}` sequences are left literal.
@@ -315,6 +415,117 @@ mod tests {
315
415
  build_argv(command, placeholders).expect("valid command")
316
416
  }
317
417
 
418
+ fn unchanged(path: &str) {
419
+ assert_eq!(non_verbatim(path), path);
420
+ }
421
+
422
+ #[test]
423
+ fn non_verbatim_strips_a_verbatim_drive_path() {
424
+ assert_eq!(
425
+ non_verbatim(r"\\?\C:\Users\runner\in_cwd.txt"),
426
+ r"C:\Users\runner\in_cwd.txt"
427
+ );
428
+ assert_eq!(non_verbatim(r"\\?\d:\x"), r"d:\x");
429
+ assert_eq!(non_verbatim(r"\\?\C:\my file.txt"), r"C:\my file.txt");
430
+ }
431
+
432
+ #[test]
433
+ fn non_verbatim_strips_a_verbatim_drive_root() {
434
+ assert_eq!(non_verbatim(r"\\?\C:\"), r"C:\");
435
+ }
436
+
437
+ #[test]
438
+ fn non_verbatim_rewrites_a_verbatim_unc_path() {
439
+ assert_eq!(
440
+ non_verbatim(r"\\?\UNC\server\share\dir\f.txt"),
441
+ r"\\server\share\dir\f.txt"
442
+ );
443
+ assert_eq!(non_verbatim(r"\\?\UNC\server\share"), r"\\server\share");
444
+ }
445
+
446
+ #[test]
447
+ fn non_verbatim_leaves_plain_paths_unchanged() {
448
+ unchanged(r"C:\Users\runner\f.txt");
449
+ unchanged(r"\\server\share\f.txt");
450
+ unchanged("/tmp/root/f.txt");
451
+ unchanged("relative/f.txt");
452
+ unchanged("");
453
+ }
454
+
455
+ #[test]
456
+ fn non_verbatim_leaves_verbatim_forms_without_a_drive_or_unc_unchanged() {
457
+ unchanged(r"\\?\GLOBALROOT\Device\HarddiskVolume1\f.txt");
458
+ unchanged(r"\\?\Volume{0b1c2d3e-0000-0000-0000-100000000000}\f.txt");
459
+ unchanged(r"\\?\C:");
460
+ unchanged(r"\\?\C:x");
461
+ unchanged(r"\\?\1:\x");
462
+ unchanged(r"\\?\");
463
+ }
464
+
465
+ #[test]
466
+ fn non_verbatim_leaves_an_incomplete_unc_unchanged() {
467
+ unchanged(r"\\?\UNC\");
468
+ unchanged(r"\\?\UNC\server");
469
+ unchanged(r"\\?\UNC\server\");
470
+ }
471
+
472
+ #[test]
473
+ fn non_verbatim_leaves_components_win32_would_reinterpret_unchanged() {
474
+ unchanged(r"\\?\C:\a\..\b");
475
+ unchanged(r"\\?\C:\a\.\b");
476
+ unchanged(r"\\?\C:\a\\b");
477
+ unchanged(r"\\?\C:\a\");
478
+ unchanged(r"\\?\C:\trailing.");
479
+ unchanged(r"\\?\C:\trailing ");
480
+ unchanged(r"\\?\C:\a/b");
481
+ unchanged(r"\\?\C:\a?b");
482
+ unchanged(r"\\?\C:\a*b");
483
+ unchanged(r"\\?\C:\a:b");
484
+ unchanged(r#"\\?\C:\a"b"#);
485
+ unchanged(r"\\?\C:\a<b");
486
+ unchanged(r"\\?\C:\a>b");
487
+ unchanged(r"\\?\C:\a|b");
488
+ unchanged("\\\\?\\C:\\a\u{1}b");
489
+ unchanged(r"\\?\UNC\server\share\..\x");
490
+ }
491
+
492
+ #[test]
493
+ fn non_verbatim_leaves_reserved_device_names_unchanged() {
494
+ for name in [
495
+ "CON", "con", "PRN", "AUX", "NUL", "nul.txt", "NUL .txt", "COM1", "COM9", "LPT1",
496
+ "lpt9.log",
497
+ ] {
498
+ unchanged(&format!(r"\\?\C:\dir\{name}"));
499
+ }
500
+ }
501
+
502
+ #[test]
503
+ fn non_verbatim_strips_names_that_only_resemble_device_names() {
504
+ for name in [
505
+ "CONSOLE", "NULL.txt", "COM0", "COM10", "LPT", "xCON", "AUXa",
506
+ ] {
507
+ assert_eq!(
508
+ non_verbatim(&format!(r"\\?\C:\dir\{name}")),
509
+ format!(r"C:\dir\{name}")
510
+ );
511
+ }
512
+ }
513
+
514
+ #[test]
515
+ fn non_verbatim_strips_up_to_max_path_and_no_further() {
516
+ let fits = format!(r"C:\{}", "a".repeat(256));
517
+ assert_eq!(fits.encode_utf16().count(), 259);
518
+ assert_eq!(non_verbatim(&format!(r"\\?\{fits}")), fits);
519
+ unchanged(&format!(r"\\?\C:\{}", "a".repeat(257)));
520
+ }
521
+
522
+ #[test]
523
+ fn path_placeholder_carries_the_non_verbatim_value() {
524
+ let placeholder = Placeholder::path("path", r"\\?\C:\a\f.txt");
525
+ assert_eq!(placeholder.name, "path");
526
+ assert_eq!(placeholder.value, r"C:\a\f.txt");
527
+ }
528
+
318
529
  #[test]
319
530
  fn splits_on_whitespace_without_placeholders() {
320
531
  assert_eq!(argv("echo hello world", &[]), ["echo", "hello", "world"]);
@@ -402,6 +613,47 @@ mod tests {
402
613
  assert_eq!(argv("run", &[Placeholder::new("path", "/tmp/x")]), ["run"]);
403
614
  }
404
615
 
616
+ #[test]
617
+ fn windows_quoting_wraps_a_plain_arg_in_quotes() {
618
+ assert_eq!(quote_windows_arg("hello"), r#""hello""#);
619
+ }
620
+
621
+ #[test]
622
+ fn windows_quoting_keeps_an_empty_arg_as_an_empty_quoted_word() {
623
+ assert_eq!(quote_windows_arg(""), r#""""#);
624
+ }
625
+
626
+ #[test]
627
+ fn windows_quoting_quotes_glob_and_brace_characters() {
628
+ assert_eq!(quote_windows_arg("[{}]"), r#""[{}]""#);
629
+ assert_eq!(quote_windows_arg("a?b*c"), r#""a?b*c""#);
630
+ }
631
+
632
+ #[test]
633
+ fn windows_quoting_leaves_backslashes_not_before_a_quote_alone() {
634
+ assert_eq!(
635
+ quote_windows_arg(r"\\?\C:\Users\x.txt"),
636
+ r#""\\?\C:\Users\x.txt""#
637
+ );
638
+ }
639
+
640
+ #[test]
641
+ fn windows_quoting_doubles_trailing_backslashes_before_the_closing_quote() {
642
+ assert_eq!(quote_windows_arg(r"C:\dir\"), r#""C:\dir\\""#);
643
+ assert_eq!(quote_windows_arg(r"a\\"), r#""a\\\\""#);
644
+ }
645
+
646
+ #[test]
647
+ fn windows_quoting_escapes_embedded_quotes() {
648
+ assert_eq!(quote_windows_arg(r#"[{"n":1}]"#), r#""[{\"n\":1}]""#);
649
+ }
650
+
651
+ #[test]
652
+ fn windows_quoting_doubles_backslashes_before_an_embedded_quote() {
653
+ assert_eq!(quote_windows_arg(r#"a\"b"#), r#""a\\\"b""#);
654
+ assert_eq!(quote_windows_arg(r#"a\\"b"#), r#""a\\\\\"b""#);
655
+ }
656
+
405
657
  #[test]
406
658
  fn extract_payload_takes_the_last_non_empty_line() {
407
659
  assert_eq!(
@@ -45,6 +45,9 @@ pub enum ConfigError {
45
45
  )]
46
46
  InvalidFunctionTimeout { name: String, value: String },
47
47
 
48
+ #[error("[[dirsql.function]] '{name}': 'batch' must be a positive integer, got {value}")]
49
+ InvalidFunctionBatch { name: String, value: String },
50
+
48
51
  #[error("Field '{0}' must not be empty")]
49
52
  EmptyField(&'static str),
50
53
 
@@ -212,6 +215,11 @@ pub struct FunctionSpec {
212
215
  /// Optional per-round-trip-call timeout, overriding the function
213
216
  /// mechanism's own 30-second default.
214
217
  pub timeout: Option<Duration>,
218
+ /// Optional cap on the calls one batched request carries. When set, a
219
+ /// statement's values are sent to the worker in bulk (see
220
+ /// `dirsql::functions`); when absent the worker only ever sees single
221
+ /// calls.
222
+ pub batch: Option<usize>,
215
223
  }
216
224
 
217
225
  /// Configuration for a single table.
@@ -267,6 +275,9 @@ struct RawFunction {
267
275
  command: Option<String>,
268
276
  deterministic: Option<bool>,
269
277
  timeout: Option<RawFunctionTimeout>,
278
+ // Any shape, so a non-integer hits the same actionable error as a
279
+ // non-positive one.
280
+ batch: Option<toml::Value>,
270
281
  }
271
282
 
272
283
  /// The `timeout` key accepts positive whole seconds (`timeout = 600`) or a
@@ -417,12 +428,27 @@ fn parse_function(raw: RawFunction) -> Result<FunctionSpec> {
417
428
  Some(raw_timeout) => Some(parse_function_timeout(&name, &raw_timeout)?),
418
429
  };
419
430
 
431
+ let batch = match raw.batch {
432
+ None => None,
433
+ Some(value) => Some(
434
+ value
435
+ .as_integer()
436
+ .and_then(|n| usize::try_from(n).ok())
437
+ .filter(|n| *n > 0)
438
+ .ok_or_else(|| ConfigError::InvalidFunctionBatch {
439
+ name: name.clone(),
440
+ value: value.to_string(),
441
+ })?,
442
+ ),
443
+ };
444
+
420
445
  Ok(FunctionSpec {
421
446
  name,
422
447
  args,
423
448
  command,
424
449
  deterministic: raw.deterministic.unwrap_or(false),
425
450
  timeout,
451
+ batch,
426
452
  })
427
453
  }
428
454
 
@@ -1162,6 +1188,7 @@ args = [1, 2]
1162
1188
  command = "dirsql-plugin-embeddings worker"
1163
1189
  deterministic = true
1164
1190
  timeout = "600s"
1191
+ batch = 64
1165
1192
  "#;
1166
1193
  let config = load_config_str(toml).unwrap();
1167
1194
  assert_eq!(
@@ -1172,6 +1199,7 @@ timeout = "600s"
1172
1199
  command: "dirsql-plugin-embeddings worker".to_string(),
1173
1200
  deterministic: true,
1174
1201
  timeout: Some(Duration::from_secs(600)),
1202
+ batch: Some(64),
1175
1203
  }]
1176
1204
  );
1177
1205
  }
@@ -1187,6 +1215,36 @@ command = "worker"
1187
1215
  let config = load_config_str(toml).unwrap();
1188
1216
  assert!(!config.functions[0].deterministic);
1189
1217
  assert!(config.functions[0].timeout.is_none());
1218
+ assert!(config.functions[0].batch.is_none());
1219
+ }
1220
+
1221
+ /// `batch = 0` would send empty requests, a negative cap means nothing,
1222
+ /// and a string is not a count; each is the one actionable error.
1223
+ #[test]
1224
+ fn function_batch_rejects_anything_but_a_positive_integer() {
1225
+ for value in ["0", "-4", "\"many\"", "2.5"] {
1226
+ let toml = format!(
1227
+ r#"
1228
+ [[dirsql.function]]
1229
+ name = "f"
1230
+ args = [1]
1231
+ command = "worker"
1232
+ batch = {value}
1233
+ "#
1234
+ );
1235
+ let err = load_config_str(&toml).unwrap_err();
1236
+ match &err {
1237
+ ConfigError::InvalidFunctionBatch { name, value: got } => {
1238
+ assert_eq!(name, "f");
1239
+ assert_eq!(got, value);
1240
+ }
1241
+ other => panic!("batch = {value}: expected InvalidFunctionBatch, got {other:?}"),
1242
+ }
1243
+ assert_eq!(
1244
+ err.to_string(),
1245
+ format!("[[dirsql.function]] 'f': 'batch' must be a positive integer, got {value}")
1246
+ );
1247
+ }
1190
1248
  }
1191
1249
 
1192
1250
  #[test]