dirsql 0.4.34__tar.gz → 0.4.35__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 (208) hide show
  1. {dirsql-0.4.34 → dirsql-0.4.35}/Cargo.lock +2 -2
  2. {dirsql-0.4.34 → dirsql-0.4.35}/PKG-INFO +1 -1
  3. {dirsql-0.4.34 → dirsql-0.4.35}/docs/reference/cli.md +18 -0
  4. {dirsql-0.4.34/packages/rust → dirsql-0.4.35}/docs/reference/config.md +9 -0
  5. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/Cargo.toml +1 -1
  6. {dirsql-0.4.34/packages/rust → dirsql-0.4.35/packages/python}/docs/reference/cli.md +18 -0
  7. {dirsql-0.4.34 → dirsql-0.4.35/packages/python}/docs/reference/config.md +9 -0
  8. dirsql-0.4.35/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +7 -0
  9. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/testing-conventions.toml +1 -1
  10. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/Cargo.toml +1 -1
  11. {dirsql-0.4.34/packages/python → dirsql-0.4.35/packages/rust}/docs/reference/cli.md +18 -0
  12. {dirsql-0.4.34/packages/python → dirsql-0.4.35/packages/rust}/docs/reference/config.md +9 -0
  13. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/functions.rs +204 -32
  14. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/progress.rs +146 -21
  15. {dirsql-0.4.34 → dirsql-0.4.35}/Cargo.toml +0 -0
  16. {dirsql-0.4.34 → dirsql-0.4.35}/README.md +0 -0
  17. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/__init__.py +0 -0
  18. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/_async.py +0 -0
  19. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/_dirsql.pyi +0 -0
  20. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/__init__.py +0 -0
  21. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/discover_plugins/__init__.py +0 -0
  22. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  23. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  24. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  25. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  26. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  27. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/main.py +0 -0
  28. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/cli/resolve_config_extensions.py +0 -0
  29. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/py.typed +0 -0
  30. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/resolve_config_extensions.py +0 -0
  31. {dirsql-0.4.34 → dirsql-0.4.35}/dirsql/resolve_extension.py +0 -0
  32. {dirsql-0.4.34 → dirsql-0.4.35}/docs/.claude/CLAUDE.md +0 -0
  33. {dirsql-0.4.34 → dirsql-0.4.35}/docs/.vitepress/config.ts +0 -0
  34. {dirsql-0.4.34 → dirsql-0.4.35}/docs/.vitepress/theme/index.ts +0 -0
  35. {dirsql-0.4.34 → dirsql-0.4.35}/docs/.vitepress/theme/lang.ts +0 -0
  36. {dirsql-0.4.34 → dirsql-0.4.35}/docs/AGENTS.md +0 -0
  37. {dirsql-0.4.34 → dirsql-0.4.35}/docs/explanation.md +0 -0
  38. {dirsql-0.4.34 → dirsql-0.4.35}/docs/getting-started.md +0 -0
  39. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/columns-from-paths.md +0 -0
  40. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/define-tables.md +0 -0
  41. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/embed.md +0 -0
  42. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/extract-from-contents.md +0 -0
  43. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/load-extension.md +0 -0
  44. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/parse-files-into-columns.md +0 -0
  45. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/persist.md +0 -0
  46. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/query-json.md +0 -0
  47. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/query-without-config.md +0 -0
  48. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/react-to-changes.md +0 -0
  49. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/search-by-meaning.md +0 -0
  50. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/search-indexes.md +0 -0
  51. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/skip-files.md +0 -0
  52. {dirsql-0.4.34 → dirsql-0.4.35}/docs/howto/write-a-plugin.md +0 -0
  53. {dirsql-0.4.34 → dirsql-0.4.35}/docs/index.md +0 -0
  54. {dirsql-0.4.34 → dirsql-0.4.35}/docs/package.json +0 -0
  55. {dirsql-0.4.34 → dirsql-0.4.35}/docs/plugins.md +0 -0
  56. {dirsql-0.4.34 → dirsql-0.4.35}/docs/pnpm-lock.yaml +0 -0
  57. {dirsql-0.4.34 → dirsql-0.4.35}/docs/pnpm-workspace.yaml +0 -0
  58. {dirsql-0.4.34 → dirsql-0.4.35}/docs/reference/columns.md +0 -0
  59. {dirsql-0.4.34 → dirsql-0.4.35}/docs/reference/hooks.md +0 -0
  60. {dirsql-0.4.34 → dirsql-0.4.35}/docs/reference/http-api.md +0 -0
  61. {dirsql-0.4.34 → dirsql-0.4.35}/docs/reference/path-tables.md +0 -0
  62. {dirsql-0.4.34 → dirsql-0.4.35}/docs/reference/sdk.md +0 -0
  63. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/CHANGELOG.md +0 -0
  64. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/MIGRATIONS.md +0 -0
  65. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/README.md +0 -0
  66. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  67. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  68. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  69. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  70. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  71. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  72. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  73. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  74. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  75. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  76. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  77. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  78. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  79. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  80. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  81. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  82. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  83. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  84. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  85. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/changelog.d/README.md +0 -0
  86. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/conftest.py +0 -0
  87. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/.claude/CLAUDE.md +0 -0
  88. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/.vitepress/config.ts +0 -0
  89. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  90. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  91. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/AGENTS.md +0 -0
  92. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/explanation.md +0 -0
  93. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/getting-started.md +0 -0
  94. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/columns-from-paths.md +0 -0
  95. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/define-tables.md +0 -0
  96. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/embed.md +0 -0
  97. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/extract-from-contents.md +0 -0
  98. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/load-extension.md +0 -0
  99. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  100. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/persist.md +0 -0
  101. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/query-json.md +0 -0
  102. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/query-without-config.md +0 -0
  103. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/react-to-changes.md +0 -0
  104. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/search-by-meaning.md +0 -0
  105. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/search-indexes.md +0 -0
  106. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/skip-files.md +0 -0
  107. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/howto/write-a-plugin.md +0 -0
  108. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/index.md +0 -0
  109. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/package.json +0 -0
  110. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/plugins.md +0 -0
  111. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/pnpm-lock.yaml +0 -0
  112. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/pnpm-workspace.yaml +0 -0
  113. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/reference/columns.md +0 -0
  114. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/reference/hooks.md +0 -0
  115. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/reference/http-api.md +0 -0
  116. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/reference/path-tables.md +0 -0
  117. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/docs/reference/sdk.md +0 -0
  118. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  119. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  120. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  121. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  122. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  123. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  124. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  125. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  126. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  127. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  128. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  129. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  130. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  131. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  132. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  133. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  134. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  135. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  136. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  137. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  138. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  139. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  140. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  141. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  142. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/migrations.d/README.md +0 -0
  143. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/src/lib.rs +0 -0
  144. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/__init__.py +0 -0
  145. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/conftest.py +0 -0
  146. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/e2e/__init__.py +0 -0
  147. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  148. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  149. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/integration/__init__.py +0 -0
  150. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/integration/binding/__init__.py +0 -0
  151. {dirsql-0.4.34 → dirsql-0.4.35}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  152. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/CHANGELOG.md +0 -0
  153. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/MIGRATIONS.md +0 -0
  154. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/README.md +0 -0
  155. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/benches/db_bench.rs +0 -0
  156. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/benches/differ_bench.rs +0 -0
  157. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/benches/matcher_bench.rs +0 -0
  158. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/benches/scanner_bench.rs +0 -0
  159. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/explanation.md +0 -0
  160. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/getting-started.md +0 -0
  161. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  162. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/define-tables.md +0 -0
  163. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/embed.md +0 -0
  164. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  165. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/load-extension.md +0 -0
  166. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  167. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/persist.md +0 -0
  168. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/query-json.md +0 -0
  169. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/query-without-config.md +0 -0
  170. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/react-to-changes.md +0 -0
  171. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  172. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/search-indexes.md +0 -0
  173. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/skip-files.md +0 -0
  174. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  175. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/index.md +0 -0
  176. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/plugins.md +0 -0
  177. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/reference/columns.md +0 -0
  178. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/reference/hooks.md +0 -0
  179. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/reference/http-api.md +0 -0
  180. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/reference/path-tables.md +0 -0
  181. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/docs/reference/sdk.md +0 -0
  182. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/bin/dirsql.rs +0 -0
  183. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/execute.rs +0 -0
  184. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/init.rs +0 -0
  185. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/mod.rs +0 -0
  186. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/repl.rs +0 -0
  187. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/router.rs +0 -0
  188. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/run.rs +0 -0
  189. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/serialize.rs +0 -0
  190. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/server.rs +0 -0
  191. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/cli/table.rs +0 -0
  192. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/command.rs +0 -0
  193. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/config.rs +0 -0
  194. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/db.rs +0 -0
  195. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/default_config.toml +0 -0
  196. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/differ.rs +0 -0
  197. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/infer.rs +0 -0
  198. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/lib.rs +0 -0
  199. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/matcher.rs +0 -0
  200. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/parsed_cache.rs +0 -0
  201. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/parsed_vtab.rs +0 -0
  202. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/path_table.rs +0 -0
  203. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/persist.rs +0 -0
  204. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/scanner.rs +0 -0
  205. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/sql_literal.rs +0 -0
  206. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/vtab.rs +0 -0
  207. {dirsql-0.4.34 → dirsql-0.4.35}/packages/rust/src/watcher.rs +0 -0
  208. {dirsql-0.4.34 → dirsql-0.4.35}/pyproject.toml +0 -0
@@ -540,7 +540,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
540
540
 
541
541
  [[package]]
542
542
  name = "dirsql"
543
- version = "0.4.34"
543
+ version = "0.4.35"
544
544
  dependencies = [
545
545
  "assert_cmd",
546
546
  "axum",
@@ -583,7 +583,7 @@ dependencies = [
583
583
 
584
584
  [[package]]
585
585
  name = "dirsql-py-ext"
586
- version = "0.4.34"
586
+ version = "0.4.35"
587
587
  dependencies = [
588
588
  "dirsql",
589
589
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.34
3
+ Version: 0.4.35
4
4
  Requires-Dist: tomli>=2 ; python_full_version < '3.11'
5
5
  Requires-Dist: bin-shim>=0.1
6
6
  Summary: Ephemeral SQL index over a local directory
@@ -498,6 +498,24 @@ and, when the query ends:
498
498
  dirsql: ran 41231 worker calls in 2m41s
499
499
  ```
500
500
 
501
+ A worker that keeps a cache of its own can say so per response
502
+ (`{"meta": {"cached": true}}` — see [Worker
503
+ protocol](./config.md#worker-protocol)), and those round trips are then split
504
+ out, because the number that answers *"will this be fast next time"* is not
505
+ the total:
506
+
507
+ ```
508
+ dirsql: running 9204 worker calls (8811 cached)
509
+ dirsql: ran 41231 worker calls in 2m41s (38104 cached)
510
+ ```
511
+
512
+ Both figures count **worker round trips, not rows**. A function declared
513
+ `deterministic = true` lets SQLite reuse one answer for identical arguments
514
+ within a query, so repeated content never reaches the worker at all and both
515
+ numbers sit below the row count on a corpus with duplicates. The split covers
516
+ only what the worker itself reports serving from cache. A worker that sends no
517
+ `meta` gets the unsplit line above, not a `(0 cached)`.
518
+
501
519
  A query that calls no worker prints nothing, whatever the setting — the
502
520
  reporting only ever speaks when there is something to report.
503
521
 
@@ -161,6 +161,15 @@ request line in, one response line out, per call:
161
161
  JSON-text vectors) — or `{"err": "message"}`, which **fails the query**
162
162
  with that message. An `{"err": ...}` response leaves the healthy worker
163
163
  running; only transport failures (timeout, crash) recycle it.
164
+ - **Optional `"meta"`**, alongside `"ok"`: a second top-level object carrying
165
+ facts about how the answer was produced rather than what it is. One key is
166
+ read today — `{"ok": [...], "meta": {"cached": true}}` says the worker
167
+ answered from a cache of its own, which dirsql reports as the `(N cached)`
168
+ split on its [worker-call progress line](./cli.md#progress-reporting). The
169
+ field is **optional and advisory**: a worker that omits it is a worker that
170
+ reports no cache hits, keys dirsql does not recognize are ignored (they
171
+ always have been), and a `meta` of an unexpected shape is ignored too rather
172
+ than failing a query over a progress counter.
164
173
  - **stderr passes through** to `dirsql`'s stderr, so a worker's progress
165
174
  bars and download logs reach the terminal.
166
175
 
@@ -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.34"
7
+ version = "0.4.35"
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
@@ -498,6 +498,24 @@ and, when the query ends:
498
498
  dirsql: ran 41231 worker calls in 2m41s
499
499
  ```
500
500
 
501
+ A worker that keeps a cache of its own can say so per response
502
+ (`{"meta": {"cached": true}}` — see [Worker
503
+ protocol](./config.md#worker-protocol)), and those round trips are then split
504
+ out, because the number that answers *"will this be fast next time"* is not
505
+ the total:
506
+
507
+ ```
508
+ dirsql: running 9204 worker calls (8811 cached)
509
+ dirsql: ran 41231 worker calls in 2m41s (38104 cached)
510
+ ```
511
+
512
+ Both figures count **worker round trips, not rows**. A function declared
513
+ `deterministic = true` lets SQLite reuse one answer for identical arguments
514
+ within a query, so repeated content never reaches the worker at all and both
515
+ numbers sit below the row count on a corpus with duplicates. The split covers
516
+ only what the worker itself reports serving from cache. A worker that sends no
517
+ `meta` gets the unsplit line above, not a `(0 cached)`.
518
+
501
519
  A query that calls no worker prints nothing, whatever the setting — the
502
520
  reporting only ever speaks when there is something to report.
503
521
 
@@ -161,6 +161,15 @@ request line in, one response line out, per call:
161
161
  JSON-text vectors) — or `{"err": "message"}`, which **fails the query**
162
162
  with that message. An `{"err": ...}` response leaves the healthy worker
163
163
  running; only transport failures (timeout, crash) recycle it.
164
+ - **Optional `"meta"`**, alongside `"ok"`: a second top-level object carrying
165
+ facts about how the answer was produced rather than what it is. One key is
166
+ read today — `{"ok": [...], "meta": {"cached": true}}` says the worker
167
+ answered from a cache of its own, which dirsql reports as the `(N cached)`
168
+ split on its [worker-call progress line](./cli.md#progress-reporting). The
169
+ field is **optional and advisory**: a worker that omits it is a worker that
170
+ reports no cache hits, keys dirsql does not recognize are ignored (they
171
+ always have been), and a `meta` of an unexpected shape is ignored too rather
172
+ than failing a query over a progress counter.
164
173
  - **stderr passes through** to `dirsql`'s stderr, so a worker's progress
165
174
  bars and download logs reach the terminal.
166
175
 
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1788557530,
4
+ "exit_code": 0,
5
+ "commit": "97fdc1dfbdf490f71bd2067d8be66a50dce22bcb",
6
+ "branch": "claude/tackle-1034-cachesplit"
7
+ }
@@ -10,7 +10,7 @@
10
10
  extra_scope = ["packages/rust/src", "packages/python/src"]
11
11
 
12
12
  [python]
13
- one_function_per_file = { max_lines = 100 }
13
+ one_function_per_file = { max_lines = 16 }
14
14
 
15
15
  # Same floor as the root file, which this lane no longer reads.
16
16
  [python.coverage]
@@ -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.34"
9
+ version = "0.4.35"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -498,6 +498,24 @@ and, when the query ends:
498
498
  dirsql: ran 41231 worker calls in 2m41s
499
499
  ```
500
500
 
501
+ A worker that keeps a cache of its own can say so per response
502
+ (`{"meta": {"cached": true}}` — see [Worker
503
+ protocol](./config.md#worker-protocol)), and those round trips are then split
504
+ out, because the number that answers *"will this be fast next time"* is not
505
+ the total:
506
+
507
+ ```
508
+ dirsql: running 9204 worker calls (8811 cached)
509
+ dirsql: ran 41231 worker calls in 2m41s (38104 cached)
510
+ ```
511
+
512
+ Both figures count **worker round trips, not rows**. A function declared
513
+ `deterministic = true` lets SQLite reuse one answer for identical arguments
514
+ within a query, so repeated content never reaches the worker at all and both
515
+ numbers sit below the row count on a corpus with duplicates. The split covers
516
+ only what the worker itself reports serving from cache. A worker that sends no
517
+ `meta` gets the unsplit line above, not a `(0 cached)`.
518
+
501
519
  A query that calls no worker prints nothing, whatever the setting — the
502
520
  reporting only ever speaks when there is something to report.
503
521
 
@@ -161,6 +161,15 @@ request line in, one response line out, per call:
161
161
  JSON-text vectors) — or `{"err": "message"}`, which **fails the query**
162
162
  with that message. An `{"err": ...}` response leaves the healthy worker
163
163
  running; only transport failures (timeout, crash) recycle it.
164
+ - **Optional `"meta"`**, alongside `"ok"`: a second top-level object carrying
165
+ facts about how the answer was produced rather than what it is. One key is
166
+ read today — `{"ok": [...], "meta": {"cached": true}}` says the worker
167
+ answered from a cache of its own, which dirsql reports as the `(N cached)`
168
+ split on its [worker-call progress line](./cli.md#progress-reporting). The
169
+ field is **optional and advisory**: a worker that omits it is a worker that
170
+ reports no cache hits, keys dirsql does not recognize are ignored (they
171
+ always have been), and a `meta` of an unexpected shape is ignored too rather
172
+ than failing a query over a progress counter.
164
173
  - **stderr passes through** to `dirsql`'s stderr, so a worker's progress
165
174
  bars and download logs reach the terminal.
166
175
 
@@ -17,7 +17,12 @@
17
17
  //! `{"$bytes": "<base64>"}`.
18
18
  //! - Response: `{"ok": <value>}` (same scalar encodings; a JSON array or any
19
19
  //! other object is bound as TEXT, its JSON text) or `{"err": "message"}`,
20
- //! which fails the query with that message.
20
+ //! which fails the query with that message. An `{"ok": ...}` response may
21
+ //! carry an optional `"meta"` object alongside it; the one key read today is
22
+ //! `{"meta": {"cached": true}}`, which says the worker answered from its own
23
+ //! cache and feeds the progress line's cache split. `meta` is advisory —
24
+ //! every other key, and a `meta` of a shape this parser does not recognize,
25
+ //! is ignored rather than failing the query.
21
26
  //! - The worker's stderr is inherited, passing straight through to dirsql's
22
27
  //! stderr.
23
28
  //!
@@ -85,9 +90,13 @@ pub(crate) fn register_all(
85
90
  args.push(Value::from(rusqlite::types::Value::from(ctx.get_raw(i))));
86
91
  }
87
92
  calls.record();
88
- worker
93
+ let reply = worker
89
94
  .call(&args)
90
- .map_err(|message| rusqlite::Error::UserFunctionError(message.into()))
95
+ .map_err(|message| rusqlite::Error::UserFunctionError(message.into()))?;
96
+ if reply.cached {
97
+ calls.mark_cached();
98
+ }
99
+ Ok(reply.value)
91
100
  },
92
101
  )?;
93
102
  }
@@ -115,6 +124,10 @@ pub(crate) struct CallReporter {
115
124
 
116
125
  struct CallState {
117
126
  count: u64,
127
+ /// How many of those round trips the worker flagged as cache hits. Core
128
+ /// cannot derive this: a cache hit *is* a round trip, so only the worker
129
+ /// knows which of its answers cost anything.
130
+ cached: u64,
118
131
  progress: Box<dyn CallProgress>,
119
132
  }
120
133
 
@@ -125,7 +138,11 @@ impl CallReporter {
125
138
 
126
139
  fn with_progress(progress: Box<dyn CallProgress>) -> Self {
127
140
  Self {
128
- state: Mutex::new(CallState { count: 0, progress }),
141
+ state: Mutex::new(CallState {
142
+ count: 0,
143
+ cached: 0,
144
+ progress,
145
+ }),
129
146
  }
130
147
  }
131
148
 
@@ -135,29 +152,48 @@ impl CallReporter {
135
152
  pub(crate) fn phase(&self) -> CallPhase<'_> {
136
153
  if let Ok(mut state) = self.state.lock() {
137
154
  state.count = 0;
155
+ state.cached = 0;
138
156
  state.progress.restart();
139
157
  }
140
158
  CallPhase(self)
141
159
  }
142
160
 
143
- /// Record one round trip. A poisoned mutex costs the counter, never the
144
- /// call: reporting is not worth failing a query over.
161
+ /// Record one round trip, before it is made — a slow worker's first call
162
+ /// is exactly when a user most wants the line to appear. A poisoned mutex
163
+ /// costs the counter, never the call: reporting is not worth failing a
164
+ /// query over.
145
165
  fn record(&self) {
146
166
  if let Ok(mut state) = self.state.lock() {
147
167
  state.count += 1;
148
- let count = state.count;
149
- state.progress.update(count);
168
+ state.report();
169
+ }
170
+ }
171
+
172
+ /// Mark a round trip that has come back as one the worker served from its
173
+ /// own cache. Separate from [`record`](Self::record) because cachedness is
174
+ /// only knowable once the response is in hand.
175
+ fn mark_cached(&self) {
176
+ if let Ok(mut state) = self.state.lock() {
177
+ state.cached += 1;
178
+ state.report();
150
179
  }
151
180
  }
152
181
 
153
182
  fn end(&self) {
154
183
  if let Ok(mut state) = self.state.lock() {
155
- let count = state.count;
156
- state.progress.finish(count);
184
+ let (count, cached) = (state.count, state.cached);
185
+ state.progress.finish(count, cached);
157
186
  }
158
187
  }
159
188
  }
160
189
 
190
+ impl CallState {
191
+ fn report(&mut self) {
192
+ let (count, cached) = (self.count, self.cached);
193
+ self.progress.update(count, cached);
194
+ }
195
+ }
196
+
161
197
  /// The open phase. Ends when it drops.
162
198
  pub(crate) struct CallPhase<'a>(&'a CallReporter);
163
199
 
@@ -241,7 +277,7 @@ impl Worker {
241
277
  /// with. A transport failure (spawn error, crash, timeout) drops the
242
278
  /// worker so the next call starts fresh; a protocol-level `{"err": ...}`
243
279
  /// leaves the healthy worker running.
244
- fn call(&self, args: &[Value]) -> Result<Value, String> {
280
+ fn call(&self, args: &[Value]) -> Result<Reply, String> {
245
281
  let mut inner = self
246
282
  .inner
247
283
  .lock()
@@ -291,9 +327,9 @@ impl Worker {
291
327
  }
292
328
 
293
329
  /// Decode one response line: `{"ok": <value>}` or `{"err": "message"}`.
294
- fn decode(&self, line: &str) -> Result<Value, String> {
330
+ fn decode(&self, line: &str) -> Result<Reply, String> {
295
331
  match parse_response(line) {
296
- Ok(Response::Ok(value)) => Ok(value),
332
+ Ok(Response::Ok { value, cached }) => Ok(Reply { value, cached }),
297
333
  Ok(Response::Err(message)) => Err(message),
298
334
  Err(defect) => Err(format!(
299
335
  "function `{}` worker sent an invalid response ({defect}): {line}",
@@ -325,9 +361,17 @@ fn value_to_json(value: &Value) -> serde_json::Value {
325
361
  }
326
362
  }
327
363
 
364
+ /// A completed round trip: the value to bind, plus whatever the response said
365
+ /// about how it was produced.
366
+ #[derive(Debug)]
367
+ pub(crate) struct Reply {
368
+ value: Value,
369
+ cached: bool,
370
+ }
371
+
328
372
  #[derive(Debug)]
329
373
  enum Response {
330
- Ok(Value),
374
+ Ok { value: Value, cached: bool },
331
375
  Err(String),
332
376
  }
333
377
 
@@ -348,7 +392,22 @@ fn parse_response(line: &str) -> Result<Response, String> {
348
392
  let value = object
349
393
  .get("ok")
350
394
  .ok_or_else(|| "expected an \"ok\" or \"err\" key".to_string())?;
351
- Ok(Response::Ok(json_to_sql_value(value)?))
395
+ Ok(Response::Ok {
396
+ value: json_to_sql_value(value)?,
397
+ cached: cached_flag(object),
398
+ })
399
+ }
400
+
401
+ /// Whether the response flagged itself as served from the worker's own cache:
402
+ /// `{"meta": {"cached": true}}`. Optional and advisory — it feeds the progress
403
+ /// line and nothing else, so an absent, misshapen or non-boolean value reads as
404
+ /// "not cached" rather than failing a query over a counter.
405
+ fn cached_flag(object: &serde_json::Map<String, serde_json::Value>) -> bool {
406
+ object
407
+ .get("meta")
408
+ .and_then(|meta| meta.get("cached"))
409
+ .and_then(serde_json::Value::as_bool)
410
+ .unwrap_or(false)
352
411
  }
353
412
 
354
413
  /// Wire JSON → SQL value: string → TEXT, integral number → INTEGER, other
@@ -539,12 +598,12 @@ mod tests {
539
598
  struct RecordingProgress(Recorder);
540
599
 
541
600
  impl CallProgress for RecordingProgress {
542
- fn update(&mut self, done: u64) {
543
- self.0.push(format!("update {done}"));
601
+ fn update(&mut self, done: u64, cached: u64) {
602
+ self.0.push(format!("update {done}/{cached}"));
544
603
  }
545
604
 
546
- fn finish(&mut self, done: u64) {
547
- self.0.push(format!("finish {done}"));
605
+ fn finish(&mut self, done: u64, cached: u64) {
606
+ self.0.push(format!("finish {done}/{cached}"));
548
607
  }
549
608
 
550
609
  fn restart(&mut self) {
@@ -565,7 +624,7 @@ mod tests {
565
624
  reporter.record();
566
625
  reporter.record();
567
626
 
568
- assert_eq!(recorder.events(), ["update 1", "update 2"]);
627
+ assert_eq!(recorder.events(), ["update 1/0", "update 2/0"]);
569
628
  }
570
629
 
571
630
  /// The phase is what the count belongs to: a second query must not carry
@@ -583,7 +642,13 @@ mod tests {
583
642
 
584
643
  assert_eq!(
585
644
  recorder.events(),
586
- ["update 1", "update 2", "restart", "update 1", "finish 1"],
645
+ [
646
+ "update 1/0",
647
+ "update 2/0",
648
+ "restart",
649
+ "update 1/0",
650
+ "finish 1/0"
651
+ ],
587
652
  "the new phase counts from one, not from three"
588
653
  );
589
654
  }
@@ -600,18 +665,76 @@ mod tests {
600
665
  reporter.record();
601
666
  assert_eq!(
602
667
  recorder.events(),
603
- ["restart", "update 1", "update 2"],
668
+ ["restart", "update 1/0", "update 2/0"],
604
669
  "nothing is finished while the phase is open"
605
670
  );
606
671
  }
607
672
 
608
673
  assert_eq!(
609
674
  recorder.events(),
610
- ["restart", "update 1", "update 2", "finish 2"],
675
+ ["restart", "update 1/0", "update 2/0", "finish 2/0"],
611
676
  "the closed phase reports the count it accumulated"
612
677
  );
613
678
  }
614
679
 
680
+ /// A cache hit is a round trip like any other, so it counts twice over:
681
+ /// once in the total and once in the split.
682
+ #[test]
683
+ fn a_cached_round_trip_counts_in_both_the_total_and_the_split() {
684
+ let (reporter, recorder) = reporter();
685
+
686
+ reporter.record();
687
+ reporter.mark_cached();
688
+
689
+ assert_eq!(recorder.events(), ["update 1/0", "update 1/1"]);
690
+ }
691
+
692
+ /// The split counts only what the worker flagged. A run that computes
693
+ /// everything must not read as a run that cached everything.
694
+ #[test]
695
+ fn an_uncached_round_trip_leaves_the_split_at_zero() {
696
+ let (reporter, recorder) = reporter();
697
+
698
+ {
699
+ let _phase = reporter.phase();
700
+ reporter.record();
701
+ reporter.record();
702
+ reporter.mark_cached();
703
+ }
704
+
705
+ assert_eq!(
706
+ recorder.events(),
707
+ [
708
+ "restart",
709
+ "update 1/0",
710
+ "update 2/0",
711
+ "update 2/1",
712
+ "finish 2/1"
713
+ ],
714
+ "one of the two round trips was flagged, not both"
715
+ );
716
+ }
717
+
718
+ /// A phase owns its split as well as its count: a warm second query must
719
+ /// not inherit the first query's cache hits.
720
+ #[test]
721
+ fn opening_a_phase_restarts_the_cache_split_too() {
722
+ let (reporter, recorder) = reporter();
723
+ reporter.record();
724
+ reporter.mark_cached();
725
+
726
+ {
727
+ let _phase = reporter.phase();
728
+ reporter.record();
729
+ }
730
+
731
+ assert_eq!(
732
+ recorder.events().last().unwrap(),
733
+ "finish 1/0",
734
+ "the new phase starts with no cache hits"
735
+ );
736
+ }
737
+
615
738
  /// The production constructor wires a real [`Progress`] through the same
616
739
  /// trait, so a whole phase runs against it here — the double proves the
617
740
  /// orchestration, this proves the wiring is connected to something real.
@@ -624,9 +747,12 @@ mod tests {
624
747
  let _phase = reporter.phase();
625
748
  reporter.record();
626
749
  reporter.record();
750
+ reporter.mark_cached();
627
751
  }
628
752
 
629
- assert_eq!(reporter.state.lock().unwrap().count, 2);
753
+ let state = reporter.state.lock().unwrap();
754
+ assert_eq!(state.count, 2);
755
+ assert_eq!(state.cached, 1);
630
756
  }
631
757
 
632
758
  // --- wire encoding -----------------------------------------------------
@@ -659,8 +785,16 @@ mod tests {
659
785
  // --- response decoding -------------------------------------------------
660
786
 
661
787
  fn ok_value(line: &str) -> Value {
788
+ ok_response(line).0
789
+ }
790
+
791
+ fn ok_cached(line: &str) -> bool {
792
+ ok_response(line).1
793
+ }
794
+
795
+ fn ok_response(line: &str) -> (Value, bool) {
662
796
  match parse_response(line).unwrap() {
663
- Response::Ok(v) => v,
797
+ Response::Ok { value, cached } => (value, cached),
664
798
  Response::Err(m) => panic!("expected ok, got err {m:?}"),
665
799
  }
666
800
  }
@@ -688,10 +822,48 @@ mod tests {
688
822
  fn response_err_carries_the_message() {
689
823
  match parse_response(r#"{"err": "boom"}"#).unwrap() {
690
824
  Response::Err(m) => assert_eq!(m, "boom"),
691
- Response::Ok(v) => panic!("expected err, got ok {v:?}"),
825
+ Response::Ok { value, .. } => panic!("expected err, got ok {value:?}"),
692
826
  }
693
827
  }
694
828
 
829
+ /// The signal core cannot derive for itself: a cache hit and a computed
830
+ /// answer are the same round trip on the wire, so the worker has to say.
831
+ #[test]
832
+ fn a_response_flagged_cached_is_read_as_a_cache_hit() {
833
+ assert!(ok_cached(r#"{"ok": 1, "meta": {"cached": true}}"#));
834
+ }
835
+
836
+ #[test]
837
+ fn a_response_without_meta_is_not_a_cache_hit() {
838
+ assert!(!ok_cached(r#"{"ok": 1}"#));
839
+ }
840
+
841
+ #[test]
842
+ fn a_response_flagged_uncached_is_not_a_cache_hit() {
843
+ assert!(!ok_cached(r#"{"ok": 1, "meta": {"cached": false}}"#));
844
+ }
845
+
846
+ /// `meta` is advisory: it drives a progress counter, so no shape of it is
847
+ /// worth failing a query over. Anything unreadable means "not cached".
848
+ #[test]
849
+ fn an_unreadable_meta_reads_as_uncached_rather_than_failing() {
850
+ assert!(!ok_cached(r#"{"ok": 1, "meta": "not-an-object"}"#));
851
+ assert!(!ok_cached(r#"{"ok": 1, "meta": {}}"#));
852
+ assert!(!ok_cached(r#"{"ok": 1, "meta": {"cached": "yes"}}"#));
853
+ assert!(!ok_cached(r#"{"ok": 1, "meta": null}"#));
854
+ assert!(!ok_cached(r#"{"ok": 1, "meta": [{"cached": true}]}"#));
855
+ }
856
+
857
+ /// The value is the `ok` field and nothing else — `meta` never leaks into
858
+ /// what the query binds.
859
+ #[test]
860
+ fn meta_does_not_change_the_decoded_value() {
861
+ assert_eq!(
862
+ ok_value(r#"{"ok": "hi", "meta": {"cached": true}}"#),
863
+ text("hi")
864
+ );
865
+ }
866
+
695
867
  #[test]
696
868
  fn response_defects_are_named() {
697
869
  assert!(
@@ -842,8 +1014,8 @@ mod tests {
842
1014
  ]],
843
1015
  false,
844
1016
  );
845
- assert_eq!(worker.call(&[text("a")]).unwrap(), text("A"));
846
- assert_eq!(worker.call(&[text("b")]).unwrap(), text("B"));
1017
+ assert_eq!(worker.call(&[text("a")]).unwrap().value, text("A"));
1018
+ assert_eq!(worker.call(&[text("b")]).unwrap().value, text("B"));
847
1019
  assert_eq!(*spawns.lock().unwrap(), 1);
848
1020
  }
849
1021
 
@@ -879,7 +1051,7 @@ mod tests {
879
1051
  false,
880
1052
  );
881
1053
  assert_eq!(worker.call(&[]).unwrap_err(), "boom");
882
- assert_eq!(worker.call(&[]).unwrap(), Value::Integer(1));
1054
+ assert_eq!(worker.call(&[]).unwrap().value, Value::Integer(1));
883
1055
  assert_eq!(*spawns.lock().unwrap(), 1, "err response must not respawn");
884
1056
  }
885
1057
 
@@ -897,7 +1069,7 @@ mod tests {
897
1069
  assert!(err.contains("`embed`"), "got: {err}");
898
1070
  assert!(err.contains("`timeout`"), "got: {err}");
899
1071
  // The next call starts a fresh worker.
900
- assert_eq!(worker.call(&[]).unwrap(), Value::Integer(1));
1072
+ assert_eq!(worker.call(&[]).unwrap().value, Value::Integer(1));
901
1073
  assert_eq!(*spawns.lock().unwrap(), 2);
902
1074
  }
903
1075
 
@@ -913,7 +1085,7 @@ mod tests {
913
1085
  let err = worker.call(&[]).unwrap_err();
914
1086
  assert!(err.contains("exited before replying"), "got: {err}");
915
1087
  assert!(err.contains("stderr"), "got: {err}");
916
- assert_eq!(worker.call(&[]).unwrap(), Value::Integer(1));
1088
+ assert_eq!(worker.call(&[]).unwrap().value, Value::Integer(1));
917
1089
  assert_eq!(*spawns.lock().unwrap(), 2);
918
1090
  }
919
1091
 
@@ -955,7 +1127,7 @@ mod tests {
955
1127
  Box::new(|| Ok(Box::new(EchoTransport))),
956
1128
  );
957
1129
  assert_eq!(
958
- worker.call(&[text("x"), Value::Integer(3)]).unwrap(),
1130
+ worker.call(&[text("x"), Value::Integer(3)]).unwrap().value,
959
1131
  Value::Null
960
1132
  );
961
1133
  }