dirsql 0.4.41__tar.gz → 0.4.42__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 (231) hide show
  1. {dirsql-0.4.41 → dirsql-0.4.42}/Cargo.lock +2 -2
  2. {dirsql-0.4.41 → dirsql-0.4.42}/PKG-INFO +1 -1
  3. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/Cargo.toml +1 -1
  4. dirsql-0.4.42/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +7 -0
  5. dirsql-0.4.42/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +7 -0
  6. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/Cargo.toml +1 -1
  7. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/db.rs +54 -2
  8. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/lib.rs +177 -5
  9. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/parsed_vtab.rs +35 -149
  10. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/vtab.rs +41 -201
  11. dirsql-0.4.42/packages/rust/src/vtab_scaffold.rs +378 -0
  12. {dirsql-0.4.41 → dirsql-0.4.42}/Cargo.toml +0 -0
  13. {dirsql-0.4.41 → dirsql-0.4.42}/README.md +0 -0
  14. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/__init__.py +0 -0
  15. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/_async.py +0 -0
  16. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/_dirsql.pyi +0 -0
  17. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/__init__.py +0 -0
  18. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/discover_plugins/__init__.py +0 -0
  19. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  20. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  21. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  22. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  23. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  24. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/main.py +0 -0
  25. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/cli/resolve_config_extensions.py +0 -0
  26. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/has_bare_name.py +0 -0
  27. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/is_bare_name.py +0 -0
  28. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/load_extension_entries.py +0 -0
  29. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/platform_patterns.py +0 -0
  30. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/py.typed +0 -0
  31. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/resolve_config_extensions.py +0 -0
  32. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/resolve_configs_extension_specs.py +0 -0
  33. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/resolve_entries.py +0 -0
  34. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/resolve_extension.py +0 -0
  35. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/resolve_package.py +0 -0
  36. {dirsql-0.4.41 → dirsql-0.4.42}/dirsql/toml_module.py +0 -0
  37. {dirsql-0.4.41 → dirsql-0.4.42}/docs/.claude/CLAUDE.md +0 -0
  38. {dirsql-0.4.41 → dirsql-0.4.42}/docs/.vitepress/config.ts +0 -0
  39. {dirsql-0.4.41 → dirsql-0.4.42}/docs/.vitepress/theme/index.ts +0 -0
  40. {dirsql-0.4.41 → dirsql-0.4.42}/docs/.vitepress/theme/lang.ts +0 -0
  41. {dirsql-0.4.41 → dirsql-0.4.42}/docs/AGENTS.md +0 -0
  42. {dirsql-0.4.41 → dirsql-0.4.42}/docs/explanation.md +0 -0
  43. {dirsql-0.4.41 → dirsql-0.4.42}/docs/getting-started.md +0 -0
  44. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/columns-from-paths.md +0 -0
  45. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/define-tables.md +0 -0
  46. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/embed.md +0 -0
  47. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/extract-from-contents.md +0 -0
  48. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/load-extension.md +0 -0
  49. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/parse-files-into-columns.md +0 -0
  50. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/persist.md +0 -0
  51. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/query-json.md +0 -0
  52. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/query-without-config.md +0 -0
  53. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/react-to-changes.md +0 -0
  54. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/search-by-meaning.md +0 -0
  55. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/search-indexes.md +0 -0
  56. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/skip-files.md +0 -0
  57. {dirsql-0.4.41 → dirsql-0.4.42}/docs/howto/write-a-plugin.md +0 -0
  58. {dirsql-0.4.41 → dirsql-0.4.42}/docs/index.md +0 -0
  59. {dirsql-0.4.41 → dirsql-0.4.42}/docs/package.json +0 -0
  60. {dirsql-0.4.41 → dirsql-0.4.42}/docs/plugins.md +0 -0
  61. {dirsql-0.4.41 → dirsql-0.4.42}/docs/pnpm-lock.yaml +0 -0
  62. {dirsql-0.4.41 → dirsql-0.4.42}/docs/pnpm-workspace.yaml +0 -0
  63. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/cli.md +0 -0
  64. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/columns.md +0 -0
  65. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/config.md +0 -0
  66. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/hooks.md +0 -0
  67. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/http-api.md +0 -0
  68. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/path-tables.md +0 -0
  69. {dirsql-0.4.41 → dirsql-0.4.42}/docs/reference/sdk.md +0 -0
  70. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/CHANGELOG.md +0 -0
  71. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/MIGRATIONS.md +0 -0
  72. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/README.md +0 -0
  73. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  74. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  75. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  76. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  77. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  78. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  79. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  80. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  81. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  82. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  83. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  84. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  85. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  86. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  87. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  88. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  89. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  90. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  91. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  92. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  93. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  94. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  95. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  96. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  97. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/changelog.d/README.md +0 -0
  98. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/conftest.py +0 -0
  99. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/.claude/CLAUDE.md +0 -0
  100. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/.vitepress/config.ts +0 -0
  101. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  102. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  103. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/AGENTS.md +0 -0
  104. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/explanation.md +0 -0
  105. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/getting-started.md +0 -0
  106. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/columns-from-paths.md +0 -0
  107. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/define-tables.md +0 -0
  108. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/embed.md +0 -0
  109. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/extract-from-contents.md +0 -0
  110. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/load-extension.md +0 -0
  111. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  112. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/persist.md +0 -0
  113. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/query-json.md +0 -0
  114. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/query-without-config.md +0 -0
  115. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/react-to-changes.md +0 -0
  116. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/search-by-meaning.md +0 -0
  117. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/search-indexes.md +0 -0
  118. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/skip-files.md +0 -0
  119. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/howto/write-a-plugin.md +0 -0
  120. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/index.md +0 -0
  121. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/package.json +0 -0
  122. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/plugins.md +0 -0
  123. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/pnpm-lock.yaml +0 -0
  124. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/pnpm-workspace.yaml +0 -0
  125. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/cli.md +0 -0
  126. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/columns.md +0 -0
  127. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/config.md +0 -0
  128. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/hooks.md +0 -0
  129. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/http-api.md +0 -0
  130. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/path-tables.md +0 -0
  131. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/docs/reference/sdk.md +0 -0
  132. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  133. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  134. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  135. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  136. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  137. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  138. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  139. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  140. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  141. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  142. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  143. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  144. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  145. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  146. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  147. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  148. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  149. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  150. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  151. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  152. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  153. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  154. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  155. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  156. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  157. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  158. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  159. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  160. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  161. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  162. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  163. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/migrations.d/README.md +0 -0
  164. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/src/lib.rs +0 -0
  165. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/testing-conventions.toml +0 -0
  166. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/__init__.py +0 -0
  167. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/conftest.py +0 -0
  168. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/e2e/__init__.py +0 -0
  169. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  170. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  171. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/integration/__init__.py +0 -0
  172. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/integration/binding/__init__.py +0 -0
  173. {dirsql-0.4.41 → dirsql-0.4.42}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  174. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/CHANGELOG.md +0 -0
  175. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/MIGRATIONS.md +0 -0
  176. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/README.md +0 -0
  177. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/benches/db_bench.rs +0 -0
  178. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/benches/differ_bench.rs +0 -0
  179. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/benches/matcher_bench.rs +0 -0
  180. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/benches/scanner_bench.rs +0 -0
  181. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/explanation.md +0 -0
  182. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/getting-started.md +0 -0
  183. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  184. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/define-tables.md +0 -0
  185. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/embed.md +0 -0
  186. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  187. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/load-extension.md +0 -0
  188. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  189. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/persist.md +0 -0
  190. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/query-json.md +0 -0
  191. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/query-without-config.md +0 -0
  192. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/react-to-changes.md +0 -0
  193. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  194. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/search-indexes.md +0 -0
  195. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/skip-files.md +0 -0
  196. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  197. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/index.md +0 -0
  198. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/plugins.md +0 -0
  199. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/cli.md +0 -0
  200. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/columns.md +0 -0
  201. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/config.md +0 -0
  202. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/hooks.md +0 -0
  203. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/http-api.md +0 -0
  204. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/path-tables.md +0 -0
  205. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/docs/reference/sdk.md +0 -0
  206. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/bin/dirsql.rs +0 -0
  207. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/execute.rs +0 -0
  208. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/init.rs +0 -0
  209. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/mod.rs +0 -0
  210. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/repl.rs +0 -0
  211. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/router.rs +0 -0
  212. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/run.rs +0 -0
  213. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/serialize.rs +0 -0
  214. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/server.rs +0 -0
  215. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/cli/table.rs +0 -0
  216. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/command.rs +0 -0
  217. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/config.rs +0 -0
  218. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/default_config.toml +0 -0
  219. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/differ.rs +0 -0
  220. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/functions.rs +0 -0
  221. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/infer.rs +0 -0
  222. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/launcher.rs +0 -0
  223. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/matcher.rs +0 -0
  224. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/parsed_cache.rs +0 -0
  225. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/path_table.rs +0 -0
  226. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/persist.rs +0 -0
  227. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/progress.rs +0 -0
  228. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/scanner.rs +0 -0
  229. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/sql_literal.rs +0 -0
  230. {dirsql-0.4.41 → dirsql-0.4.42}/packages/rust/src/watcher.rs +0 -0
  231. {dirsql-0.4.41 → dirsql-0.4.42}/pyproject.toml +0 -0
@@ -540,7 +540,7 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
540
540
 
541
541
  [[package]]
542
542
  name = "dirsql"
543
- version = "0.4.41"
543
+ version = "0.4.42"
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.41"
586
+ version = "0.4.42"
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.41
3
+ Version: 0.4.42
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
@@ -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.41"
7
+ version = "0.4.42"
8
8
  # abi3 note: the `abi3-py310` pyo3 feature (below) builds ONE stable-ABI
9
9
  # `cp310-abi3` wheel per platform that loads on every CPython >= 3.10
10
10
  # (matching `pyproject.toml`'s `requires-python = ">=3.10"`), instead of
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1789515494,
4
+ "exit_code": 0,
5
+ "commit": "51555d8d0ad35095d69c1568e4478f34feb36e09",
6
+ "branch": "claude/1080-share-vtab-scaffolding"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1789595962,
4
+ "exit_code": 0,
5
+ "commit": "c645cfe8052bdca311060ca475f6ee102246650b",
6
+ "branch": "claude/1109-watch-moved-out"
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.41"
9
+ version = "0.4.42"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -645,6 +645,19 @@ impl Db {
645
645
  Ok(out)
646
646
  }
647
647
 
648
+ /// Every distinct `(table_name, file_path)` whose file sits beneath the
649
+ /// directory `dir`, ordered by table then path.
650
+ pub fn files_under(&self, dir: &str) -> Result<Vec<(String, String)>> {
651
+ let prefix = format!("{dir}{}", std::path::MAIN_SEPARATOR);
652
+ let mut stmt = self.conn.prepare(
653
+ "SELECT DISTINCT table_name, file_path FROM _dirsql_internal_rows \
654
+ WHERE substr(file_path, 1, length(?1)) = ?1 \
655
+ ORDER BY table_name, file_path",
656
+ )?;
657
+ let rows = stmt.query_map([prefix], |r| Ok((r.get(0)?, r.get(1)?)))?;
658
+ Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
659
+ }
660
+
648
661
  /// Helper to execute delete_rows_by_file statements on any connection.
649
662
  /// Called from delete_rows_by_file() either inside a caller-supplied
650
663
  /// transaction or inside a transaction opened by delete_rows_by_file().
@@ -1700,6 +1713,45 @@ mod tests {
1700
1713
  assert_eq!(rows[0].2, 42, "rowid_ref must be the user-declared rowid");
1701
1714
  }
1702
1715
 
1716
+ #[test]
1717
+ fn files_under_lists_each_file_beneath_the_directory_once_across_tables() {
1718
+ let db = Db::new().unwrap();
1719
+ db.create_table("t1", "CREATE TABLE t1 (id TEXT)").unwrap();
1720
+ db.create_table("t2", "CREATE TABLE t2 (id TEXT)").unwrap();
1721
+ let row = HashMap::from([("id".into(), Value::Text("x".into()))]);
1722
+ for (table, file, idx) in [
1723
+ ("t1", "dir/a.jsonl", 0),
1724
+ ("t1", "dir/a.jsonl", 1),
1725
+ ("t1", "dir/sub/b.jsonl", 0),
1726
+ ("t2", "dir/c.jsonl", 0),
1727
+ ("t1", "dir2/x.jsonl", 0),
1728
+ ("t1", "other.jsonl", 0),
1729
+ ] {
1730
+ db.insert_row(table, &row, file, idx).unwrap();
1731
+ }
1732
+
1733
+ assert_eq!(
1734
+ db.files_under("dir").unwrap(),
1735
+ vec![
1736
+ ("t1".to_string(), "dir/a.jsonl".to_string()),
1737
+ ("t1".to_string(), "dir/sub/b.jsonl".to_string()),
1738
+ ("t2".to_string(), "dir/c.jsonl".to_string()),
1739
+ ],
1740
+ "a multi-row file is listed once; `dir2/` is not beneath `dir`"
1741
+ );
1742
+ assert!(db.files_under("nothing").unwrap().is_empty());
1743
+ }
1744
+
1745
+ #[test]
1746
+ fn files_under_surfaces_sql_failure() {
1747
+ let db = Db::new().unwrap();
1748
+ db.conn
1749
+ .execute("DROP TABLE _dirsql_internal_rows", [])
1750
+ .unwrap();
1751
+ let err = db.files_under("dir").unwrap_err();
1752
+ assert!(matches!(err, DbError::Sqlite(_)), "got: {err:?}");
1753
+ }
1754
+
1703
1755
  #[test]
1704
1756
  fn delete_rows_by_file_removes_mapping() {
1705
1757
  let db = Db::new().unwrap();
@@ -2340,7 +2392,7 @@ mod tests {
2340
2392
  let mut db = Db::new().unwrap();
2341
2393
  db.set_path_table_root(PathBuf::from("/nonexistent-dirsql-root"));
2342
2394
 
2343
- let rows = db.query("SELECT path FROM './'").unwrap();
2395
+ let rows = db.query("SELECT path FROM './' LIMIT 1").unwrap();
2344
2396
 
2345
2397
  assert!(rows.is_empty(), "an empty root yields no rows: {rows:?}");
2346
2398
  }
@@ -2361,7 +2413,7 @@ mod tests {
2361
2413
  db.set_path_table_root(PathBuf::from("/nonexistent-dirsql-root"));
2362
2414
 
2363
2415
  let rows = db
2364
- .query("SELECT path FROM '/nonexistent-dirsql-dir/*.log'")
2416
+ .query("SELECT path FROM '/nonexistent-dirsql-dir/*.log' LIMIT 1")
2365
2417
  .unwrap();
2366
2418
 
2367
2419
  assert!(
@@ -39,6 +39,7 @@ pub mod scanner;
39
39
  pub mod sql_literal;
40
40
  #[doc(hidden)]
41
41
  pub mod vtab;
42
+ mod vtab_scaffold;
42
43
  #[doc(hidden)]
43
44
  pub mod watcher;
44
45
 
@@ -557,9 +558,6 @@ impl DirSQL {
557
558
  // unspecified. An `on_file` failure produces an error event for that
558
559
  // table only; the other matching tables still process the event.
559
560
  let matches = self.inner.matcher.match_all(&rel_path_buf);
560
- if matches.is_empty() {
561
- return Vec::new();
562
- }
563
561
  let rel_path = rel_path_buf.to_string_lossy().to_string();
564
562
 
565
563
  let mut events = Vec::new();
@@ -573,6 +571,33 @@ impl DirSQL {
573
571
  }
574
572
  }
575
573
  }
574
+ // The mirror of the walk above: a directory that leaves the tree
575
+ // (`rm -r`, or a rename out of or within the root) arrives as one
576
+ // event naming the directory and none for the files beneath it. It is
577
+ // already gone, so the rows are the only record of what it held.
578
+ if matches!(event, FileEvent::Deleted(_)) {
579
+ events.extend(self.delete_subtree(&rel_path));
580
+ }
581
+ events
582
+ }
583
+
584
+ /// Delete the rows of every file recorded beneath `rel_dir`, in every
585
+ /// table, as if each file had its own delete event.
586
+ fn delete_subtree(&self, rel_dir: &str) -> Vec<RowEvent> {
587
+ let files = {
588
+ let db = match self.inner.db.lock() {
589
+ Ok(db) => db,
590
+ Err(e) => return vec![error_event(None, rel_dir, e.to_string())],
591
+ };
592
+ match db.files_under(rel_dir) {
593
+ Ok(files) => files,
594
+ Err(e) => return vec![error_event(None, rel_dir, e.to_string())],
595
+ }
596
+ };
597
+ let mut events = Vec::new();
598
+ for (table, file_path) in files {
599
+ events.extend(self.handle_delete(&table, &file_path));
600
+ }
576
601
  events
577
602
  }
578
603
 
@@ -2753,6 +2778,153 @@ mod internal_tests {
2753
2778
  );
2754
2779
  }
2755
2780
 
2781
+ /// A one-table db whose fake fs stats every file in `files`, each already
2782
+ /// upserted so the db holds a row per file.
2783
+ fn populated_db(root: &Path, glob: &str, files: &[&str]) -> DirSQL {
2784
+ let mut fake = FakeFs::default();
2785
+ for f in files {
2786
+ fake.stats.insert(root.join(f), fake_stat());
2787
+ }
2788
+ let db = DirSQL::with_ignore_and_fs(
2789
+ root,
2790
+ vec![Table::new(
2791
+ "items",
2792
+ "CREATE TABLE items (name TEXT)",
2793
+ glob,
2794
+ |path| {
2795
+ vec![Row::from_iter([(
2796
+ "name".to_string(),
2797
+ Value::Text(path.to_string()),
2798
+ )])]
2799
+ },
2800
+ )],
2801
+ Vec::<String>::new(),
2802
+ Arc::new(fake),
2803
+ )
2804
+ .unwrap();
2805
+ for f in files {
2806
+ let events = db.handle_upsert("items", &root.join(f), f);
2807
+ assert!(
2808
+ matches!(&events[..], [RowEvent::Insert { .. }]),
2809
+ "got: {events:?}"
2810
+ );
2811
+ }
2812
+ db
2813
+ }
2814
+
2815
+ fn row_names(db: &DirSQL) -> Vec<String> {
2816
+ let mut names: Vec<String> = db
2817
+ .query("SELECT name FROM items")
2818
+ .unwrap()
2819
+ .into_iter()
2820
+ .filter_map(|r| match r.get("name") {
2821
+ Some(Value::Text(n)) => Some(n.clone()),
2822
+ _ => None,
2823
+ })
2824
+ .collect();
2825
+ names.sort();
2826
+ names
2827
+ }
2828
+
2829
+ fn deleted_paths(events: &[RowEvent]) -> Vec<String> {
2830
+ assert!(
2831
+ events.iter().all(|e| matches!(e, RowEvent::Delete { .. })),
2832
+ "expected only Delete events: {events:?}"
2833
+ );
2834
+ let mut paths: Vec<String> = events
2835
+ .iter()
2836
+ .filter_map(|e| match e {
2837
+ RowEvent::Delete { file_path, .. } => Some(file_path.clone()),
2838
+ _ => None,
2839
+ })
2840
+ .collect();
2841
+ paths.sort();
2842
+ paths
2843
+ }
2844
+
2845
+ /// A deleted directory matches no file glob, yet every row recorded
2846
+ /// beneath it goes, with one Delete per file. A sibling whose name merely
2847
+ /// starts with the directory's (`moved2/`) is untouched.
2848
+ #[test]
2849
+ fn process_file_event_deleted_directory_removes_the_rows_beneath_it() {
2850
+ let dir = TempDir::new().unwrap();
2851
+ let root = dir.path().to_path_buf();
2852
+ let files = ["moved/a.txt", "moved/one/deep.txt", "moved2/x.txt"];
2853
+ let db = populated_db(&root, "**/*.txt", &files);
2854
+
2855
+ let events = db.process_file_event(FileEvent::Deleted(root.join("moved")));
2856
+
2857
+ assert_eq!(
2858
+ deleted_paths(&events),
2859
+ vec!["moved/a.txt".to_string(), "moved/one/deep.txt".to_string()]
2860
+ );
2861
+ assert_eq!(
2862
+ row_names(&db),
2863
+ vec![root.join("moved2/x.txt").to_string_lossy().to_string()]
2864
+ );
2865
+ }
2866
+
2867
+ /// With a glob that also matches the directory itself (`**/*`), the
2868
+ /// exact-path delete finds no row and the subtree delete still runs.
2869
+ #[test]
2870
+ fn process_file_event_deleted_directory_matching_a_glob_removes_the_rows_beneath_it() {
2871
+ let dir = TempDir::new().unwrap();
2872
+ let root = dir.path().to_path_buf();
2873
+ let db = populated_db(&root, "**/*", &["moved/a.txt", "keep.txt"]);
2874
+
2875
+ let events = db.process_file_event(FileEvent::Deleted(root.join("moved")));
2876
+
2877
+ assert_eq!(deleted_paths(&events), vec!["moved/a.txt".to_string()]);
2878
+ assert_eq!(
2879
+ row_names(&db),
2880
+ vec![root.join("keep.txt").to_string_lossy().to_string()]
2881
+ );
2882
+ }
2883
+
2884
+ /// Only a *deleted* directory empties its subtree; a metadata change on
2885
+ /// one says nothing about its files.
2886
+ #[test]
2887
+ fn process_file_event_keeps_the_rows_beneath_a_modified_directory() {
2888
+ let dir = TempDir::new().unwrap();
2889
+ let root = dir.path().to_path_buf();
2890
+ let db = populated_db(&root, "**/*.txt", &["moved/a.txt"]);
2891
+
2892
+ let events = db.process_file_event(FileEvent::Modified(root.join("moved")));
2893
+
2894
+ assert!(events.is_empty(), "got: {events:?}");
2895
+ assert_eq!(
2896
+ row_names(&db),
2897
+ vec![root.join("moved/a.txt").to_string_lossy().to_string()]
2898
+ );
2899
+ }
2900
+
2901
+ #[test]
2902
+ fn delete_subtree_surfaces_db_poison() {
2903
+ let (_dir, db, _abs, _rel) = upsert_fixture();
2904
+ poison(&db.inner.db);
2905
+ let events = db.delete_subtree("moved");
2906
+ assert_single_lock_error(&events);
2907
+ }
2908
+
2909
+ #[test]
2910
+ fn delete_subtree_surfaces_db_failure() {
2911
+ let (_dir, db, _abs, _rel) = upsert_fixture();
2912
+ db.inner
2913
+ .db
2914
+ .lock()
2915
+ .unwrap()
2916
+ .conn()
2917
+ .execute("DROP TABLE _dirsql_internal_rows", [])
2918
+ .unwrap();
2919
+ let events = db.delete_subtree("moved");
2920
+ assert_eq!(events.len(), 1, "expected one error event: {events:?}");
2921
+ assert!(
2922
+ matches!(&events[0], RowEvent::Error { table: None, file_path, error }
2923
+ if file_path == Path::new("moved") && error.contains("no such table")),
2924
+ "got: {events:?}"
2925
+ );
2926
+ }
2927
+
2756
2928
  /// Building with a **relative** root canonicalizes `watch_root` to an
2757
2929
  /// absolute path while leaving `root` exactly as the caller supplied it,
2758
2930
  /// so `notify` never sees `.`.
@@ -3538,7 +3710,7 @@ mod internal_tests {
3538
3710
  let db = DirSQL::builder().root(dir.path()).build().unwrap();
3539
3711
 
3540
3712
  assert!(
3541
- db.query("SELECT path FROM './'").is_ok(),
3713
+ db.query("SELECT path FROM './' LIMIT 1").is_ok(),
3542
3714
  "the path-table fallback must be armed on an ephemeral db"
3543
3715
  );
3544
3716
  }
@@ -3579,7 +3751,7 @@ mod internal_tests {
3579
3751
  .unwrap();
3580
3752
 
3581
3753
  assert!(
3582
- db.query("SELECT path FROM './'").is_ok(),
3754
+ db.query("SELECT path FROM './' LIMIT 1").is_ok(),
3583
3755
  "the persist branch must arm the fallback too"
3584
3756
  );
3585
3757
  }
@@ -23,13 +23,11 @@
23
23
  use std::collections::{HashMap, HashSet};
24
24
  use std::ffi::c_int;
25
25
  use std::path::{Path, PathBuf};
26
+ use std::sync::Arc;
26
27
 
27
28
  use globset::GlobSet;
28
- use rusqlite::vtab::{
29
- Context, CreateVTab, IndexInfo, VTab, VTabConnection, VTabCursor, VTabKind, Values,
30
- read_only_module,
31
- };
32
- use rusqlite::{Connection, Error, Result, ffi};
29
+ use rusqlite::vtab::Context;
30
+ use rusqlite::{Connection, Error, Result};
33
31
 
34
32
  use crate::Value;
35
33
  use crate::command::{Placeholder, run_command};
@@ -38,16 +36,15 @@ use crate::matcher::TableMatcher;
38
36
  use crate::parsed_cache::{self, CachedParse, Entry, RowCache, SqliteRowCache};
39
37
  use crate::path_table;
40
38
  use crate::persist::{FileStat, hash_file, now_ns};
41
- use crate::scanner::{self, scan_glob};
42
- use crate::sql_literal::unquote;
39
+ use crate::scanner::scan_glob;
40
+ use crate::vtab_scaffold::{self, TableSource};
43
41
 
44
42
  /// SQL module name a parsed path-table is created with.
45
43
  pub const MODULE_NAME: &str = "dirsql_parsed";
46
44
 
47
45
  /// Register the parsed path-table module on `conn`.
48
46
  pub fn load_module(conn: &Connection) -> Result<()> {
49
- let aux: Option<()> = None;
50
- conn.create_module(MODULE_NAME, read_only_module::<ParsedTab>(), aux)
47
+ vtab_scaffold::load_module::<ParsedTable>(conn)
51
48
  }
52
49
 
53
50
  /// Number of module arguments that are not ignore patterns.
@@ -67,43 +64,32 @@ struct ModuleArgs {
67
64
  ignore: TableMatcher,
68
65
  }
69
66
 
70
- /// Compile the ignore patterns a parsed path-table scan applies.
71
- fn compile_ignore(patterns: &[String]) -> Result<TableMatcher> {
72
- let refs: Vec<&str> = patterns.iter().map(String::as_str).collect();
73
- TableMatcher::new(&[], &refs).map_err(|e| Error::ModuleError(e.to_string()))
74
- }
75
-
76
67
  /// Parse a parsed path-table's `CREATE VIRTUAL TABLE` arguments. `args[0..3]`
77
68
  /// are the module, database and table names; the module's own follow — root,
78
69
  /// glob, parser, the gitignore switch, the cache path, then any ignore
79
70
  /// patterns.
80
71
  fn parse_module_args(args: &[&[u8]]) -> Result<ModuleArgs> {
81
- let user_args: Vec<String> = args
82
- .iter()
83
- .skip(3)
84
- .map(|a| unquote(&String::from_utf8_lossy(a)).to_string())
85
- .collect();
72
+ let user_args = vtab_scaffold::user_args(args);
86
73
 
87
74
  let [root, pattern, command, gitignore, cache, ignore @ ..] = user_args.as_slice() else {
88
- return Err(Error::ModuleError(format!(
89
- "{MODULE_NAME} takes at least {FIXED_ARGS} arguments \
90
- (root, glob, parser, gitignore switch, cache path), got {}",
91
- user_args.len()
92
- )));
75
+ return Err(vtab_scaffold::arity_error(
76
+ MODULE_NAME,
77
+ FIXED_ARGS,
78
+ "root, glob, parser, gitignore switch, cache path",
79
+ user_args.len(),
80
+ ));
93
81
  };
94
82
 
95
- let glob = scanner::compile_glob(pattern).map_err(|e| Error::ModuleError(e.to_string()))?;
96
-
97
83
  Ok(ModuleArgs {
98
84
  root: PathBuf::from(root),
99
85
  pattern: pattern.clone(),
100
- glob,
86
+ glob: vtab_scaffold::compile_glob(pattern)?,
101
87
  command: command.clone(),
102
- gitignore: scanner::parse_gitignore_arg(gitignore).map_err(Error::ModuleError)?,
88
+ gitignore: vtab_scaffold::parse_gitignore(gitignore)?,
103
89
  // The empty string is how "no cache" is spelled: a module argument
104
90
  // cannot be absent without shifting every argument after it.
105
91
  cache: Some(PathBuf::from(cache)).filter(|p| !p.as_os_str().is_empty()),
106
- ignore: compile_ignore(ignore)?,
92
+ ignore: vtab_scaffold::compile_ignore(ignore)?,
107
93
  })
108
94
  }
109
95
 
@@ -321,37 +307,21 @@ fn column_name(names: &[String], i: c_int) -> Option<&str> {
321
307
  names.get(index).map(String::as_str)
322
308
  }
323
309
 
324
- /// Whether the cursor has run past the last row.
325
- fn at_eof(index: usize, row_count: usize) -> bool {
326
- index >= row_count
327
- }
328
-
329
- /// Rowid for a cursor position, saturating rather than wrapping on a row count
330
- /// no filesystem will produce.
331
- fn rowid_of(index: usize) -> i64 {
332
- i64::try_from(index).unwrap_or(i64::MAX)
333
- }
334
-
335
- #[repr(C)]
336
- struct ParsedTab {
337
- /// Base class. Must be first.
338
- base: ffi::sqlite3_vtab,
310
+ /// The table a `CREATE` produced: the inferred column names and the rows the
311
+ /// parsers emitted, both fixed for the life of the table.
312
+ struct ParsedTable {
339
313
  /// Only the names survive registration: the types live in the schema
340
314
  /// SQLite already holds, and a cursor addresses columns by index.
341
315
  column_names: Vec<String>,
342
- rows: Vec<JsonRow>,
316
+ rows: Arc<Vec<JsonRow>>,
343
317
  }
344
318
 
345
- #[expect(unsafe_code, reason = "rusqlite requires VTab to be an unsafe trait")]
346
- unsafe impl<'vtab> VTab<'vtab> for ParsedTab {
347
- type Aux = ();
348
- type Cursor = ParsedTabCursor;
319
+ impl TableSource for ParsedTable {
320
+ type Row = JsonRow;
321
+
322
+ const NAME: &'static str = MODULE_NAME;
349
323
 
350
- fn connect(
351
- _db: &mut VTabConnection,
352
- _aux: Option<&()>,
353
- args: &[&[u8]],
354
- ) -> Result<(String, Self)> {
324
+ fn connect(args: &[&[u8]]) -> Result<(String, Self)> {
355
325
  let ModuleArgs {
356
326
  root,
357
327
  pattern,
@@ -384,81 +354,25 @@ unsafe impl<'vtab> VTab<'vtab> for ParsedTab {
384
354
  }
385
355
 
386
356
  let schema = declared_schema(&columns);
387
- let vtab = Self {
388
- base: ffi::sqlite3_vtab::default(),
357
+ let table = Self {
389
358
  column_names: columns.into_iter().map(|c| c.name).collect(),
390
- rows,
359
+ rows: Arc::new(rows),
391
360
  };
392
- Ok((schema, vtab))
361
+ Ok((schema, table))
393
362
  }
394
363
 
395
- fn best_index(&self, info: &mut IndexInfo) -> Result<()> {
396
- info.set_estimated_cost(1000.);
397
- Ok(())
364
+ fn rows(&self) -> Arc<Vec<JsonRow>> {
365
+ // Materialized once at CREATE, so every statement reads the same rows
366
+ // rather than re-spawning a parser per file.
367
+ Arc::clone(&self.rows)
398
368
  }
399
369
 
400
- fn open(&'vtab mut self) -> Result<ParsedTabCursor> {
401
- Ok(ParsedTabCursor {
402
- base: ffi::sqlite3_vtab_cursor::default(),
403
- column_names: self.column_names.clone(),
404
- rows: self.rows.clone(),
405
- index: 0,
406
- })
407
- }
408
- }
409
-
410
- impl<'vtab> CreateVTab<'vtab> for ParsedTab {
411
- const KIND: VTabKind = VTabKind::Default;
412
- }
413
-
414
- #[repr(C)]
415
- struct ParsedTabCursor {
416
- /// Base class. Must be first: `rust_open` hands this pointer straight to
417
- /// SQLite as a `sqlite3_vtab_cursor`, so anything ahead of it gets
418
- /// overwritten.
419
- base: ffi::sqlite3_vtab_cursor,
420
- column_names: Vec<String>,
421
- rows: Vec<JsonRow>,
422
- index: usize,
423
- }
424
-
425
- #[expect(
426
- unsafe_code,
427
- reason = "rusqlite requires VTabCursor to be an unsafe trait"
428
- )]
429
- unsafe impl VTabCursor for ParsedTabCursor {
430
- fn filter(
431
- &mut self,
432
- _idx_num: c_int,
433
- _idx_str: Option<&str>,
434
- _args: &Values<'_>,
435
- ) -> Result<()> {
436
- self.index = 0;
437
- Ok(())
438
- }
439
-
440
- fn next(&mut self) -> Result<()> {
441
- self.index += 1;
442
- Ok(())
443
- }
444
-
445
- fn eof(&self) -> bool {
446
- at_eof(self.index, self.rows.len())
447
- }
448
-
449
- fn column(&self, ctx: &mut Context, i: c_int) -> Result<()> {
450
- let value = self
451
- .rows
452
- .get(self.index)
453
- .zip(column_name(&self.column_names, i))
454
- .map(|(row, name)| cell(row, name))
370
+ fn column(&self, row: &JsonRow, ctx: &mut Context, i: c_int) -> Result<()> {
371
+ let value = column_name(&self.column_names, i)
372
+ .map(|name| cell(row, name))
455
373
  .unwrap_or(Value::Null);
456
374
  ctx.set_result(&value)
457
375
  }
458
-
459
- fn rowid(&self) -> Result<i64> {
460
- Ok(rowid_of(self.index))
461
- }
462
376
  }
463
377
 
464
378
  #[cfg(test)]
@@ -729,34 +643,6 @@ mod tests {
729
643
  assert_eq!(column_name(&names(), -1), None);
730
644
  }
731
645
 
732
- #[test]
733
- fn at_eof_is_false_while_rows_remain() {
734
- assert!(!at_eof(0, 2));
735
- assert!(!at_eof(1, 2));
736
- }
737
-
738
- #[test]
739
- fn at_eof_is_true_once_past_the_last_row() {
740
- assert!(at_eof(2, 2));
741
- assert!(at_eof(3, 2));
742
- }
743
-
744
- #[test]
745
- fn at_eof_is_true_for_an_empty_row_set() {
746
- assert!(at_eof(0, 0));
747
- }
748
-
749
- #[test]
750
- fn rowid_of_tracks_the_cursor_position() {
751
- assert_eq!(rowid_of(0), 0);
752
- assert_eq!(rowid_of(7), 7);
753
- }
754
-
755
- #[test]
756
- fn rowid_of_saturates_rather_than_wrapping() {
757
- assert_eq!(rowid_of(usize::MAX), i64::MAX);
758
- }
759
-
760
646
  // The run_parser tests spawn a real `sh`. Their test code statically
761
647
  // references only `super::` items (plus pure std), matching the pattern
762
648
  // `command.rs` uses for the runner underneath.