dirsql 0.4.54__tar.gz → 0.4.56__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 (246) hide show
  1. {dirsql-0.4.54 → dirsql-0.4.56}/Cargo.lock +4 -2
  2. {dirsql-0.4.54 → dirsql-0.4.56}/Cargo.toml +3 -1
  3. {dirsql-0.4.54 → dirsql-0.4.56}/PKG-INFO +1 -1
  4. {dirsql-0.4.54 → dirsql-0.4.56}/docs/.vitepress/config.ts +1 -1
  5. {dirsql-0.4.54 → dirsql-0.4.56}/docs/AGENTS.md +2 -2
  6. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/sdk.md +19 -0
  7. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/Cargo.toml +1 -1
  8. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/.vitepress/config.ts +1 -1
  9. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/AGENTS.md +2 -2
  10. {dirsql-0.4.54/packages/rust → dirsql-0.4.56/packages/python}/docs/reference/sdk.md +19 -0
  11. dirsql-0.4.56/packages/python/e2e-attestations/claude-1170-projection-order.json +7 -0
  12. dirsql-0.4.56/packages/python/e2e-attestations/claude-1172-on-file-path.json +7 -0
  13. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/Cargo.toml +1 -1
  14. {dirsql-0.4.54/packages/python → dirsql-0.4.56/packages/rust}/docs/reference/sdk.md +19 -0
  15. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/execute.rs +7 -3
  16. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/serialize.rs +48 -3
  17. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/table.rs +31 -6
  18. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/command.rs +104 -9
  19. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/db.rs +53 -1
  20. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/lib.rs +9 -2
  21. {dirsql-0.4.54 → dirsql-0.4.56}/README.md +0 -0
  22. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/__init__.py +0 -0
  23. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/_async.py +0 -0
  24. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/_dirsql.pyi +0 -0
  25. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/__init__.py +0 -0
  26. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/discover_plugins/__init__.py +0 -0
  27. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  28. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  29. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  30. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  31. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  32. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/main.py +0 -0
  33. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/cli/resolve_config_extensions.py +0 -0
  34. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/plan_entry_path.py +0 -0
  35. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/py.typed +0 -0
  36. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/read_config.py +0 -0
  37. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/resolve_configs_extension_specs.py +0 -0
  38. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/resolve_extension.py +0 -0
  39. {dirsql-0.4.54 → dirsql-0.4.56}/dirsql/resolve_package.py +0 -0
  40. {dirsql-0.4.54 → dirsql-0.4.56}/docs/.claude/CLAUDE.md +0 -0
  41. {dirsql-0.4.54 → dirsql-0.4.56}/docs/.vitepress/theme/index.ts +0 -0
  42. {dirsql-0.4.54 → dirsql-0.4.56}/docs/.vitepress/theme/lang.ts +0 -0
  43. {dirsql-0.4.54 → dirsql-0.4.56}/docs/explanation.md +0 -0
  44. {dirsql-0.4.54 → dirsql-0.4.56}/docs/getting-started.md +0 -0
  45. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/columns-from-paths.md +0 -0
  46. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/define-tables.md +0 -0
  47. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/embed.md +0 -0
  48. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/extract-from-contents.md +0 -0
  49. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/load-extension.md +0 -0
  50. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/parse-files-into-columns.md +0 -0
  51. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/persist.md +0 -0
  52. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/query-json.md +0 -0
  53. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/query-without-config.md +0 -0
  54. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/react-to-changes.md +0 -0
  55. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/search-by-meaning.md +0 -0
  56. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/search-indexes.md +0 -0
  57. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/skip-files.md +0 -0
  58. {dirsql-0.4.54 → dirsql-0.4.56}/docs/howto/write-a-plugin.md +0 -0
  59. {dirsql-0.4.54 → dirsql-0.4.56}/docs/index.md +0 -0
  60. {dirsql-0.4.54 → dirsql-0.4.56}/docs/package.json +0 -0
  61. {dirsql-0.4.54 → dirsql-0.4.56}/docs/plugins.md +0 -0
  62. {dirsql-0.4.54 → dirsql-0.4.56}/docs/pnpm-lock.yaml +0 -0
  63. {dirsql-0.4.54 → dirsql-0.4.56}/docs/pnpm-workspace.yaml +0 -0
  64. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/cli.md +0 -0
  65. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/columns.md +0 -0
  66. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/config.md +0 -0
  67. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/hooks.md +0 -0
  68. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/http-api.md +0 -0
  69. {dirsql-0.4.54 → dirsql-0.4.56}/docs/reference/path-tables.md +0 -0
  70. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/CHANGELOG.md +0 -0
  71. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/MIGRATIONS.md +0 -0
  72. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/README.md +0 -0
  73. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  74. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  75. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  76. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  77. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  78. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  79. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  80. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  81. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  82. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  83. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  84. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  85. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  86. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  87. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  88. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  89. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  90. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  91. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  92. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  93. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  94. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  95. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  96. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  97. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
  98. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-18-export-scan-failure.md +0 -0
  99. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +0 -0
  100. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
  101. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  102. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/changelog.d/README.md +0 -0
  103. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/conftest.py +0 -0
  104. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/.claude/CLAUDE.md +0 -0
  105. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  106. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  107. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/explanation.md +0 -0
  108. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/getting-started.md +0 -0
  109. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/columns-from-paths.md +0 -0
  110. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/define-tables.md +0 -0
  111. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/embed.md +0 -0
  112. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/extract-from-contents.md +0 -0
  113. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/load-extension.md +0 -0
  114. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  115. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/persist.md +0 -0
  116. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/query-json.md +0 -0
  117. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/query-without-config.md +0 -0
  118. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/react-to-changes.md +0 -0
  119. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/search-by-meaning.md +0 -0
  120. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/search-indexes.md +0 -0
  121. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/skip-files.md +0 -0
  122. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/howto/write-a-plugin.md +0 -0
  123. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/index.md +0 -0
  124. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/package.json +0 -0
  125. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/plugins.md +0 -0
  126. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/pnpm-lock.yaml +0 -0
  127. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/pnpm-workspace.yaml +0 -0
  128. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/reference/cli.md +0 -0
  129. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/reference/columns.md +0 -0
  130. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/reference/config.md +0 -0
  131. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/reference/hooks.md +0 -0
  132. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/reference/http-api.md +0 -0
  133. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/docs/reference/path-tables.md +0 -0
  134. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  135. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  136. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  137. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  138. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  139. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
  140. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
  141. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  142. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
  143. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
  144. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
  145. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
  146. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1130-codec-crates.json +0 -0
  147. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +0 -0
  148. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1139-sse-flatten.json +0 -0
  149. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1140-scan-failure-export.json +0 -0
  150. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1148-clippy-all-targets.json +0 -0
  151. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-1164-sigint-fatal.json +0 -0
  152. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  153. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  154. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  155. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  156. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  157. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  158. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  159. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  160. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  161. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  162. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  163. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  164. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  165. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  166. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  167. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  168. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  169. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  170. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  171. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  172. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  173. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  174. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  175. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  176. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  177. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  178. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/migrations.d/README.md +0 -0
  179. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/src/lib.rs +0 -0
  180. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/testing-conventions.toml +0 -0
  181. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/__init__.py +0 -0
  182. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/conftest.py +0 -0
  183. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/e2e/__init__.py +0 -0
  184. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  185. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  186. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/integration/__init__.py +0 -0
  187. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/integration/binding/__init__.py +0 -0
  188. {dirsql-0.4.54 → dirsql-0.4.56}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  189. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/CHANGELOG.md +0 -0
  190. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/MIGRATIONS.md +0 -0
  191. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/README.md +0 -0
  192. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/benches/db_bench.rs +0 -0
  193. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/benches/differ_bench.rs +0 -0
  194. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/benches/matcher_bench.rs +0 -0
  195. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/benches/scanner_bench.rs +0 -0
  196. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/explanation.md +0 -0
  197. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/getting-started.md +0 -0
  198. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  199. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/define-tables.md +0 -0
  200. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/embed.md +0 -0
  201. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  202. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/load-extension.md +0 -0
  203. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  204. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/persist.md +0 -0
  205. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/query-json.md +0 -0
  206. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/query-without-config.md +0 -0
  207. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/react-to-changes.md +0 -0
  208. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  209. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/search-indexes.md +0 -0
  210. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/skip-files.md +0 -0
  211. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  212. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/index.md +0 -0
  213. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/plugins.md +0 -0
  214. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/reference/cli.md +0 -0
  215. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/reference/columns.md +0 -0
  216. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/reference/config.md +0 -0
  217. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/reference/hooks.md +0 -0
  218. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/reference/http-api.md +0 -0
  219. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/docs/reference/path-tables.md +0 -0
  220. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/bin/dirsql.rs +0 -0
  221. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/init.rs +0 -0
  222. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/mod.rs +0 -0
  223. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/repl.rs +0 -0
  224. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/router.rs +0 -0
  225. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/run.rs +0 -0
  226. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/cli/server.rs +0 -0
  227. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/config.rs +0 -0
  228. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/default_config.toml +0 -0
  229. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/differ.rs +0 -0
  230. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/extension_resolution.rs +0 -0
  231. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/functions.rs +0 -0
  232. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/infer.rs +0 -0
  233. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/launcher.rs +0 -0
  234. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/matcher.rs +0 -0
  235. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/parsed_cache.rs +0 -0
  236. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/parsed_vtab.rs +0 -0
  237. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/path_table.rs +0 -0
  238. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/persist.rs +0 -0
  239. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/progress.rs +0 -0
  240. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/row_event_flat.rs +0 -0
  241. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/scanner.rs +0 -0
  242. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/sql_literal.rs +0 -0
  243. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/vtab.rs +0 -0
  244. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/vtab_scaffold.rs +0 -0
  245. {dirsql-0.4.54 → dirsql-0.4.56}/packages/rust/src/watcher.rs +0 -0
  246. {dirsql-0.4.54 → dirsql-0.4.56}/pyproject.toml +0 -0
@@ -552,7 +552,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
552
552
 
553
553
  [[package]]
554
554
  name = "dirsql"
555
- version = "0.4.54"
555
+ version = "0.4.56"
556
556
  dependencies = [
557
557
  "assert_cmd",
558
558
  "axum",
@@ -592,6 +592,7 @@ name = "dirsql-napi"
592
592
  version = "0.1.0"
593
593
  dependencies = [
594
594
  "dirsql",
595
+ "libc",
595
596
  "napi",
596
597
  "napi-build",
597
598
  "napi-derive",
@@ -599,7 +600,7 @@ dependencies = [
599
600
 
600
601
  [[package]]
601
602
  name = "dirsql-py-ext"
602
- version = "0.4.54"
603
+ version = "0.4.56"
603
604
  dependencies = [
604
605
  "dirsql",
605
606
  "pyo3",
@@ -2451,6 +2452,7 @@ version = "1.0.149"
2451
2452
  source = "registry+https://github.com/rust-lang/crates.io-index"
2452
2453
  checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
2453
2454
  dependencies = [
2455
+ "indexmap",
2454
2456
  "itoa",
2455
2457
  "memchr",
2456
2458
  "serde",
@@ -16,7 +16,9 @@ repository = "https://github.com/thekevinscott/dirsql"
16
16
  [workspace.dependencies]
17
17
  dirsql = { path = "packages/rust" }
18
18
  serde = { version = "1", features = ["derive"] }
19
- serde_json = "1"
19
+ # `preserve_order`: a serialized row must keep the projection order the SELECT
20
+ # list asked for, which the default BTreeMap-backed object discards.
21
+ serde_json = { version = "1", features = ["preserve_order"] }
20
22
  regex = "1"
21
23
  thiserror = "2"
22
24
  tokio = { version = "1", features = ["rt", "sync"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.54
3
+ Version: 0.4.56
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
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress'
3
3
  export default defineConfig({
4
4
  title: 'dirsql',
5
5
  description: 'Ephemeral SQL index over a local directory. Watches a filesystem, ingests structured files into an ephemeral SQLite database, and exposes a SQL query interface.',
6
- base: '/dirsql/',
6
+ base: '/',
7
7
 
8
8
  // Vite blocks dev-server requests whose Host header isn't allowlisted, so
9
9
  // `pnpm dev --host` reached by a network hostname (e.g. a Tailscale
@@ -14,7 +14,7 @@ pnpm install
14
14
  pnpm dev
15
15
  ```
16
16
 
17
- This starts a local dev server (default: `http://localhost:5173/dirsql/`). The site hot-reloads on file changes.
17
+ This starts a local dev server (default: `http://localhost:5173/`). The site hot-reloads on file changes.
18
18
 
19
19
  ## Building
20
20
 
@@ -94,4 +94,4 @@ Working rules:
94
94
  - Use VitePress [code group](https://vitepress.dev/guide/markdown#code-groups) syntax (`::: code-group`) for multi-language examples with `Python`, `Rust`, and `TypeScript` tabs
95
95
  - Internal links use relative paths (e.g., `./howto/define-tables.md`)
96
96
  - The VitePress config is at `docs/.vitepress/config.ts`
97
- - The site is deployed under the `/dirsql/` base path
97
+ - The site is deployed at the root of `dirsql.dev`
@@ -239,6 +239,25 @@ than `Number.MAX_SAFE_INTEGER`, 2^53 − 1) cannot be represented as a JS
239
239
  than return a rounded value. Python's `int` is unbounded, so it always
240
240
  round-trips.
241
241
 
242
+ ### `queryOrdered`
243
+
244
+ ::: code-group
245
+
246
+ ```typescript [TypeScript]
247
+ await db.queryOrdered(sql: string) -> { columns: string[]; rows: Record<string, unknown>[] }
248
+ ```
249
+
250
+ ```rust [Rust]
251
+ db.query_ordered(sql: &str) -> Result<QueryResult> // QueryResult { columns, rows }
252
+ ```
253
+
254
+ :::
255
+
256
+ Runs `query` and also returns the column names in the order the SELECT list
257
+ names them. Use it to render columns in the order the caller asked for: a
258
+ row's keys do not carry that order. In TypeScript, object keys that look like
259
+ integers always enumerate first.
260
+
242
261
  ### `watch`
243
262
 
244
263
  ::: code-group
@@ -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.54"
7
+ version = "0.4.56"
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
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitepress'
3
3
  export default defineConfig({
4
4
  title: 'dirsql',
5
5
  description: 'Ephemeral SQL index over a local directory. Watches a filesystem, ingests structured files into an ephemeral SQLite database, and exposes a SQL query interface.',
6
- base: '/dirsql/',
6
+ base: '/',
7
7
 
8
8
  // Vite blocks dev-server requests whose Host header isn't allowlisted, so
9
9
  // `pnpm dev --host` reached by a network hostname (e.g. a Tailscale
@@ -14,7 +14,7 @@ pnpm install
14
14
  pnpm dev
15
15
  ```
16
16
 
17
- This starts a local dev server (default: `http://localhost:5173/dirsql/`). The site hot-reloads on file changes.
17
+ This starts a local dev server (default: `http://localhost:5173/`). The site hot-reloads on file changes.
18
18
 
19
19
  ## Building
20
20
 
@@ -94,4 +94,4 @@ Working rules:
94
94
  - Use VitePress [code group](https://vitepress.dev/guide/markdown#code-groups) syntax (`::: code-group`) for multi-language examples with `Python`, `Rust`, and `TypeScript` tabs
95
95
  - Internal links use relative paths (e.g., `./howto/define-tables.md`)
96
96
  - The VitePress config is at `docs/.vitepress/config.ts`
97
- - The site is deployed under the `/dirsql/` base path
97
+ - The site is deployed at the root of `dirsql.dev`
@@ -239,6 +239,25 @@ than `Number.MAX_SAFE_INTEGER`, 2^53 − 1) cannot be represented as a JS
239
239
  than return a rounded value. Python's `int` is unbounded, so it always
240
240
  round-trips.
241
241
 
242
+ ### `queryOrdered`
243
+
244
+ ::: code-group
245
+
246
+ ```typescript [TypeScript]
247
+ await db.queryOrdered(sql: string) -> { columns: string[]; rows: Record<string, unknown>[] }
248
+ ```
249
+
250
+ ```rust [Rust]
251
+ db.query_ordered(sql: &str) -> Result<QueryResult> // QueryResult { columns, rows }
252
+ ```
253
+
254
+ :::
255
+
256
+ Runs `query` and also returns the column names in the order the SELECT list
257
+ names them. Use it to render columns in the order the caller asked for: a
258
+ row's keys do not carry that order. In TypeScript, object keys that look like
259
+ integers always enumerate first.
260
+
242
261
  ### `watch`
243
262
 
244
263
  ::: code-group
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790181270,
4
+ "exit_code": 0,
5
+ "commit": "010d9772867fcb1e1b56ce4a77eb8b0223b0870a",
6
+ "branch": "claude/1170-projection-order"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790250389,
4
+ "exit_code": 0,
5
+ "commit": "b36c850be9c0cd4b06992b42fe2c689d38783757",
6
+ "branch": "claude/1172-on-file-path"
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.54"
9
+ version = "0.4.56"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -239,6 +239,25 @@ than `Number.MAX_SAFE_INTEGER`, 2^53 − 1) cannot be represented as a JS
239
239
  than return a rounded value. Python's `int` is unbounded, so it always
240
240
  round-trips.
241
241
 
242
+ ### `queryOrdered`
243
+
244
+ ::: code-group
245
+
246
+ ```typescript [TypeScript]
247
+ await db.queryOrdered(sql: string) -> { columns: string[]; rows: Record<string, unknown>[] }
248
+ ```
249
+
250
+ ```rust [Rust]
251
+ db.query_ordered(sql: &str) -> Result<QueryResult> // QueryResult { columns, rows }
252
+ ```
253
+
254
+ :::
255
+
256
+ Runs `query` and also returns the column names in the order the SELECT list
257
+ names them. Use it to render columns in the order the caller asked for: a
258
+ row's keys do not carry that order. In TypeScript, object keys that look like
259
+ integers always enumerate first.
260
+
242
261
  ### `watch`
243
262
 
244
263
  ::: code-group
@@ -15,7 +15,7 @@ use serde::Deserialize;
15
15
  use serde_json::Value;
16
16
 
17
17
  use super::AppState;
18
- use super::serialize::rows_to_json;
18
+ use super::serialize::result_to_json;
19
19
  use crate::{DirSQL, DirSqlError};
20
20
 
21
21
  /// Why a query failed, classified independently of transport. The HTTP
@@ -66,10 +66,14 @@ pub async fn execute_query(
66
66
 
67
67
  let db = require_ready(state)?;
68
68
 
69
- let join = run_bounded(timeout, tokio::task::spawn_blocking(move || db.query(&sql))).await?;
69
+ let join = run_bounded(
70
+ timeout,
71
+ tokio::task::spawn_blocking(move || db.query_ordered(&sql)),
72
+ )
73
+ .await?;
70
74
 
71
75
  match join {
72
- Ok(Ok(rows)) => Ok(Value::Array(rows_to_json(&rows))),
76
+ Ok(Ok(result)) => Ok(Value::Array(result_to_json(&result))),
73
77
  Ok(Err(err)) => Err(classify_query_error(err)),
74
78
  Err(join_err) => Err(QueryFailure::Internal(join_err.to_string())),
75
79
  }
@@ -2,13 +2,29 @@
2
2
 
3
3
  use serde_json::{Map, Value};
4
4
 
5
- use crate::Row;
6
5
  use crate::db::Value as CellValue;
7
6
  use crate::differ::RowEvent;
8
7
  use crate::row_event_flat::flatten_row_event;
8
+ use crate::{QueryResult, Row};
9
+
10
+ /// Serialize a query result, each object keyed in projection order. Row events
11
+ /// carry no projection, so they keep going through [`row_to_json`].
12
+ pub(super) fn result_to_json(result: &QueryResult) -> Vec<Value> {
13
+ result
14
+ .rows
15
+ .iter()
16
+ .map(|row| ordered_row_to_json(&result.columns, row))
17
+ .collect()
18
+ }
9
19
 
10
- pub(super) fn rows_to_json(rows: &[Row]) -> Vec<Value> {
11
- rows.iter().map(row_to_json).collect()
20
+ fn ordered_row_to_json(columns: &[String], row: &Row) -> Value {
21
+ let mut map = Map::with_capacity(columns.len());
22
+ for column in columns {
23
+ if let Some(value) = row.get(column) {
24
+ map.insert(column.clone(), cell_to_json(value));
25
+ }
26
+ }
27
+ Value::Object(map)
12
28
  }
13
29
 
14
30
  pub(super) fn row_to_json(row: &Row) -> Value {
@@ -75,6 +91,35 @@ mod tests {
75
91
  assert_eq!(json.get("count").and_then(Value::as_i64), Some(3));
76
92
  }
77
93
 
94
+ #[test]
95
+ fn a_result_serializes_its_rows_in_projection_order() {
96
+ let mut row: Row = HashMap::new();
97
+ row.insert("title".into(), CellValue::Text("Hello".into()));
98
+ row.insert("count".into(), CellValue::Integer(3));
99
+ let result = QueryResult {
100
+ columns: vec!["count".into(), "title".into()],
101
+ rows: vec![row],
102
+ };
103
+
104
+ let json = result_to_json(&result);
105
+
106
+ assert_eq!(json[0].to_string(), r#"{"count":3,"title":"Hello"}"#);
107
+ }
108
+
109
+ #[test]
110
+ fn a_column_no_row_key_matches_is_left_out() {
111
+ let mut row: Row = HashMap::new();
112
+ row.insert("title".into(), CellValue::Text("Hello".into()));
113
+ let result = QueryResult {
114
+ columns: vec!["title".into(), "title".into()],
115
+ rows: vec![row],
116
+ };
117
+
118
+ let json = result_to_json(&result);
119
+
120
+ assert_eq!(json[0].to_string(), r#"{"title":"Hello"}"#);
121
+ }
122
+
78
123
  #[test]
79
124
  fn insert_event_emits_expected_shape() {
80
125
  let mut row: Row = HashMap::new();
@@ -11,7 +11,7 @@
11
11
  //! swallowing the table; laying the table out to the terminal's width, and
12
12
  //! paging a long result, are both out of scope.
13
13
 
14
- use std::collections::BTreeSet;
14
+ use std::collections::HashSet;
15
15
 
16
16
  use serde_json::Value;
17
17
 
@@ -53,16 +53,22 @@ pub(super) fn render(rows: &[Value]) -> String {
53
53
  out
54
54
  }
55
55
 
56
- /// Every column any row carries. Rows are JSON objects, so the keys arrive
57
- /// sorted and stay in step with the JSON rendering of the same result.
56
+ /// Every column any row carries, first seen first. Rows are JSON objects
57
+ /// serialized in projection order, so that is the order the SELECT list asked
58
+ /// for -- and the same order the JSON rendering of this result prints.
58
59
  fn columns(rows: &[Value]) -> Vec<String> {
59
- let mut names = BTreeSet::new();
60
+ let mut seen = HashSet::new();
61
+ let mut names = Vec::new();
60
62
  for row in rows {
61
63
  if let Value::Object(map) = row {
62
- names.extend(map.keys().cloned());
64
+ for key in map.keys() {
65
+ if seen.insert(key.clone()) {
66
+ names.push(key.clone());
67
+ }
68
+ }
63
69
  }
64
70
  }
65
- names.into_iter().collect()
71
+ names
66
72
  }
67
73
 
68
74
  /// One row's cells, in column order. A row missing a column renders it as
@@ -242,6 +248,25 @@ mod tests {
242
248
  }
243
249
  }
244
250
 
251
+ #[test]
252
+ fn the_header_keeps_the_order_the_row_carries() {
253
+ // Rows arrive serialized in projection order; re-sorting them here is
254
+ // what made `SELECT size, basename` print `basename size`.
255
+ let table = rendered([row([
256
+ ("size", Value::from(6)),
257
+ ("basename", Value::from("a.md")),
258
+ ])]);
259
+
260
+ assert!(table.starts_with("size basename\n"), "{table:?}");
261
+ }
262
+
263
+ #[test]
264
+ fn a_later_rows_new_columns_come_after_the_earlier_ones() {
265
+ let table = rendered([row([("b", Value::from(1))]), row([("a", Value::from(2))])]);
266
+
267
+ assert!(table.starts_with("b a\n"), "{table:?}");
268
+ }
269
+
245
270
  #[test]
246
271
  fn every_column_any_row_carries_is_shown() {
247
272
  // A ragged result must not silently drop the columns only some rows
@@ -26,7 +26,7 @@
26
26
  //! - **Errors.** A non-zero exit is a failure carrying the tail of stderr.
27
27
 
28
28
  use std::io::{Read, Write};
29
- use std::path::Path;
29
+ use std::path::{Path, PathBuf};
30
30
  use std::process::{Command, Stdio};
31
31
 
32
32
  /// A named placeholder substituted into a command's argv.
@@ -66,14 +66,22 @@ pub enum CommandError {
66
66
  #[error("invalid command template: {0:?}")]
67
67
  InvalidCommand(String),
68
68
 
69
- /// The child could not be spawned (e.g. the program was not found).
70
- #[error("failed to spawn `{command}`: {source}")]
69
+ /// The child could not be spawned.
70
+ #[error("failed to spawn `{program}`: {source}")]
71
71
  Spawn {
72
- command: String,
72
+ program: String,
73
73
  #[source]
74
74
  source: std::io::Error,
75
75
  },
76
76
 
77
+ /// A program named without a `/` was not found on `$PATH`. `local` is the
78
+ /// file of that name in the child's cwd, which a bare name never reaches.
79
+ #[error("failed to spawn `{program}`: not found on $PATH{}", local_hint(.program, .local.as_deref()))]
80
+ NotOnPath {
81
+ program: String,
82
+ local: Option<PathBuf>,
83
+ },
84
+
77
85
  /// The child exited with a non-zero status. `code` is the exit code, or
78
86
  /// `"signal"` when the child was terminated by a signal.
79
87
  #[error("command `{command}` failed (exit {code}): {stderr_tail}")]
@@ -121,10 +129,9 @@ pub fn run_command(
121
129
  Stdio::null()
122
130
  });
123
131
 
124
- let mut child = cmd.spawn().map_err(|source| CommandError::Spawn {
125
- command: command.to_string(),
126
- source,
127
- })?;
132
+ let mut child = cmd
133
+ .spawn()
134
+ .map_err(|source| spawn_error(&argv[0], source, cwd))?;
128
135
 
129
136
  // Feed stdin and drain stdout/stderr on their own threads. Draining the
130
137
  // output pipes concurrently is required: a child that writes more than a
@@ -186,6 +193,37 @@ pub fn run_command(
186
193
  }
187
194
  }
188
195
 
196
+ fn spawn_error(program: &str, source: std::io::Error, cwd: &Path) -> CommandError {
197
+ // A missing cwd fails the child's chdir with `NotFound` too.
198
+ if !searched_path(program, source.kind()) || !cwd.is_dir() {
199
+ return CommandError::Spawn {
200
+ program: program.to_string(),
201
+ source,
202
+ };
203
+ }
204
+ let candidate = cwd.join(program);
205
+ CommandError::NotOnPath {
206
+ program: program.to_string(),
207
+ local: candidate.is_file().then_some(candidate),
208
+ }
209
+ }
210
+
211
+ /// A `NotFound` for a name without a `/` means the `$PATH` search came up
212
+ /// empty (`execvp` semantics); with a `/` the name was a path.
213
+ fn searched_path(program: &str, kind: std::io::ErrorKind) -> bool {
214
+ kind == std::io::ErrorKind::NotFound && !program.contains('/')
215
+ }
216
+
217
+ fn local_hint(program: &str, local: Option<&Path>) -> String {
218
+ match local {
219
+ Some(path) => format!(
220
+ ". Names without a `/` are resolved against $PATH; use `./{program}` to run `{}`",
221
+ path.display()
222
+ ),
223
+ None => String::new(),
224
+ }
225
+ }
226
+
189
227
  /// Split a command template into argv (shell-like quoting, no shell) and
190
228
  /// substitute placeholders into whole tokens.
191
229
  pub(crate) fn build_argv(
@@ -444,8 +482,65 @@ mod tests {
444
482
  }
445
483
 
446
484
  #[test]
447
- fn run_command_reports_spawn_failure_for_a_missing_program() {
485
+ fn run_command_reports_a_bare_missing_program_as_not_on_path() {
448
486
  let err = run_command("definitely-not-a-real-binary-xyzzy", &[], &cwd(), None).unwrap_err();
487
+ assert!(
488
+ matches!(err, CommandError::NotOnPath { local: None, .. }),
489
+ "got: {err:?}"
490
+ );
491
+ }
492
+
493
+ #[test]
494
+ fn run_command_reports_a_missing_path_as_a_plain_spawn_error() {
495
+ let err =
496
+ run_command("./definitely-not-a-real-binary-xyzzy", &[], &cwd(), None).unwrap_err();
449
497
  assert!(matches!(err, CommandError::Spawn { .. }), "got: {err:?}");
450
498
  }
499
+
500
+ #[test]
501
+ fn a_bare_not_found_name_searched_path() {
502
+ assert!(searched_path("extract.py", std::io::ErrorKind::NotFound));
503
+ }
504
+
505
+ #[test]
506
+ fn a_name_with_a_slash_did_not_search_path() {
507
+ assert!(!searched_path("./extract.py", std::io::ErrorKind::NotFound));
508
+ assert!(!searched_path(
509
+ "bin/extract.py",
510
+ std::io::ErrorKind::NotFound
511
+ ));
512
+ }
513
+
514
+ #[test]
515
+ fn a_non_not_found_error_did_not_search_path() {
516
+ assert!(!searched_path(
517
+ "extract.py",
518
+ std::io::ErrorKind::PermissionDenied
519
+ ));
520
+ }
521
+
522
+ #[test]
523
+ fn not_on_path_without_a_local_file_offers_no_hint() {
524
+ let err = CommandError::NotOnPath {
525
+ program: "extract.py".to_string(),
526
+ local: None,
527
+ };
528
+ assert_eq!(
529
+ err.to_string(),
530
+ "failed to spawn `extract.py`: not found on $PATH"
531
+ );
532
+ }
533
+
534
+ #[test]
535
+ fn not_on_path_with_a_local_file_offers_the_dot_slash_form() {
536
+ let err = CommandError::NotOnPath {
537
+ program: "extract.py".to_string(),
538
+ local: Some(PathBuf::from("/notes/extract.py")),
539
+ };
540
+ assert_eq!(
541
+ err.to_string(),
542
+ "failed to spawn `extract.py`: not found on $PATH. Names without a `/` are \
543
+ resolved against $PATH; use `./extract.py` to run `/notes/extract.py`"
544
+ );
545
+ }
451
546
  }
@@ -244,6 +244,13 @@ pub fn ensure_internal_rows_table(conn: &Connection) -> rusqlite::Result<()> {
244
244
  )
245
245
  }
246
246
 
247
+ /// One query's rows plus the column order the SELECT list asked for.
248
+ #[derive(Debug, Clone, PartialEq)]
249
+ pub struct QueryResult {
250
+ pub columns: Vec<String>,
251
+ pub rows: Vec<HashMap<String, Value>>,
252
+ }
253
+
247
254
  pub struct Db {
248
255
  conn: Connection,
249
256
  /// The directory a path-table's glob is resolved against. `None` disables
@@ -714,6 +721,14 @@ impl Db {
714
721
  /// internal writes (`insert_row`, delete-by-file, persist), which never go
715
722
  /// through `query()`, are unaffected.
716
723
  pub fn query(&self, sql: &str) -> Result<Vec<HashMap<String, Value>>> {
724
+ self.query_ordered(sql).map(|result| result.rows)
725
+ }
726
+
727
+ /// [`query`](Self::query), plus the projection order. A row is a
728
+ /// `HashMap`, so the order the SELECT list asked for survives only in
729
+ /// [`QueryResult::columns`]; a renderer that re-derives its columns from
730
+ /// row keys prints them alphabetically instead.
731
+ pub fn query_ordered(&self, sql: &str) -> Result<QueryResult> {
717
732
  // Bracket the whole statement, prepare included: a worker-backed
718
733
  // function called on every row makes this the slowest thing dirsql
719
734
  // does, and the guard is what erases the counter before the caller
@@ -757,7 +772,10 @@ impl Db {
757
772
  for row in rows {
758
773
  results.push(row?);
759
774
  }
760
- Ok(results)
775
+ Ok(QueryResult {
776
+ columns: column_names,
777
+ rows: results,
778
+ })
761
779
  }
762
780
 
763
781
  /// Prepare `sql` with the internal-table / ATTACH authorizer installed for
@@ -1026,6 +1044,40 @@ mod tests {
1026
1044
  assert!(!rows[0].contains_key("_dirsql_row_index"));
1027
1045
  }
1028
1046
 
1047
+ #[test]
1048
+ fn query_ordered_reports_the_select_list_order() {
1049
+ let db = Db::new().unwrap();
1050
+ db.create_table("docs", "CREATE TABLE docs (title TEXT, draft INTEGER)")
1051
+ .unwrap();
1052
+
1053
+ let result = db.query_ordered("SELECT draft, title FROM docs").unwrap();
1054
+
1055
+ assert_eq!(
1056
+ result.columns,
1057
+ vec!["draft".to_string(), "title".to_string()]
1058
+ );
1059
+ }
1060
+
1061
+ #[test]
1062
+ fn query_ordered_carries_the_same_rows_query_returns() {
1063
+ let db = Db::new().unwrap();
1064
+ db.create_table("docs", "CREATE TABLE docs (title TEXT, draft INTEGER)")
1065
+ .unwrap();
1066
+ let row = HashMap::from([
1067
+ ("title".into(), Value::Text("Hello".into())),
1068
+ ("draft".into(), Value::Integer(0)),
1069
+ ]);
1070
+ db.insert_row("docs", &row, "docs/hello.md", 0).unwrap();
1071
+
1072
+ let result = db.query_ordered("SELECT title, draft FROM docs").unwrap();
1073
+
1074
+ assert_eq!(
1075
+ result.rows,
1076
+ db.query("SELECT title, draft FROM docs").unwrap()
1077
+ );
1078
+ assert_eq!(result.rows[0]["title"], Value::Text("Hello".into()));
1079
+ }
1080
+
1029
1081
  #[test]
1030
1082
  fn pragma_table_info_matches_user_ddl_exactly() {
1031
1083
  let db = Db::new().unwrap();
@@ -77,7 +77,7 @@ use std::time::Duration;
77
77
  use thiserror::Error;
78
78
 
79
79
  pub use crate::config::ExtensionSpec as Extension;
80
- pub use crate::db::{DbError, Value};
80
+ pub use crate::db::{DbError, QueryResult, Value};
81
81
  pub use crate::differ::RowEvent;
82
82
  #[doc(hidden)]
83
83
  pub use crate::row_event_flat::{FlatRowEvent, flatten_row_event};
@@ -404,8 +404,15 @@ impl DirSQL {
404
404
  /// keeps the ephemeral index consistent with the on-disk files that back
405
405
  /// it: mutations only happen through the watcher/indexer pipeline.
406
406
  pub fn query(&self, sql: &str) -> Result<Vec<Row>> {
407
+ self.query_ordered(sql).map(|result| result.rows)
408
+ }
409
+
410
+ /// [`query`](Self::query), plus the projection order in
411
+ /// [`QueryResult::columns`]. A [`Row`] is a `HashMap`, so a caller that
412
+ /// renders columns in the order the user asked for needs this instead.
413
+ pub fn query_ordered(&self, sql: &str) -> Result<QueryResult> {
407
414
  let db = self.inner.db.lock().map_err(DirSqlError::lock)?;
408
- db.query(sql).map_err(map_db_error)
415
+ db.query_ordered(sql).map_err(map_db_error)
409
416
  }
410
417
 
411
418
  /// Lazily create the filesystem watcher. Idempotent; subsequent calls are
File without changes
File without changes
File without changes
File without changes
File without changes