dirsql 0.4.55__tar.gz → 0.4.57__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 (250) hide show
  1. {dirsql-0.4.55 → dirsql-0.4.57}/Cargo.lock +3 -2
  2. {dirsql-0.4.55 → dirsql-0.4.57}/PKG-INFO +1 -1
  3. {dirsql-0.4.55 → dirsql-0.4.57}/docs/.vitepress/config.ts +1 -1
  4. {dirsql-0.4.55 → dirsql-0.4.57}/docs/AGENTS.md +2 -2
  5. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/sdk.md +22 -0
  6. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/Cargo.toml +1 -1
  7. dirsql-0.4.57/packages/python/changelog.d/2026-09-24-query-rows-in-projection-order.md +1 -0
  8. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/.vitepress/config.ts +1 -1
  9. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/AGENTS.md +2 -2
  10. {dirsql-0.4.55/packages/rust → dirsql-0.4.57/packages/python}/docs/reference/sdk.md +22 -0
  11. dirsql-0.4.57/packages/python/e2e-attestations/claude-1172-on-file-path.json +7 -0
  12. dirsql-0.4.57/packages/python/e2e-attestations/claude-1173-py-projection-order.json +7 -0
  13. dirsql-0.4.57/packages/python/migrations.d/2026-09-24-query-rows-in-projection-order.md +36 -0
  14. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/src/lib.rs +17 -3
  15. dirsql-0.4.57/packages/python/tests/e2e/speed_of_light.py +124 -0
  16. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/Cargo.toml +1 -1
  17. {dirsql-0.4.55/packages/python → dirsql-0.4.57/packages/rust}/docs/reference/sdk.md +22 -0
  18. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/command.rs +104 -9
  19. {dirsql-0.4.55 → dirsql-0.4.57}/Cargo.toml +0 -0
  20. {dirsql-0.4.55 → dirsql-0.4.57}/README.md +0 -0
  21. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/__init__.py +0 -0
  22. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/_async.py +0 -0
  23. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/_dirsql.pyi +0 -0
  24. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/__init__.py +0 -0
  25. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/discover_plugins/__init__.py +0 -0
  26. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  27. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  28. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  29. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  30. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  31. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/main.py +0 -0
  32. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/cli/resolve_config_extensions.py +0 -0
  33. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/plan_entry_path.py +0 -0
  34. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/py.typed +0 -0
  35. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/read_config.py +0 -0
  36. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/resolve_configs_extension_specs.py +0 -0
  37. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/resolve_extension.py +0 -0
  38. {dirsql-0.4.55 → dirsql-0.4.57}/dirsql/resolve_package.py +0 -0
  39. {dirsql-0.4.55 → dirsql-0.4.57}/docs/.claude/CLAUDE.md +0 -0
  40. {dirsql-0.4.55 → dirsql-0.4.57}/docs/.vitepress/theme/index.ts +0 -0
  41. {dirsql-0.4.55 → dirsql-0.4.57}/docs/.vitepress/theme/lang.ts +0 -0
  42. {dirsql-0.4.55 → dirsql-0.4.57}/docs/explanation.md +0 -0
  43. {dirsql-0.4.55 → dirsql-0.4.57}/docs/getting-started.md +0 -0
  44. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/columns-from-paths.md +0 -0
  45. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/define-tables.md +0 -0
  46. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/embed.md +0 -0
  47. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/extract-from-contents.md +0 -0
  48. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/load-extension.md +0 -0
  49. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/parse-files-into-columns.md +0 -0
  50. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/persist.md +0 -0
  51. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/query-json.md +0 -0
  52. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/query-without-config.md +0 -0
  53. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/react-to-changes.md +0 -0
  54. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/search-by-meaning.md +0 -0
  55. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/search-indexes.md +0 -0
  56. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/skip-files.md +0 -0
  57. {dirsql-0.4.55 → dirsql-0.4.57}/docs/howto/write-a-plugin.md +0 -0
  58. {dirsql-0.4.55 → dirsql-0.4.57}/docs/index.md +0 -0
  59. {dirsql-0.4.55 → dirsql-0.4.57}/docs/package.json +0 -0
  60. {dirsql-0.4.55 → dirsql-0.4.57}/docs/plugins.md +0 -0
  61. {dirsql-0.4.55 → dirsql-0.4.57}/docs/pnpm-lock.yaml +0 -0
  62. {dirsql-0.4.55 → dirsql-0.4.57}/docs/pnpm-workspace.yaml +0 -0
  63. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/cli.md +0 -0
  64. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/columns.md +0 -0
  65. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/config.md +0 -0
  66. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/hooks.md +0 -0
  67. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/http-api.md +0 -0
  68. {dirsql-0.4.55 → dirsql-0.4.57}/docs/reference/path-tables.md +0 -0
  69. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/CHANGELOG.md +0 -0
  70. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/MIGRATIONS.md +0 -0
  71. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/README.md +0 -0
  72. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  73. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  74. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  75. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  76. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  77. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  78. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  79. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  80. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  81. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  82. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  83. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  84. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  85. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  86. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  87. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  88. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  89. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  90. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  91. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  92. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  93. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  94. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  95. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  96. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
  97. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-18-export-scan-failure.md +0 -0
  98. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +0 -0
  99. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
  100. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  101. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/changelog.d/README.md +0 -0
  102. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/conftest.py +0 -0
  103. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/.claude/CLAUDE.md +0 -0
  104. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  105. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  106. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/explanation.md +0 -0
  107. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/getting-started.md +0 -0
  108. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/columns-from-paths.md +0 -0
  109. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/define-tables.md +0 -0
  110. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/embed.md +0 -0
  111. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/extract-from-contents.md +0 -0
  112. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/load-extension.md +0 -0
  113. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  114. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/persist.md +0 -0
  115. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/query-json.md +0 -0
  116. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/query-without-config.md +0 -0
  117. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/react-to-changes.md +0 -0
  118. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/search-by-meaning.md +0 -0
  119. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/search-indexes.md +0 -0
  120. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/skip-files.md +0 -0
  121. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/howto/write-a-plugin.md +0 -0
  122. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/index.md +0 -0
  123. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/package.json +0 -0
  124. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/plugins.md +0 -0
  125. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/pnpm-lock.yaml +0 -0
  126. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/pnpm-workspace.yaml +0 -0
  127. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/reference/cli.md +0 -0
  128. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/reference/columns.md +0 -0
  129. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/reference/config.md +0 -0
  130. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/reference/hooks.md +0 -0
  131. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/reference/http-api.md +0 -0
  132. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/docs/reference/path-tables.md +0 -0
  133. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  134. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  135. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  136. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  137. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  138. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
  139. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
  140. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  141. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
  142. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
  143. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
  144. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
  145. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1130-codec-crates.json +0 -0
  146. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +0 -0
  147. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1139-sse-flatten.json +0 -0
  148. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1140-scan-failure-export.json +0 -0
  149. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1148-clippy-all-targets.json +0 -0
  150. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1164-sigint-fatal.json +0 -0
  151. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-1170-projection-order.json +0 -0
  152. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  153. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  154. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  155. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  156. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  157. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  158. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  159. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  160. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  161. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  162. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  163. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  164. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  165. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  166. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  167. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  168. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  169. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  170. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  171. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  172. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  173. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  174. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  175. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  176. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  177. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/2026-09-20-sigint-ends-a-run-in-progress.md +0 -0
  178. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/migrations.d/README.md +0 -0
  179. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/testing-conventions.toml +0 -0
  180. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/__init__.py +0 -0
  181. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/conftest.py +0 -0
  182. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/e2e/__init__.py +0 -0
  183. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  184. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  185. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/integration/__init__.py +0 -0
  186. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/integration/binding/__init__.py +0 -0
  187. {dirsql-0.4.55 → dirsql-0.4.57}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  188. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/CHANGELOG.md +0 -0
  189. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/MIGRATIONS.md +0 -0
  190. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/README.md +0 -0
  191. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/benches/db_bench.rs +0 -0
  192. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/benches/differ_bench.rs +0 -0
  193. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/benches/matcher_bench.rs +0 -0
  194. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/benches/scanner_bench.rs +0 -0
  195. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/explanation.md +0 -0
  196. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/getting-started.md +0 -0
  197. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  198. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/define-tables.md +0 -0
  199. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/embed.md +0 -0
  200. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  201. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/load-extension.md +0 -0
  202. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  203. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/persist.md +0 -0
  204. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/query-json.md +0 -0
  205. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/query-without-config.md +0 -0
  206. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/react-to-changes.md +0 -0
  207. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  208. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/search-indexes.md +0 -0
  209. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/skip-files.md +0 -0
  210. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  211. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/index.md +0 -0
  212. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/plugins.md +0 -0
  213. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/reference/cli.md +0 -0
  214. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/reference/columns.md +0 -0
  215. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/reference/config.md +0 -0
  216. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/reference/hooks.md +0 -0
  217. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/reference/http-api.md +0 -0
  218. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/docs/reference/path-tables.md +0 -0
  219. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/bin/dirsql.rs +0 -0
  220. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/execute.rs +0 -0
  221. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/init.rs +0 -0
  222. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/mod.rs +0 -0
  223. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/repl.rs +0 -0
  224. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/router.rs +0 -0
  225. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/run.rs +0 -0
  226. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/serialize.rs +0 -0
  227. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/server.rs +0 -0
  228. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/cli/table.rs +0 -0
  229. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/config.rs +0 -0
  230. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/db.rs +0 -0
  231. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/default_config.toml +0 -0
  232. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/differ.rs +0 -0
  233. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/extension_resolution.rs +0 -0
  234. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/functions.rs +0 -0
  235. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/infer.rs +0 -0
  236. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/launcher.rs +0 -0
  237. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/lib.rs +0 -0
  238. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/matcher.rs +0 -0
  239. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/parsed_cache.rs +0 -0
  240. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/parsed_vtab.rs +0 -0
  241. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/path_table.rs +0 -0
  242. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/persist.rs +0 -0
  243. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/progress.rs +0 -0
  244. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/row_event_flat.rs +0 -0
  245. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/scanner.rs +0 -0
  246. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/sql_literal.rs +0 -0
  247. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/vtab.rs +0 -0
  248. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/vtab_scaffold.rs +0 -0
  249. {dirsql-0.4.55 → dirsql-0.4.57}/packages/rust/src/watcher.rs +0 -0
  250. {dirsql-0.4.55 → dirsql-0.4.57}/pyproject.toml +0 -0
@@ -552,7 +552,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
552
552
 
553
553
  [[package]]
554
554
  name = "dirsql"
555
- version = "0.4.55"
555
+ version = "0.4.57"
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.55"
603
+ version = "0.4.57"
603
604
  dependencies = [
604
605
  "dirsql",
605
606
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.55
3
+ Version: 0.4.57
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`
@@ -223,6 +223,9 @@ Executes a SQL query and returns rows keyed by column name.
223
223
  [Read-only by design](../explanation#read-only-by-design).
224
224
  - Internal tracking columns (`_dirsql_file_path`, `_dirsql_row_index`) are
225
225
  excluded from `SELECT *` results; name them explicitly to see them.
226
+ - **Column order.** Python row dicts are keyed in the order the SELECT list
227
+ names the columns. Rust and TypeScript rows do not carry that order; use
228
+ [`queryOrdered`](#queryordered).
226
229
  - SQLite values map back to language types:
227
230
 
228
231
  | SQLite | Python | TypeScript | Rust |
@@ -239,6 +242,25 @@ than `Number.MAX_SAFE_INTEGER`, 2^53 − 1) cannot be represented as a JS
239
242
  than return a rounded value. Python's `int` is unbounded, so it always
240
243
  round-trips.
241
244
 
245
+ ### `queryOrdered`
246
+
247
+ ::: code-group
248
+
249
+ ```typescript [TypeScript]
250
+ await db.queryOrdered(sql: string) -> { columns: string[]; rows: Record<string, unknown>[] }
251
+ ```
252
+
253
+ ```rust [Rust]
254
+ db.query_ordered(sql: &str) -> Result<QueryResult> // QueryResult { columns, rows }
255
+ ```
256
+
257
+ :::
258
+
259
+ Runs `query` and also returns the column names in the order the SELECT list
260
+ names them. Use it to render columns in the order the caller asked for: a
261
+ row's keys do not carry that order. In TypeScript, object keys that look like
262
+ integers always enumerate first.
263
+
242
264
  ### `watch`
243
265
 
244
266
  ::: 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.55"
7
+ version = "0.4.57"
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 @@
1
+ **Fixed** `db.query()` rows are now dicts keyed in the order the SELECT list names the columns, so `list(row)` gives the projection order (`SELECT *` follows the table's declared column order). Keys previously came out in the core's hash-map order, which varied from run to run, so a caller rendering columns had no order to follow.
@@ -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`
@@ -223,6 +223,9 @@ Executes a SQL query and returns rows keyed by column name.
223
223
  [Read-only by design](../explanation#read-only-by-design).
224
224
  - Internal tracking columns (`_dirsql_file_path`, `_dirsql_row_index`) are
225
225
  excluded from `SELECT *` results; name them explicitly to see them.
226
+ - **Column order.** Python row dicts are keyed in the order the SELECT list
227
+ names the columns. Rust and TypeScript rows do not carry that order; use
228
+ [`queryOrdered`](#queryordered).
226
229
  - SQLite values map back to language types:
227
230
 
228
231
  | SQLite | Python | TypeScript | Rust |
@@ -239,6 +242,25 @@ than `Number.MAX_SAFE_INTEGER`, 2^53 − 1) cannot be represented as a JS
239
242
  than return a rounded value. Python's `int` is unbounded, so it always
240
243
  round-trips.
241
244
 
245
+ ### `queryOrdered`
246
+
247
+ ::: code-group
248
+
249
+ ```typescript [TypeScript]
250
+ await db.queryOrdered(sql: string) -> { columns: string[]; rows: Record<string, unknown>[] }
251
+ ```
252
+
253
+ ```rust [Rust]
254
+ db.query_ordered(sql: &str) -> Result<QueryResult> // QueryResult { columns, rows }
255
+ ```
256
+
257
+ :::
258
+
259
+ Runs `query` and also returns the column names in the order the SELECT list
260
+ names them. Use it to render columns in the order the caller asked for: a
261
+ row's keys do not carry that order. In TypeScript, object keys that look like
262
+ integers always enumerate first.
263
+
242
264
  ### `watch`
243
265
 
244
266
  ::: code-group
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1790283783,
4
+ "exit_code": 0,
5
+ "commit": "2fb22b61a9db9022d3c3adbdf7b7373530de091c",
6
+ "branch": "claude/1173-py-projection-order"
7
+ }
@@ -0,0 +1,36 @@
1
+ ### `db.query()` rows are keyed in projection order
2
+
3
+ **Summary**
4
+
5
+ Each dict `db.query()` returns is now keyed in the order the SELECT list names the columns; `SELECT *` follows the table's declared column order. The keys previously came out in the core's hash-map order, which varied from process to process. No signature changed.
6
+
7
+ **Required changes**
8
+
9
+ _None._
10
+
11
+ **Deprecations removed**
12
+
13
+ _None._
14
+
15
+ **Behavior changes without code changes**
16
+
17
+ | Call | Before | After |
18
+ | --- | --- | --- |
19
+ | `await db.query("SELECT path, size FROM './'")` | keys in arbitrary order, e.g. `{'size': 6, 'path': 'a.md'}` | `{'path': 'a.md', 'size': 6}` |
20
+
21
+ The old order was never stable, so no caller could have depended on it. Code that sorted the keys to get a stable order can drop the sort if it wants the order the query asked for.
22
+
23
+ **Verification**
24
+
25
+ ```python
26
+ import asyncio
27
+ from dirsql import DirSQL
28
+
29
+ async def main():
30
+ db = DirSQL(".")
31
+ print(await db.query("SELECT size, path FROM './' LIMIT 1"))
32
+
33
+ asyncio.run(main())
34
+ ```
35
+
36
+ Expected: `[{'size': ..., 'path': ...}]`, `size` first as written.
@@ -193,11 +193,13 @@ mod python {
193
193
 
194
194
  fn query(&self, py: Python<'_>, sql: String) -> PyResult<Py<PyList>> {
195
195
  let db = self.inner.clone();
196
- let rows = py.detach(move || db.query(&sql)).map_err(to_py_err)?;
196
+ let result = py
197
+ .detach(move || db.query_ordered(&sql))
198
+ .map_err(to_py_err)?;
197
199
 
198
200
  let list = PyList::empty(py);
199
- for row in rows {
200
- list.append(value_row_to_py_dict(py, &row)?)?;
201
+ for row in &result.rows {
202
+ list.append(ordered_row_to_py_dict(py, &result.columns, row)?)?;
201
203
  }
202
204
  Ok(list.unbind())
203
205
  }
@@ -302,6 +304,18 @@ mod python {
302
304
  Ok(dict.unbind())
303
305
  }
304
306
 
307
+ fn ordered_row_to_py_dict(
308
+ py: Python<'_>,
309
+ columns: &[String],
310
+ row: &Row,
311
+ ) -> PyResult<Py<PyDict>> {
312
+ let dict = PyDict::new(py);
313
+ for column in columns {
314
+ dict.set_item(column, value_to_py(py, &row[column]))?;
315
+ }
316
+ Ok(dict.unbind())
317
+ }
318
+
305
319
  fn convert_py_row(
306
320
  py: Python<'_>,
307
321
  py_row: &HashMap<String, Py<PyAny>>,
@@ -0,0 +1,124 @@
1
+ """Harness for the use-case speed-of-light tests.
2
+
3
+ Each use-case test asks one question of a generated corpus twice: once through
4
+ the fastest native pipeline (find, sort, join, awk, a single parser process)
5
+ and once through the real `dirsql` launcher. The rows must be identical and
6
+ dirsql's wall time, after subtracting the launcher's measured startup, must be
7
+ within ten percent of native. The bar is a ratio, never a millisecond number,
8
+ so it holds on any machine.
9
+
10
+ Startup is the mean of ten `dirsql --help` runs; `--help` and `SELECT 1` cost
11
+ the same, so it stands in for the fixed per-invocation cost. A corpus too
12
+ small to time is grown by doubling until the native pipeline takes at least a
13
+ second, so the comparison is above the noise floor. Each side is run three
14
+ times and the fastest run counts.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import shutil
21
+ import subprocess
22
+ import time
23
+
24
+ ONE_SECOND = 1.0
25
+ TOLERANCE = 1.1
26
+ STARTUP_PROBES = 10
27
+ BEST_OF = 3
28
+
29
+
30
+ def cli() -> str:
31
+ dirsql = shutil.which("dirsql")
32
+ assert dirsql is not None, (
33
+ "`dirsql` console script not on PATH -- run `uv run maturin develop`"
34
+ )
35
+ return dirsql
36
+
37
+
38
+ def timed(argv, cwd, timeout, env=None):
39
+ started = time.perf_counter()
40
+ proc = subprocess.run(
41
+ argv,
42
+ cwd=str(cwd),
43
+ stdin=subprocess.DEVNULL,
44
+ capture_output=True,
45
+ text=True,
46
+ timeout=timeout,
47
+ env=env,
48
+ check=False,
49
+ )
50
+ return proc, time.perf_counter() - started
51
+
52
+
53
+ def startup_seconds(argv=None, cwd=None) -> float:
54
+ argv = [cli(), "--help"] if argv is None else argv
55
+ samples = []
56
+ for _ in range(STARTUP_PROBES):
57
+ proc, seconds = timed(argv, cwd or ".", timeout=60)
58
+ assert proc.returncode == 0, proc.stderr
59
+ samples.append(seconds)
60
+ return sum(samples) / len(samples)
61
+
62
+
63
+ def dirsql_rows(proc, columns):
64
+ assert proc.returncode == 0, (
65
+ f"dirsql failed: stdout={proc.stdout!r} stderr={proc.stderr!r}"
66
+ )
67
+ return [tuple(row[column] for column in columns) for row in json.loads(proc.stdout)]
68
+
69
+
70
+ def grow_until_native_takes_a_second(build, native, start, ceiling):
71
+ """Grow the corpus by doubling until `native` runs for a second.
72
+
73
+ `build(lo, hi)` adds items `lo` through `hi - 1` to the corpus already
74
+ holding `lo` items, so each doubling writes only the new half.
75
+ """
76
+ built, size = 0, start
77
+ while True:
78
+ build(built, size)
79
+ built = size
80
+ _, seconds = native()
81
+ if seconds >= ONE_SECOND or size >= ceiling:
82
+ return size
83
+ size *= 2
84
+
85
+
86
+ def best_of(run, name, hopeless=None):
87
+ """Run `run` three times; return the rows and wall time of the fastest.
88
+
89
+ `run` returns `(rows, seconds)`. A run that raises `TimeoutExpired` fails
90
+ the test immediately with the timeout it blew through. A run slower than
91
+ `hopeless` seconds ends the series early: noise does not account for an
92
+ order of magnitude, so no retry could bring the best under the bar.
93
+ """
94
+ fastest = None
95
+ for _ in range(BEST_OF):
96
+ try:
97
+ rows, seconds = run()
98
+ except subprocess.TimeoutExpired as expired:
99
+ raise AssertionError(
100
+ f"{name} did not finish within {expired.timeout:.0f}s"
101
+ ) from None
102
+ if fastest is None or seconds < fastest[1]:
103
+ fastest = (rows, seconds)
104
+ if hopeless is not None and seconds > hopeless:
105
+ break
106
+ return fastest
107
+
108
+
109
+ def hopeless_seconds(native_seconds: float, startup: float) -> float:
110
+ return startup + 10 * TOLERANCE * native_seconds
111
+
112
+
113
+ def dirsql_timeout(native_seconds: float) -> float:
114
+ return max(60.0, 30 * native_seconds)
115
+
116
+
117
+ def assert_speed_of_light(name, native_seconds, dirsql_seconds, startup):
118
+ measured = dirsql_seconds - startup
119
+ budget = TOLERANCE * native_seconds
120
+ assert measured <= budget, (
121
+ f"{name}: dirsql {dirsql_seconds:.3f}s minus startup {startup:.3f}s"
122
+ f" = {measured:.3f}s, over {TOLERANCE}x native {native_seconds:.3f}s"
123
+ f" = {budget:.3f}s"
124
+ )
@@ -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.55"
9
+ version = "0.4.57"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -223,6 +223,9 @@ Executes a SQL query and returns rows keyed by column name.
223
223
  [Read-only by design](../explanation#read-only-by-design).
224
224
  - Internal tracking columns (`_dirsql_file_path`, `_dirsql_row_index`) are
225
225
  excluded from `SELECT *` results; name them explicitly to see them.
226
+ - **Column order.** Python row dicts are keyed in the order the SELECT list
227
+ names the columns. Rust and TypeScript rows do not carry that order; use
228
+ [`queryOrdered`](#queryordered).
226
229
  - SQLite values map back to language types:
227
230
 
228
231
  | SQLite | Python | TypeScript | Rust |
@@ -239,6 +242,25 @@ than `Number.MAX_SAFE_INTEGER`, 2^53 − 1) cannot be represented as a JS
239
242
  than return a rounded value. Python's `int` is unbounded, so it always
240
243
  round-trips.
241
244
 
245
+ ### `queryOrdered`
246
+
247
+ ::: code-group
248
+
249
+ ```typescript [TypeScript]
250
+ await db.queryOrdered(sql: string) -> { columns: string[]; rows: Record<string, unknown>[] }
251
+ ```
252
+
253
+ ```rust [Rust]
254
+ db.query_ordered(sql: &str) -> Result<QueryResult> // QueryResult { columns, rows }
255
+ ```
256
+
257
+ :::
258
+
259
+ Runs `query` and also returns the column names in the order the SELECT list
260
+ names them. Use it to render columns in the order the caller asked for: a
261
+ row's keys do not carry that order. In TypeScript, object keys that look like
262
+ integers always enumerate first.
263
+
242
264
  ### `watch`
243
265
 
244
266
  ::: code-group
@@ -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
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes