dirsql 0.4.58__tar.gz → 0.4.60__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 (255) hide show
  1. {dirsql-0.4.58 → dirsql-0.4.60}/Cargo.lock +2 -2
  2. {dirsql-0.4.58 → dirsql-0.4.60}/PKG-INFO +1 -1
  3. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/Cargo.toml +1 -1
  4. dirsql-0.4.60/packages/python/e2e-attestations/claude-1184-win-rel-paths.json +7 -0
  5. dirsql-0.4.60/packages/python/e2e-attestations/claude-1186-win-test-harness.json +7 -0
  6. dirsql-0.4.60/packages/python/e2e-attestations/claude-1207-win-argv-quote.json +7 -0
  7. dirsql-0.4.60/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json +7 -0
  8. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/Cargo.toml +1 -1
  9. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/command.rs +254 -2
  10. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/db.rs +1 -1
  11. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/extension_resolution.rs +5 -1
  12. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/lib.rs +37 -27
  13. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/parsed_vtab.rs +12 -9
  14. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/scanner.rs +36 -0
  15. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/vtab.rs +14 -4
  16. {dirsql-0.4.58 → dirsql-0.4.60}/Cargo.toml +0 -0
  17. {dirsql-0.4.58 → dirsql-0.4.60}/README.md +0 -0
  18. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/__init__.py +0 -0
  19. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/_async.py +0 -0
  20. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/_dirsql.pyi +0 -0
  21. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/__init__.py +0 -0
  22. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/discover_plugins/__init__.py +0 -0
  23. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  24. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  25. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  26. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  27. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  28. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/main.py +0 -0
  29. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/cli/resolve_config_extensions.py +0 -0
  30. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/plan_entry_path.py +0 -0
  31. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/py.typed +0 -0
  32. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/read_config.py +0 -0
  33. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/resolve_configs_extension_specs.py +0 -0
  34. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/resolve_extension.py +0 -0
  35. {dirsql-0.4.58 → dirsql-0.4.60}/dirsql/resolve_package.py +0 -0
  36. {dirsql-0.4.58 → dirsql-0.4.60}/docs/.claude/CLAUDE.md +0 -0
  37. {dirsql-0.4.58 → dirsql-0.4.60}/docs/.vitepress/config.ts +0 -0
  38. {dirsql-0.4.58 → dirsql-0.4.60}/docs/.vitepress/theme/index.ts +0 -0
  39. {dirsql-0.4.58 → dirsql-0.4.60}/docs/.vitepress/theme/lang.ts +0 -0
  40. {dirsql-0.4.58 → dirsql-0.4.60}/docs/AGENTS.md +0 -0
  41. {dirsql-0.4.58 → dirsql-0.4.60}/docs/explanation.md +0 -0
  42. {dirsql-0.4.58 → dirsql-0.4.60}/docs/getting-started.md +0 -0
  43. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/columns-from-paths.md +0 -0
  44. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/define-tables.md +0 -0
  45. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/embed.md +0 -0
  46. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/extract-from-contents.md +0 -0
  47. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/load-extension.md +0 -0
  48. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/parse-files-into-columns.md +0 -0
  49. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/persist.md +0 -0
  50. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/query-json.md +0 -0
  51. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/query-without-config.md +0 -0
  52. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/react-to-changes.md +0 -0
  53. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/search-by-meaning.md +0 -0
  54. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/search-indexes.md +0 -0
  55. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/skip-files.md +0 -0
  56. {dirsql-0.4.58 → dirsql-0.4.60}/docs/howto/write-a-plugin.md +0 -0
  57. {dirsql-0.4.58 → dirsql-0.4.60}/docs/index.md +0 -0
  58. {dirsql-0.4.58 → dirsql-0.4.60}/docs/package.json +0 -0
  59. {dirsql-0.4.58 → dirsql-0.4.60}/docs/plugins.md +0 -0
  60. {dirsql-0.4.58 → dirsql-0.4.60}/docs/pnpm-lock.yaml +0 -0
  61. {dirsql-0.4.58 → dirsql-0.4.60}/docs/pnpm-workspace.yaml +0 -0
  62. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/cli.md +0 -0
  63. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/columns.md +0 -0
  64. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/config.md +0 -0
  65. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/hooks.md +0 -0
  66. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/http-api.md +0 -0
  67. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/path-tables.md +0 -0
  68. {dirsql-0.4.58 → dirsql-0.4.60}/docs/reference/sdk.md +0 -0
  69. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/CHANGELOG.md +0 -0
  70. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/MIGRATIONS.md +0 -0
  71. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/README.md +0 -0
  72. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  73. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  74. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  75. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  76. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  77. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  78. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  79. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  80. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  81. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  82. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  83. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  84. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  85. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  86. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  87. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  88. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  89. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  90. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  91. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  92. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  93. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  94. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  95. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  96. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
  97. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-18-export-scan-failure.md +0 -0
  98. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +0 -0
  99. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
  100. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  101. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/2026-09-24-query-rows-in-projection-order.md +0 -0
  102. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/changelog.d/README.md +0 -0
  103. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/conftest.py +0 -0
  104. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/.claude/CLAUDE.md +0 -0
  105. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/.vitepress/config.ts +0 -0
  106. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  107. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  108. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/AGENTS.md +0 -0
  109. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/explanation.md +0 -0
  110. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/getting-started.md +0 -0
  111. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/columns-from-paths.md +0 -0
  112. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/define-tables.md +0 -0
  113. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/embed.md +0 -0
  114. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/extract-from-contents.md +0 -0
  115. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/load-extension.md +0 -0
  116. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  117. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/persist.md +0 -0
  118. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/query-json.md +0 -0
  119. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/query-without-config.md +0 -0
  120. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/react-to-changes.md +0 -0
  121. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/search-by-meaning.md +0 -0
  122. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/search-indexes.md +0 -0
  123. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/skip-files.md +0 -0
  124. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/howto/write-a-plugin.md +0 -0
  125. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/index.md +0 -0
  126. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/package.json +0 -0
  127. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/plugins.md +0 -0
  128. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/pnpm-lock.yaml +0 -0
  129. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/pnpm-workspace.yaml +0 -0
  130. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/cli.md +0 -0
  131. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/columns.md +0 -0
  132. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/config.md +0 -0
  133. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/hooks.md +0 -0
  134. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/http-api.md +0 -0
  135. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/path-tables.md +0 -0
  136. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/docs/reference/sdk.md +0 -0
  137. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  138. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  139. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  140. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  141. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  142. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
  143. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
  144. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  145. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
  146. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
  147. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
  148. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
  149. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1130-codec-crates.json +0 -0
  150. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +0 -0
  151. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1139-sse-flatten.json +0 -0
  152. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1140-scan-failure-export.json +0 -0
  153. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1148-clippy-all-targets.json +0 -0
  154. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1164-sigint-fatal.json +0 -0
  155. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1170-projection-order.json +0 -0
  156. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1172-on-file-path.json +0 -0
  157. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1173-py-projection-order.json +0 -0
  158. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-1185-win-abs-table-names.json +0 -0
  159. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  160. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  161. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  162. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  163. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  164. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  165. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  166. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  167. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  168. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  169. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  170. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  171. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  172. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  173. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  174. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  175. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  176. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  177. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  178. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  179. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  180. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  181. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  182. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  183. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  184. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  185. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/2026-09-24-query-rows-in-projection-order.md +0 -0
  186. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/migrations.d/README.md +0 -0
  187. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/src/lib.rs +0 -0
  188. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/testing-conventions.toml +0 -0
  189. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/__init__.py +0 -0
  190. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/conftest.py +0 -0
  191. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/e2e/__init__.py +0 -0
  192. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  193. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  194. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/e2e/speed_of_light.py +0 -0
  195. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/integration/__init__.py +0 -0
  196. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/integration/binding/__init__.py +0 -0
  197. {dirsql-0.4.58 → dirsql-0.4.60}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  198. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/CHANGELOG.md +0 -0
  199. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/MIGRATIONS.md +0 -0
  200. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/README.md +0 -0
  201. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/benches/db_bench.rs +0 -0
  202. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/benches/differ_bench.rs +0 -0
  203. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/benches/matcher_bench.rs +0 -0
  204. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/benches/scanner_bench.rs +0 -0
  205. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/explanation.md +0 -0
  206. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/getting-started.md +0 -0
  207. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  208. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/define-tables.md +0 -0
  209. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/embed.md +0 -0
  210. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  211. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/load-extension.md +0 -0
  212. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  213. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/persist.md +0 -0
  214. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/query-json.md +0 -0
  215. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/query-without-config.md +0 -0
  216. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/react-to-changes.md +0 -0
  217. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  218. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/search-indexes.md +0 -0
  219. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/skip-files.md +0 -0
  220. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  221. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/index.md +0 -0
  222. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/plugins.md +0 -0
  223. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/cli.md +0 -0
  224. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/columns.md +0 -0
  225. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/config.md +0 -0
  226. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/hooks.md +0 -0
  227. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/http-api.md +0 -0
  228. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/path-tables.md +0 -0
  229. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/docs/reference/sdk.md +0 -0
  230. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/bin/dirsql.rs +0 -0
  231. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/execute.rs +0 -0
  232. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/init.rs +0 -0
  233. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/mod.rs +0 -0
  234. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/repl.rs +0 -0
  235. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/router.rs +0 -0
  236. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/run.rs +0 -0
  237. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/serialize.rs +0 -0
  238. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/server.rs +0 -0
  239. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/cli/table.rs +0 -0
  240. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/config.rs +0 -0
  241. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/default_config.toml +0 -0
  242. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/differ.rs +0 -0
  243. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/functions.rs +0 -0
  244. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/infer.rs +0 -0
  245. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/launcher.rs +0 -0
  246. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/matcher.rs +0 -0
  247. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/parsed_cache.rs +0 -0
  248. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/path_table.rs +0 -0
  249. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/persist.rs +0 -0
  250. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/progress.rs +0 -0
  251. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/row_event_flat.rs +0 -0
  252. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/sql_literal.rs +0 -0
  253. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/vtab_scaffold.rs +0 -0
  254. {dirsql-0.4.58 → dirsql-0.4.60}/packages/rust/src/watcher.rs +0 -0
  255. {dirsql-0.4.58 → dirsql-0.4.60}/pyproject.toml +0 -0
@@ -552,7 +552,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
552
552
 
553
553
  [[package]]
554
554
  name = "dirsql"
555
- version = "0.4.58"
555
+ version = "0.4.60"
556
556
  dependencies = [
557
557
  "assert_cmd",
558
558
  "axum",
@@ -601,7 +601,7 @@ dependencies = [
601
601
 
602
602
  [[package]]
603
603
  name = "dirsql-py-ext"
604
- version = "0.4.58"
604
+ version = "0.4.60"
605
605
  dependencies = [
606
606
  "dirsql",
607
607
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.58
3
+ Version: 0.4.60
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
@@ -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.58"
7
+ version = "0.4.60"
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,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790605446,
4
+ "exit_code": 0,
5
+ "commit": "ab668f9f958ed79fe0598f6cc494f1d0cc049316",
6
+ "branch": "claude/1184-win-rel-paths"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790603833,
4
+ "exit_code": 0,
5
+ "commit": "6b84d84d60179482a842b0db205ee257f8e03c11",
6
+ "branch": "claude/1186-win-test-harness"
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.58"
9
+ version = "0.4.60"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -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!(
@@ -655,7 +655,7 @@ impl Db {
655
655
  /// Every distinct `(table_name, file_path)` whose file sits beneath the
656
656
  /// directory `dir`, ordered by table then path.
657
657
  pub fn files_under(&self, dir: &str) -> Result<Vec<(String, String)>> {
658
- let prefix = format!("{dir}{}", std::path::MAIN_SEPARATOR);
658
+ let prefix = format!("{dir}/");
659
659
  let mut stmt = self.conn.prepare(
660
660
  "SELECT DISTINCT table_name, file_path FROM _dirsql_internal_rows \
661
661
  WHERE substr(file_path, 1, length(?1)) = ?1 \
@@ -512,7 +512,11 @@ mod tests {
512
512
  );
513
513
  assert_eq!(
514
514
  err.to_string(),
515
- "multiple loadable extension files found in package 'vec': /pkg/a.so, /pkg/z.so; disambiguate with a literal path"
515
+ format!(
516
+ "multiple loadable extension files found in package 'vec': {}, {}; disambiguate with a literal path",
517
+ dir.join("a.so").display(),
518
+ dir.join("z.so").display()
519
+ )
516
520
  );
517
521
  }
518
522
  }
@@ -571,7 +571,7 @@ impl DirSQL {
571
571
  // unspecified. An `on_file` failure produces an error event for that
572
572
  // table only; the other matching tables still process the event.
573
573
  let matches = self.inner.matcher.match_all(&rel_path_buf);
574
- let rel_path = rel_path_buf.to_string_lossy().to_string();
574
+ let rel_path = scanner::to_slash(&rel_path_buf);
575
575
 
576
576
  let mut events = Vec::new();
577
577
  for m in matches {
@@ -625,11 +625,7 @@ impl DirSQL {
625
625
  };
626
626
  let mut events = Vec::new();
627
627
  for (path, table) in self.inner.fs.scan_subtree(base, dir, &self.inner.matcher) {
628
- let rel_path = path
629
- .strip_prefix(base)
630
- .unwrap_or(&path)
631
- .to_string_lossy()
632
- .to_string();
628
+ let rel_path = scanner::to_slash(path.strip_prefix(base).unwrap_or(&path));
633
629
  events.extend(self.handle_upsert(&table, &path, &rel_path));
634
630
  }
635
631
  events
@@ -1836,10 +1832,7 @@ fn run_channel_loop(db: DirSQL, tx: UnboundedSender<RowEvent>) {
1836
1832
  }
1837
1833
 
1838
1834
  fn relative_path(root: &Path, path: &Path) -> String {
1839
- path.strip_prefix(root)
1840
- .unwrap_or(path)
1841
- .to_string_lossy()
1842
- .to_string()
1835
+ scanner::to_slash(path.strip_prefix(root).unwrap_or(path))
1843
1836
  }
1844
1837
 
1845
1838
  /// Build [`Table`] objects from a parsed config.
@@ -1945,8 +1938,8 @@ fn run_on_file(
1945
1938
  root: &Path,
1946
1939
  ) -> std::result::Result<Vec<Row>, BoxError> {
1947
1940
  let placeholders = [
1948
- Placeholder::new("path", abs_path),
1949
- Placeholder::new("root", root.to_string_lossy().into_owned()),
1941
+ Placeholder::path("path", abs_path),
1942
+ Placeholder::path("root", &root.to_string_lossy()),
1950
1943
  ];
1951
1944
 
1952
1945
  // Errors travel to the caller rather than being logged and flattened to an
@@ -2943,7 +2936,12 @@ mod internal_tests {
2943
2936
  /// so `notify` never sees `.`.
2944
2937
  #[test]
2945
2938
  fn relative_root_canonicalizes_watch_root_only() {
2946
- let fake = FakeFs::default().with_canonical_root(".", "/ws/canonical");
2939
+ let canonical = if cfg!(windows) {
2940
+ r"C:\ws\canonical"
2941
+ } else {
2942
+ "/ws/canonical"
2943
+ };
2944
+ let fake = FakeFs::default().with_canonical_root(".", canonical);
2947
2945
  let db = DirSQL::with_ignore_and_fs(
2948
2946
  ".",
2949
2947
  vec![Table::new(
@@ -2963,7 +2961,7 @@ mod internal_tests {
2963
2961
  "watch_root must be absolute, got {:?}",
2964
2962
  db.inner.watch_root
2965
2963
  );
2966
- assert_eq!(db.inner.watch_root, PathBuf::from("/ws/canonical"));
2964
+ assert_eq!(db.inner.watch_root, PathBuf::from(canonical));
2967
2965
  }
2968
2966
 
2969
2967
  /// With an absolute root, `process_file_event` strips the `watch_root`
@@ -4346,15 +4344,13 @@ mod internal_tests {
4346
4344
  }
4347
4345
 
4348
4346
  /// `{abspath}` is not in the substitution table: it is left literal like any
4349
- /// unknown `{…}`, so `printf` receives the string `{abspath}` verbatim. The
4350
- /// template references `{path}` so that arg is the real path (and no path is
4351
- /// appended), isolating the `{abspath}` behavior in the `q` column.
4347
+ /// unknown `{…}`, so `printf` receives the string `{abspath}` verbatim.
4352
4348
  #[test]
4353
4349
  fn run_on_file_does_not_substitute_abspath() {
4354
4350
  let dir = TempDir::new().unwrap();
4355
4351
  let abs = dir.path().join("f.txt");
4356
4352
  let rows = run_on_file(
4357
- r#"printf '[{"p":"%s","q":"%s"}]' {path} {abspath}"#,
4353
+ r#"printf '[{"q":"%s"}]' {abspath}"#,
4358
4354
  &abs.to_string_lossy(),
4359
4355
  dir.path(),
4360
4356
  dir.path(),
@@ -4365,25 +4361,39 @@ mod internal_tests {
4365
4361
  }
4366
4362
 
4367
4363
  /// `{path}` interpolates the matched file's **absolute** path (not a
4368
- /// root-relative one). The command echoes its `{path}` argument back as a
4369
- /// row value, and we assert it is byte-for-byte the absolute path even when
4370
- /// the file sits directly under `root` (the case the old `strip_prefix`
4371
- /// would have shortened to a bare relative path).
4364
+ /// root-relative one), byte-for-byte, even when the file sits directly
4365
+ /// under `root`. The command writes its argument to a side file rather than
4366
+ /// into the JSON payload, where a Windows path's `\` would need escaping.
4372
4367
  #[test]
4373
4368
  fn run_on_file_passes_absolute_path_for_path_placeholder() {
4374
4369
  let dir = TempDir::new().unwrap();
4375
4370
  let abs = dir.path().join("f.txt");
4376
- let rows = run_on_file(
4377
- r#"sh -c "printf '[{\"p\":\"%s\"}]' \"$1\"" sh {path}"#,
4371
+ run_on_file(
4372
+ r#"sh -c 'printf %s "$1" > seen; echo "[]"' sh {path}"#,
4378
4373
  &abs.to_string_lossy(),
4379
4374
  dir.path(),
4380
4375
  dir.path(),
4381
4376
  )
4382
4377
  .expect("a well-formed payload parses");
4383
- assert_eq!(rows.len(), 1);
4384
4378
  assert_eq!(
4385
- rows[0]["p"],
4386
- Value::Text(abs.to_string_lossy().into_owned())
4379
+ std::fs::read_to_string(dir.path().join("seen")).unwrap(),
4380
+ abs.to_string_lossy()
4381
+ );
4382
+ }
4383
+
4384
+ #[test]
4385
+ fn run_on_file_hands_the_hook_non_verbatim_path_and_root() {
4386
+ let dir = TempDir::new().unwrap();
4387
+ run_on_file(
4388
+ r#"sh -c 'printf "%s|%s" "$1" "$2" > seen; echo "[]"' sh {path} {root}"#,
4389
+ r"\\?\C:\r\f.txt",
4390
+ dir.path(),
4391
+ Path::new(r"\\?\D:\r"),
4392
+ )
4393
+ .expect("a well-formed payload parses");
4394
+ assert_eq!(
4395
+ std::fs::read_to_string(dir.path().join("seen")).unwrap(),
4396
+ r"C:\r\f.txt|D:\r"
4387
4397
  );
4388
4398
  }
4389
4399
 
@@ -36,7 +36,7 @@ use crate::matcher::TableMatcher;
36
36
  use crate::parsed_cache::{self, CachedParse, Entry, RowCache, SqliteRowCache};
37
37
  use crate::path_table;
38
38
  use crate::persist::{FileStat, hash_file, now_ns};
39
- use crate::scanner::scan_glob;
39
+ use crate::scanner::{scan_glob, to_slash};
40
40
  use crate::vtab_scaffold::{self, TableSource};
41
41
 
42
42
  /// SQL module name a parsed path-table is created with.
@@ -161,7 +161,7 @@ fn collect_rows_cached(
161
161
 
162
162
  let mut seen: Vec<(String, Parsed)> = Vec::with_capacity(rel_paths.len());
163
163
  for rel_path in rel_paths {
164
- let key = rel_path.to_string_lossy().into_owned();
164
+ let key = to_slash(rel_path);
165
165
  let Some(live) = fs.stat(rel_path) else {
166
166
  // The file vanished between the scan and the stat. Nothing to
167
167
  // parse and nothing to cache; the next run decides afresh.
@@ -268,7 +268,7 @@ impl ParsedFs for RootedFs<'_> {
268
268
  fn command_skip_message(rel_path: &Path, error: &str) -> String {
269
269
  format!(
270
270
  "dirsql: skipping `{}`: on-file command failed: {error}",
271
- rel_path.display()
271
+ to_slash(rel_path)
272
272
  )
273
273
  }
274
274
 
@@ -276,7 +276,7 @@ fn command_skip_message(rel_path: &Path, error: &str) -> String {
276
276
  fn parse_skip_message(rel_path: &Path, message: &str) -> String {
277
277
  format!(
278
278
  "dirsql: skipping `{}`: on-file output was not a JSON array of rows: {message}",
279
- rel_path.display()
279
+ to_slash(rel_path)
280
280
  )
281
281
  }
282
282
 
@@ -292,8 +292,8 @@ fn no_rows_message(pattern: &str) -> String {
292
292
  fn run_parser(command: &str, root: &Path, rel_path: &Path) -> std::result::Result<String, String> {
293
293
  let abs_path = root.join(rel_path);
294
294
  let placeholders = [
295
- Placeholder::new("path", abs_path.to_string_lossy().into_owned()),
296
- Placeholder::new("root", root.to_string_lossy().into_owned()),
295
+ Placeholder::path("path", &abs_path.to_string_lossy()),
296
+ Placeholder::path("root", &root.to_string_lossy()),
297
297
  ];
298
298
 
299
299
  run_command(command, &placeholders, root, None)
@@ -660,9 +660,12 @@ mod tests {
660
660
 
661
661
  #[test]
662
662
  fn run_parser_substitutes_the_absolute_path_and_the_root() {
663
- let payload =
664
- run_parser("echo {path} {root}", Path::new("/tmp"), Path::new("a.json")).unwrap();
665
- assert_eq!(payload, "/tmp/a.json /tmp");
663
+ let root = std::env::temp_dir();
664
+ let payload = run_parser("echo {path} {root}", &root, Path::new("a.json")).unwrap();
665
+ assert_eq!(
666
+ payload,
667
+ format!("{} {}", root.join("a.json").display(), root.display())
668
+ );
666
669
  }
667
670
 
668
671
  #[test]
@@ -10,6 +10,19 @@ use walkdir::WalkDir;
10
10
  /// whether persistence is enabled.
11
11
  pub const RESERVED_DIR: &str = ".dirsql";
12
12
 
13
+ /// A root-relative path as dirsql stores and reports it: `/`-separated on
14
+ /// every platform, so globs, `file_path` keys and `path` columns agree.
15
+ pub(crate) fn to_slash(path: &Path) -> String {
16
+ with_slashes(&path.to_string_lossy(), std::path::MAIN_SEPARATOR)
17
+ }
18
+
19
+ fn with_slashes(path: &str, native: char) -> String {
20
+ if native == '/' {
21
+ return path.to_owned();
22
+ }
23
+ path.replace(native, "/")
24
+ }
25
+
13
26
  /// Walk a directory tree and return all file paths paired with their matching table name.
14
27
  /// Ignored paths and directories are skipped. Only files (not directories) are returned.
15
28
  ///
@@ -354,6 +367,29 @@ mod tests {
354
367
  assert!(!is_glob_match(&set, Path::new("a.md")));
355
368
  }
356
369
 
370
+ #[test]
371
+ fn with_slashes_rewrites_a_backslash_native_separator() {
372
+ assert_eq!(
373
+ with_slashes(r"moved\one\mid.txt", '\\'),
374
+ "moved/one/mid.txt"
375
+ );
376
+ }
377
+
378
+ #[test]
379
+ fn with_slashes_keeps_a_mixed_path_slash_separated() {
380
+ assert_eq!(with_slashes(r"a/b\c.txt", '\\'), "a/b/c.txt");
381
+ }
382
+
383
+ #[test]
384
+ fn with_slashes_leaves_a_backslash_in_a_unix_file_name() {
385
+ assert_eq!(with_slashes(r"dir/a\b.txt", '/'), r"dir/a\b.txt");
386
+ }
387
+
388
+ #[test]
389
+ fn to_slash_keeps_a_native_unix_path() {
390
+ assert_eq!(to_slash(Path::new("docs/nested/a.md")), "docs/nested/a.md");
391
+ }
392
+
357
393
  #[test]
358
394
  fn compile_glob_rejects_an_invalid_pattern() {
359
395
  assert!(compile_glob("[").is_err());