dirsql 0.4.48__tar.gz → 0.4.50__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 (237) hide show
  1. {dirsql-0.4.48 → dirsql-0.4.50}/Cargo.lock +10 -2
  2. {dirsql-0.4.48 → dirsql-0.4.50}/PKG-INFO +1 -1
  3. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/Cargo.toml +1 -1
  4. dirsql-0.4.50/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +5 -0
  5. dirsql-0.4.50/packages/python/e2e-attestations/claude-1130-codec-crates.json +7 -0
  6. dirsql-0.4.50/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +7 -0
  7. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/src/lib.rs +8 -150
  8. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/Cargo.toml +6 -1
  9. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/serialize.rs +3 -15
  10. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/functions.rs +6 -100
  11. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/lib.rs +4 -0
  12. dirsql-0.4.50/packages/rust/src/row_event_flat.rs +170 -0
  13. {dirsql-0.4.48 → dirsql-0.4.50}/Cargo.toml +0 -0
  14. {dirsql-0.4.48 → dirsql-0.4.50}/README.md +0 -0
  15. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/__init__.py +0 -0
  16. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/_async.py +0 -0
  17. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/_dirsql.pyi +0 -0
  18. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/__init__.py +0 -0
  19. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/__init__.py +0 -0
  20. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
  21. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
  22. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
  23. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
  24. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
  25. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/main.py +0 -0
  26. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/resolve_config_extensions.py +0 -0
  27. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/plan_entry_path.py +0 -0
  28. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/py.typed +0 -0
  29. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/read_config.py +0 -0
  30. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/resolve_configs_extension_specs.py +0 -0
  31. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/resolve_extension.py +0 -0
  32. {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/resolve_package.py +0 -0
  33. {dirsql-0.4.48 → dirsql-0.4.50}/docs/.claude/CLAUDE.md +0 -0
  34. {dirsql-0.4.48 → dirsql-0.4.50}/docs/.vitepress/config.ts +0 -0
  35. {dirsql-0.4.48 → dirsql-0.4.50}/docs/.vitepress/theme/index.ts +0 -0
  36. {dirsql-0.4.48 → dirsql-0.4.50}/docs/.vitepress/theme/lang.ts +0 -0
  37. {dirsql-0.4.48 → dirsql-0.4.50}/docs/AGENTS.md +0 -0
  38. {dirsql-0.4.48 → dirsql-0.4.50}/docs/explanation.md +0 -0
  39. {dirsql-0.4.48 → dirsql-0.4.50}/docs/getting-started.md +0 -0
  40. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/columns-from-paths.md +0 -0
  41. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/define-tables.md +0 -0
  42. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/embed.md +0 -0
  43. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/extract-from-contents.md +0 -0
  44. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/load-extension.md +0 -0
  45. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/parse-files-into-columns.md +0 -0
  46. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/persist.md +0 -0
  47. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/query-json.md +0 -0
  48. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/query-without-config.md +0 -0
  49. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/react-to-changes.md +0 -0
  50. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/search-by-meaning.md +0 -0
  51. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/search-indexes.md +0 -0
  52. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/skip-files.md +0 -0
  53. {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/write-a-plugin.md +0 -0
  54. {dirsql-0.4.48 → dirsql-0.4.50}/docs/index.md +0 -0
  55. {dirsql-0.4.48 → dirsql-0.4.50}/docs/package.json +0 -0
  56. {dirsql-0.4.48 → dirsql-0.4.50}/docs/plugins.md +0 -0
  57. {dirsql-0.4.48 → dirsql-0.4.50}/docs/pnpm-lock.yaml +0 -0
  58. {dirsql-0.4.48 → dirsql-0.4.50}/docs/pnpm-workspace.yaml +0 -0
  59. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/cli.md +0 -0
  60. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/columns.md +0 -0
  61. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/config.md +0 -0
  62. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/hooks.md +0 -0
  63. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/http-api.md +0 -0
  64. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/path-tables.md +0 -0
  65. {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/sdk.md +0 -0
  66. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/CHANGELOG.md +0 -0
  67. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/MIGRATIONS.md +0 -0
  68. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/README.md +0 -0
  69. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  70. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
  71. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  72. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
  73. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  74. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
  75. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
  76. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
  77. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  78. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
  79. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
  80. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
  81. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
  82. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
  83. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
  84. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
  85. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
  86. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
  87. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
  88. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
  89. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
  90. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
  91. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
  92. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
  93. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
  94. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
  95. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/README.md +0 -0
  96. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/conftest.py +0 -0
  97. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.claude/CLAUDE.md +0 -0
  98. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.vitepress/config.ts +0 -0
  99. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  100. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  101. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/AGENTS.md +0 -0
  102. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/explanation.md +0 -0
  103. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/getting-started.md +0 -0
  104. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/columns-from-paths.md +0 -0
  105. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/define-tables.md +0 -0
  106. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/embed.md +0 -0
  107. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/extract-from-contents.md +0 -0
  108. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/load-extension.md +0 -0
  109. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
  110. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/persist.md +0 -0
  111. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/query-json.md +0 -0
  112. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/query-without-config.md +0 -0
  113. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/react-to-changes.md +0 -0
  114. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/search-by-meaning.md +0 -0
  115. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/search-indexes.md +0 -0
  116. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/skip-files.md +0 -0
  117. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/write-a-plugin.md +0 -0
  118. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/index.md +0 -0
  119. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/package.json +0 -0
  120. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/plugins.md +0 -0
  121. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/pnpm-lock.yaml +0 -0
  122. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/pnpm-workspace.yaml +0 -0
  123. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/cli.md +0 -0
  124. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/columns.md +0 -0
  125. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/config.md +0 -0
  126. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/hooks.md +0 -0
  127. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/http-api.md +0 -0
  128. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/path-tables.md +0 -0
  129. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/sdk.md +0 -0
  130. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
  131. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
  132. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
  133. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
  134. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
  135. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
  136. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
  137. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
  138. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
  139. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
  140. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
  141. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
  142. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
  143. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
  144. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
  145. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
  146. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
  147. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
  148. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
  149. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
  150. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
  151. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
  152. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
  153. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
  154. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
  155. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
  156. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
  157. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
  158. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
  159. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
  160. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
  161. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
  162. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
  163. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
  164. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
  165. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
  166. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
  167. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/README.md +0 -0
  168. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/testing-conventions.toml +0 -0
  169. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/__init__.py +0 -0
  170. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/conftest.py +0 -0
  171. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/e2e/__init__.py +0 -0
  172. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
  173. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
  174. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/integration/__init__.py +0 -0
  175. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/integration/binding/__init__.py +0 -0
  176. {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/integration/hermetic/__init__.py +0 -0
  177. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/CHANGELOG.md +0 -0
  178. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/MIGRATIONS.md +0 -0
  179. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/README.md +0 -0
  180. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/db_bench.rs +0 -0
  181. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/differ_bench.rs +0 -0
  182. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/matcher_bench.rs +0 -0
  183. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/scanner_bench.rs +0 -0
  184. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/explanation.md +0 -0
  185. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/getting-started.md +0 -0
  186. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  187. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/define-tables.md +0 -0
  188. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/embed.md +0 -0
  189. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  190. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/load-extension.md +0 -0
  191. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
  192. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/persist.md +0 -0
  193. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/query-json.md +0 -0
  194. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/query-without-config.md +0 -0
  195. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/react-to-changes.md +0 -0
  196. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  197. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/search-indexes.md +0 -0
  198. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/skip-files.md +0 -0
  199. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/write-a-plugin.md +0 -0
  200. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/index.md +0 -0
  201. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/plugins.md +0 -0
  202. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/cli.md +0 -0
  203. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/columns.md +0 -0
  204. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/config.md +0 -0
  205. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/hooks.md +0 -0
  206. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/http-api.md +0 -0
  207. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/path-tables.md +0 -0
  208. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/sdk.md +0 -0
  209. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/bin/dirsql.rs +0 -0
  210. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/execute.rs +0 -0
  211. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/init.rs +0 -0
  212. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/mod.rs +0 -0
  213. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/repl.rs +0 -0
  214. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/router.rs +0 -0
  215. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/run.rs +0 -0
  216. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/server.rs +0 -0
  217. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/table.rs +0 -0
  218. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/command.rs +0 -0
  219. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/config.rs +0 -0
  220. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/db.rs +0 -0
  221. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/default_config.toml +0 -0
  222. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/differ.rs +0 -0
  223. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/extension_resolution.rs +0 -0
  224. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/infer.rs +0 -0
  225. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/launcher.rs +0 -0
  226. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/matcher.rs +0 -0
  227. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/parsed_cache.rs +0 -0
  228. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/parsed_vtab.rs +0 -0
  229. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/path_table.rs +0 -0
  230. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/persist.rs +0 -0
  231. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/progress.rs +0 -0
  232. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/scanner.rs +0 -0
  233. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/sql_literal.rs +0 -0
  234. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/vtab.rs +0 -0
  235. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/vtab_scaffold.rs +0 -0
  236. {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/watcher.rs +0 -0
  237. {dirsql-0.4.48 → dirsql-0.4.50}/pyproject.toml +0 -0
@@ -552,10 +552,11 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
552
552
 
553
553
  [[package]]
554
554
  name = "dirsql"
555
- version = "0.4.48"
555
+ version = "0.4.50"
556
556
  dependencies = [
557
557
  "assert_cmd",
558
558
  "axum",
559
+ "base64 0.22.1",
559
560
  "blake3",
560
561
  "clap",
561
562
  "console",
@@ -566,6 +567,7 @@ dependencies = [
566
567
  "futures-executor",
567
568
  "futures-util",
568
569
  "globset",
570
+ "hex",
569
571
  "ignore",
570
572
  "indicatif",
571
573
  "libc",
@@ -597,7 +599,7 @@ dependencies = [
597
599
 
598
600
  [[package]]
599
601
  name = "dirsql-py-ext"
600
- version = "0.4.48"
602
+ version = "0.4.50"
601
603
  dependencies = [
602
604
  "dirsql",
603
605
  "pyo3",
@@ -974,6 +976,12 @@ version = "0.5.2"
974
976
  source = "registry+https://github.com/rust-lang/crates.io-index"
975
977
  checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
976
978
 
979
+ [[package]]
980
+ name = "hex"
981
+ version = "0.4.3"
982
+ source = "registry+https://github.com/rust-lang/crates.io-index"
983
+ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
984
+
977
985
  [[package]]
978
986
  name = "http"
979
987
  version = "0.2.12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.4.48
3
+ Version: 0.4.50
4
4
  Requires-Dist: bin-shim>=0.1
5
5
  Summary: Ephemeral SQL index over a local directory
6
6
  Keywords: sql,filesystem,directory,sqlite,index
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
4
4
  # pypi/maturin handler can rewrite it via `write-version` before
5
5
  # `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
6
6
  # and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
7
- version = "0.4.48"
7
+ version = "0.4.50"
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,5 @@
1
+ **Changed**
2
+
3
+ The extension module now flattens row events through the core's
4
+ `flatten_row_event` instead of its own copy of that match. No user-visible
5
+ change: `RowEvent` fields and `action` values are identical.
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1789748436,
4
+ "exit_code": 0,
5
+ "commit": "9468882ee26238c102d59814a212d6c7f9c6be92",
6
+ "branch": "claude/1130-codec-crates"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "just test-e2e",
3
+ "ran_at": 1789748553,
4
+ "exit_code": 0,
5
+ "commit": "d106fd4917b9dc93978646f6cb6d8f94374af617",
6
+ "branch": "claude/1131-flatten-row-event"
7
+ }
@@ -13,7 +13,7 @@
13
13
  #[cfg(feature = "extension-module")]
14
14
  mod python {
15
15
  use ::dirsql::extension_resolution::{self, ConfigSource, PlanEntry};
16
- use ::dirsql::{DirSQL, Extension, Row, RowEvent, Table, Value};
16
+ use ::dirsql::{DirSQL, Extension, Row, RowEvent, Table, Value, flatten_row_event};
17
17
  use pyo3::exceptions::{PyOverflowError, PyRuntimeError, PyValueError};
18
18
  use pyo3::prelude::*;
19
19
  use pyo3::types::{PyBool, PyByteArray, PyBytes, PyDict, PyInt, PyList};
@@ -279,90 +279,18 @@ mod python {
279
279
  PyRuntimeError::new_err(e.to_string())
280
280
  }
281
281
 
282
- /// Pure, GIL-free intermediate for a row event. [`row_event_to_plain`]
283
- /// builds it from a core [`RowEvent`] (unit-testable without a Python
284
- /// interpreter); [`row_event_to_py`] then marshals it into the
285
- /// Python-facing [`PyRowEvent`] (the GIL step).
286
- struct PlainRowEvent {
287
- table: Option<String>,
288
- action: &'static str,
289
- row: Option<Row>,
290
- old_row: Option<Row>,
291
- error: Option<String>,
292
- file_path: String,
293
- }
294
-
295
- fn row_event_to_plain(event: &RowEvent) -> PlainRowEvent {
296
- match event {
297
- RowEvent::Insert {
298
- table,
299
- row,
300
- file_path,
301
- } => PlainRowEvent {
302
- table: Some(table.clone()),
303
- action: "insert",
304
- row: Some(row.clone()),
305
- old_row: None,
306
- error: None,
307
- file_path: file_path.clone(),
308
- },
309
- RowEvent::Update {
310
- table,
311
- old_row,
312
- new_row,
313
- file_path,
314
- } => PlainRowEvent {
315
- table: Some(table.clone()),
316
- action: "update",
317
- row: Some(new_row.clone()),
318
- old_row: Some(old_row.clone()),
319
- error: None,
320
- file_path: file_path.clone(),
321
- },
322
- RowEvent::Delete {
323
- table,
324
- row,
325
- file_path,
326
- } => PlainRowEvent {
327
- table: Some(table.clone()),
328
- action: "delete",
329
- row: Some(row.clone()),
330
- old_row: None,
331
- error: None,
332
- file_path: file_path.clone(),
333
- },
334
- RowEvent::Error {
335
- table,
336
- file_path,
337
- error,
338
- } => PlainRowEvent {
339
- table: table.clone(),
340
- action: "error",
341
- row: None,
342
- old_row: None,
343
- error: Some(error.clone()),
344
- file_path: file_path.to_string_lossy().to_string(),
345
- },
346
- }
347
- }
348
-
349
282
  fn row_event_to_py(py: Python<'_>, event: &RowEvent) -> PyResult<PyRowEvent> {
350
- let plain = row_event_to_plain(event);
283
+ let flat = flatten_row_event(event);
351
284
  Ok(PyRowEvent {
352
- table: plain.table,
353
- action: plain.action.to_string(),
354
- row: plain
355
- .row
356
- .as_ref()
357
- .map(|r| value_row_to_py_dict(py, r))
358
- .transpose()?,
359
- old_row: plain
285
+ table: flat.table,
286
+ action: flat.action.to_string(),
287
+ row: flat.row.map(|r| value_row_to_py_dict(py, r)).transpose()?,
288
+ old_row: flat
360
289
  .old_row
361
- .as_ref()
362
290
  .map(|r| value_row_to_py_dict(py, r))
363
291
  .transpose()?,
364
- error: plain.error,
365
- file_path: Some(plain.file_path),
292
+ error: flat.error,
293
+ file_path: Some(flat.file_path),
366
294
  })
367
295
  }
368
296
 
@@ -609,76 +537,6 @@ mod python {
609
537
  mod tests {
610
538
  use super::*;
611
539
 
612
- fn one_row() -> HashMap<String, Value> {
613
- HashMap::from([("k".to_string(), Value::Integer(7))])
614
- }
615
-
616
- #[test]
617
- fn plain_insert_maps_row_and_action() {
618
- let p = row_event_to_plain(&RowEvent::Insert {
619
- table: "t".into(),
620
- row: one_row(),
621
- file_path: "/f".into(),
622
- });
623
- assert_eq!(p.action, "insert");
624
- assert_eq!(p.table.as_deref(), Some("t"));
625
- assert_eq!(
626
- p.row.as_ref().and_then(|r| r.get("k")),
627
- Some(&Value::Integer(7))
628
- );
629
- assert!(p.old_row.is_none());
630
- assert!(p.error.is_none());
631
- assert_eq!(p.file_path, "/f");
632
- }
633
-
634
- #[test]
635
- fn plain_update_carries_old_and_new() {
636
- let mut new = one_row();
637
- new.insert("k".to_string(), Value::Integer(9));
638
- let p = row_event_to_plain(&RowEvent::Update {
639
- table: "t".into(),
640
- old_row: one_row(),
641
- new_row: new,
642
- file_path: "/f".into(),
643
- });
644
- assert_eq!(p.action, "update");
645
- assert_eq!(
646
- p.row.as_ref().and_then(|r| r.get("k")),
647
- Some(&Value::Integer(9))
648
- );
649
- assert_eq!(
650
- p.old_row.as_ref().and_then(|r| r.get("k")),
651
- Some(&Value::Integer(7))
652
- );
653
- }
654
-
655
- #[test]
656
- fn plain_delete_has_row_no_old() {
657
- let p = row_event_to_plain(&RowEvent::Delete {
658
- table: "t".into(),
659
- row: one_row(),
660
- file_path: "/f".into(),
661
- });
662
- assert_eq!(p.action, "delete");
663
- assert!(p.row.is_some());
664
- assert!(p.old_row.is_none());
665
- }
666
-
667
- #[test]
668
- fn plain_error_has_no_row_and_optional_table() {
669
- let p = row_event_to_plain(&RowEvent::Error {
670
- table: None,
671
- file_path: std::path::PathBuf::from("/f"),
672
- error: "boom".into(),
673
- });
674
- assert_eq!(p.action, "error");
675
- assert!(p.table.is_none());
676
- assert!(p.row.is_none());
677
- assert!(p.old_row.is_none());
678
- assert_eq!(p.error.as_deref(), Some("boom"));
679
- assert_eq!(p.file_path, "/f");
680
- }
681
-
682
540
  #[test]
683
541
  fn on_file_error_displays_inner() {
684
542
  assert_eq!(OnFileError("bad".to_string()).to_string(), "bad");
@@ -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.48"
9
+ version = "0.4.50"
10
10
  edition.workspace = true
11
11
  # Literal `license` (not `license.workspace = true`) because
12
12
  # putitoutthere's preflight check reads `[package].license` directly
@@ -47,6 +47,7 @@ cli = [
47
47
  "dep:axum",
48
48
  "dep:clap",
49
49
  "dep:futures",
50
+ "dep:hex",
50
51
  "dep:reedline",
51
52
  "dep:tokio-stream",
52
53
  "tokio/rt-multi-thread",
@@ -57,12 +58,16 @@ cli = [
57
58
 
58
59
  [dependencies]
59
60
  axum = { version = "0.8", optional = true }
61
+ base64 = "0.22"
60
62
  blake3 = "1"
61
63
  clap = { version = "4", features = ["derive"], optional = true }
62
64
  console = "0.16.6"
63
65
  futures = { version = "0.3", optional = true }
64
66
  futures-channel.workspace = true
65
67
  globset = "0.4"
68
+ # Hex rendering of BLOB cells in the CLI's JSON output. CLI-only, so a
69
+ # library consumer never pulls it.
70
+ hex = { version = "0.4", optional = true }
66
71
  # Hierarchical `.gitignore` matching for path-table scans (the fd/ripgrep
67
72
  # semantics), without adopting its walker: the scan keeps `walkdir`.
68
73
  ignore = "0.4"
@@ -26,19 +26,10 @@ fn cell_to_json(value: &CellValue) -> Value {
26
26
  .map(Value::Number)
27
27
  .unwrap_or(Value::Null),
28
28
  CellValue::Text(s) => Value::String(s.clone()),
29
- CellValue::Blob(bytes) => Value::String(hex_encode(bytes)),
29
+ CellValue::Blob(bytes) => Value::String(hex::encode(bytes)),
30
30
  }
31
31
  }
32
32
 
33
- /// Hex-encode a byte slice, for `BLOB` SQLite values in JSON output.
34
- fn hex_encode(bytes: &[u8]) -> String {
35
- let mut out = String::with_capacity(bytes.len() * 2);
36
- for b in bytes {
37
- out.push_str(&format!("{b:02x}"));
38
- }
39
- out
40
- }
41
-
42
33
  pub(super) fn event_to_json(event: &RowEvent) -> String {
43
34
  event_to_value(event).to_string()
44
35
  }
@@ -198,6 +189,8 @@ mod tests {
198
189
  fn blob_cell_becomes_hex_string() {
199
190
  let json = cell_to_json(&CellValue::Blob(vec![0xde, 0xad, 0xbe, 0xef]));
200
191
  assert_eq!(json.as_str(), Some("deadbeef"));
192
+ let padded = cell_to_json(&CellValue::Blob(vec![0x00, 0x0f, 0xff]));
193
+ assert_eq!(padded.as_str(), Some("000fff"));
201
194
  }
202
195
 
203
196
  #[test]
@@ -205,9 +198,4 @@ mod tests {
205
198
  assert!(cell_to_json(&CellValue::Real(f64::NAN)).is_null());
206
199
  assert!(cell_to_json(&CellValue::Real(f64::INFINITY)).is_null());
207
200
  }
208
-
209
- #[test]
210
- fn hex_encode_matches_lowercase_spec() {
211
- assert_eq!(hex_encode(&[0x00, 0x0f, 0xff, 0xde, 0xad]), "000fffdead");
212
- }
213
201
  }
@@ -38,6 +38,8 @@ use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
38
38
  use std::sync::{Arc, Mutex};
39
39
  use std::time::Duration;
40
40
 
41
+ use base64::Engine as _;
42
+ use base64::engine::general_purpose::STANDARD;
41
43
  use rusqlite::Connection;
42
44
  use rusqlite::functions::FunctionFlags;
43
45
 
@@ -357,7 +359,7 @@ fn value_to_json(value: &Value) -> serde_json::Value {
357
359
  .map(serde_json::Value::Number)
358
360
  .unwrap_or(serde_json::Value::Null),
359
361
  Value::Text(s) => serde_json::Value::from(s.as_str()),
360
- Value::Blob(bytes) => serde_json::json!({ "$bytes": base64_encode(bytes) }),
362
+ Value::Blob(bytes) => serde_json::json!({ "$bytes": STANDARD.encode(bytes) }),
361
363
  }
362
364
  }
363
365
 
@@ -428,77 +430,15 @@ fn json_to_sql_value(value: &serde_json::Value) -> Result<Value, String> {
428
430
  let encoded = map["$bytes"]
429
431
  .as_str()
430
432
  .ok_or_else(|| "\"$bytes\" must be a base64 string".to_string())?;
431
- let bytes = base64_decode(encoded)
432
- .ok_or_else(|| "\"$bytes\" is not valid base64".to_string())?;
433
+ let bytes = STANDARD
434
+ .decode(encoded)
435
+ .map_err(|_| "\"$bytes\" is not valid base64".to_string())?;
433
436
  Ok(Value::Blob(bytes))
434
437
  }
435
438
  other => Ok(Value::Text(other.to_string())),
436
439
  }
437
440
  }
438
441
 
439
- const BASE64_ALPHABET: &[u8; 64] =
440
- b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
441
-
442
- /// Standard base64 with padding. Hand-rolled (~20 lines) rather than a new
443
- /// dependency for one wire field.
444
- fn base64_encode(bytes: &[u8]) -> String {
445
- let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
446
- for chunk in bytes.chunks(3) {
447
- let b0 = u32::from(chunk[0]);
448
- let b1 = chunk.get(1).map(|b| u32::from(*b));
449
- let b2 = chunk.get(2).map(|b| u32::from(*b));
450
- let triple = (b0 << 16) | (b1.unwrap_or(0) << 8) | b2.unwrap_or(0);
451
- let index = |shift: u32| BASE64_ALPHABET[(triple >> shift & 0x3f) as usize] as char;
452
- out.push(index(18));
453
- out.push(index(12));
454
- out.push(if b1.is_some() { index(6) } else { '=' });
455
- out.push(if b2.is_some() { index(0) } else { '=' });
456
- }
457
- out
458
- }
459
-
460
- /// Decode standard base64 (padding required). `None` on any malformed input.
461
- fn base64_decode(text: &str) -> Option<Vec<u8>> {
462
- let bytes = text.as_bytes();
463
- if !bytes.len().is_multiple_of(4) {
464
- return None;
465
- }
466
- let value_of = |b: u8| -> Option<u32> {
467
- BASE64_ALPHABET
468
- .iter()
469
- .position(|c| *c == b)
470
- .map(|i| u32::try_from(i).expect("index < 64"))
471
- };
472
- let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
473
- for (i, chunk) in bytes.chunks(4).enumerate() {
474
- let last = (i + 1) * 4 == bytes.len();
475
- let pad = chunk.iter().filter(|b| **b == b'=').count();
476
- // Padding may only close the final chunk, as its last one or two chars.
477
- if pad > 0 && (!last || pad > 2 || chunk[..4 - pad].contains(&b'=')) {
478
- return None;
479
- }
480
- let mut triple: u32 = 0;
481
- for b in &chunk[..4 - pad] {
482
- triple = (triple << 6) | value_of(*b)?;
483
- }
484
- triple <<= 6 * u32::try_from(pad).expect("pad <= 2");
485
- #[expect(
486
- clippy::cast_possible_truncation,
487
- reason = "each shift isolates one byte"
488
- )]
489
- {
490
- out.push((triple >> 16) as u8);
491
- if pad < 2 {
492
- out.push((triple >> 8) as u8);
493
- }
494
- if pad < 1 {
495
- out.push(triple as u8);
496
- }
497
- }
498
- }
499
- Some(out)
500
- }
501
-
502
442
  /// The production transport: the spawned worker process, its piped stdin,
503
443
  /// and a reader thread draining its stdout line-by-line into a channel (a
504
444
  /// blocking read has no timeout; `recv_timeout` on the channel does).
@@ -902,40 +842,6 @@ mod tests {
902
842
  );
903
843
  }
904
844
 
905
- // --- base64 ------------------------------------------------------------
906
-
907
- #[test]
908
- fn base64_round_trips_every_padding_length() {
909
- for bytes in [
910
- &b""[..],
911
- &b"f"[..],
912
- &b"fo"[..],
913
- &b"foo"[..],
914
- &b"foob"[..],
915
- &[0u8, 255, 16, 3][..],
916
- ] {
917
- let encoded = base64_encode(bytes);
918
- assert_eq!(base64_decode(&encoded).as_deref(), Some(bytes), "{encoded}");
919
- }
920
- }
921
-
922
- #[test]
923
- fn base64_known_vectors() {
924
- assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
925
- assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
926
- assert_eq!(base64_encode(&[1, 2]), "AQI=");
927
- assert_eq!(base64_decode("Zm9vYmFy").as_deref(), Some(&b"foobar"[..]));
928
- }
929
-
930
- #[test]
931
- fn base64_decode_rejects_malformed_input() {
932
- assert_eq!(base64_decode("abc"), None); // not a multiple of 4
933
- assert_eq!(base64_decode("a!=="), None); // bad alphabet
934
- assert_eq!(base64_decode("ab=c"), None); // padding inside a chunk
935
- assert_eq!(base64_decode("ab==cd=="), None); // padding before the last chunk
936
- assert_eq!(base64_decode("a==="), None); // over-padding
937
- }
938
-
939
845
  // --- defaults -----------------------------------------------------------
940
846
 
941
847
  #[test]
@@ -36,6 +36,8 @@ pub mod persist;
36
36
  #[doc(hidden)]
37
37
  pub mod progress;
38
38
  #[doc(hidden)]
39
+ pub mod row_event_flat;
40
+ #[doc(hidden)]
39
41
  pub mod scanner;
40
42
  #[doc(hidden)]
41
43
  pub mod sql_literal;
@@ -78,6 +80,8 @@ pub use crate::config::ExtensionSpec as Extension;
78
80
  pub use crate::db::{DbError, Value};
79
81
  pub use crate::differ::RowEvent;
80
82
  #[doc(hidden)]
83
+ pub use crate::row_event_flat::{FlatRowEvent, flatten_row_event};
84
+ #[doc(hidden)]
81
85
  pub use crate::watcher::FileEvent as RawFileEvent;
82
86
 
83
87
  pub type Row = HashMap<String, Value>;
@@ -0,0 +1,170 @@
1
+ //! Flattening a [`RowEvent`] into the six-field shape both bindings expose.
2
+ //!
3
+ //! Every binding publishes the same `{table, action, row, old_row, error,
4
+ //! file_path}` record, and none of the decisions behind it are host-specific:
5
+ //! which variant carries an old row, which one may have no table, and the
6
+ //! lowercase action names all belong to the core enum. Only the marshaling of
7
+ //! the borrowed rows into host values is per-binding.
8
+ //!
9
+ //! Rows are borrowed so the clone stays at the binding boundary, where it is
10
+ //! already paid.
11
+
12
+ use crate::Row;
13
+ #[cfg(test)]
14
+ use crate::db::Value;
15
+ use crate::differ::RowEvent;
16
+
17
+ /// A [`RowEvent`] reduced to the flat record the bindings publish.
18
+ #[derive(Debug, Clone, PartialEq)]
19
+ pub struct FlatRowEvent<'a> {
20
+ pub table: Option<String>,
21
+ pub action: &'static str,
22
+ pub row: Option<&'a Row>,
23
+ pub old_row: Option<&'a Row>,
24
+ pub error: Option<String>,
25
+ pub file_path: String,
26
+ }
27
+
28
+ pub fn flatten_row_event(event: &RowEvent) -> FlatRowEvent<'_> {
29
+ match event {
30
+ RowEvent::Insert {
31
+ table,
32
+ row,
33
+ file_path,
34
+ } => FlatRowEvent {
35
+ table: Some(table.clone()),
36
+ action: "insert",
37
+ row: Some(row),
38
+ old_row: None,
39
+ error: None,
40
+ file_path: file_path.clone(),
41
+ },
42
+ RowEvent::Update {
43
+ table,
44
+ old_row,
45
+ new_row,
46
+ file_path,
47
+ } => FlatRowEvent {
48
+ table: Some(table.clone()),
49
+ action: "update",
50
+ row: Some(new_row),
51
+ old_row: Some(old_row),
52
+ error: None,
53
+ file_path: file_path.clone(),
54
+ },
55
+ RowEvent::Delete {
56
+ table,
57
+ row,
58
+ file_path,
59
+ } => FlatRowEvent {
60
+ table: Some(table.clone()),
61
+ action: "delete",
62
+ row: Some(row),
63
+ old_row: None,
64
+ error: None,
65
+ file_path: file_path.clone(),
66
+ },
67
+ RowEvent::Error {
68
+ table,
69
+ file_path,
70
+ error,
71
+ } => FlatRowEvent {
72
+ table: table.clone(),
73
+ action: "error",
74
+ row: None,
75
+ old_row: None,
76
+ error: Some(error.clone()),
77
+ file_path: file_path.to_string_lossy().to_string(),
78
+ },
79
+ }
80
+ }
81
+
82
+ #[cfg(test)]
83
+ mod tests {
84
+ use std::path::PathBuf;
85
+
86
+ use super::*;
87
+
88
+ fn one_row(v: i64) -> Row {
89
+ Row::from([("k".to_string(), Value::Integer(v))])
90
+ }
91
+
92
+ #[test]
93
+ fn insert_maps_row_and_action() {
94
+ let event = RowEvent::Insert {
95
+ table: "t".into(),
96
+ row: one_row(7),
97
+ file_path: "/f".into(),
98
+ };
99
+ let flat = flatten_row_event(&event);
100
+ assert_eq!(flat.action, "insert");
101
+ assert_eq!(flat.table.as_deref(), Some("t"));
102
+ assert_eq!(flat.row.and_then(|r| r.get("k")), Some(&Value::Integer(7)));
103
+ assert!(flat.old_row.is_none());
104
+ assert!(flat.error.is_none());
105
+ assert_eq!(flat.file_path, "/f");
106
+ }
107
+
108
+ #[test]
109
+ fn update_carries_new_row_as_row_and_old_row_beside_it() {
110
+ let event = RowEvent::Update {
111
+ table: "t".into(),
112
+ old_row: one_row(7),
113
+ new_row: one_row(9),
114
+ file_path: "/f".into(),
115
+ };
116
+ let flat = flatten_row_event(&event);
117
+ assert_eq!(flat.action, "update");
118
+ assert_eq!(flat.table.as_deref(), Some("t"));
119
+ assert_eq!(flat.row.and_then(|r| r.get("k")), Some(&Value::Integer(9)));
120
+ assert_eq!(
121
+ flat.old_row.and_then(|r| r.get("k")),
122
+ Some(&Value::Integer(7))
123
+ );
124
+ assert!(flat.error.is_none());
125
+ assert_eq!(flat.file_path, "/f");
126
+ }
127
+
128
+ #[test]
129
+ fn delete_has_a_row_but_no_old_row() {
130
+ let event = RowEvent::Delete {
131
+ table: "t".into(),
132
+ row: one_row(7),
133
+ file_path: "/f".into(),
134
+ };
135
+ let flat = flatten_row_event(&event);
136
+ assert_eq!(flat.action, "delete");
137
+ assert_eq!(flat.table.as_deref(), Some("t"));
138
+ assert_eq!(flat.row.and_then(|r| r.get("k")), Some(&Value::Integer(7)));
139
+ assert!(flat.old_row.is_none());
140
+ assert!(flat.error.is_none());
141
+ assert_eq!(flat.file_path, "/f");
142
+ }
143
+
144
+ #[test]
145
+ fn error_has_no_rows_and_stringifies_its_path() {
146
+ let event = RowEvent::Error {
147
+ table: None,
148
+ file_path: PathBuf::from("/f"),
149
+ error: "boom".into(),
150
+ };
151
+ let flat = flatten_row_event(&event);
152
+ assert_eq!(flat.action, "error");
153
+ assert!(flat.table.is_none());
154
+ assert!(flat.row.is_none());
155
+ assert!(flat.old_row.is_none());
156
+ assert_eq!(flat.error.as_deref(), Some("boom"));
157
+ assert_eq!(flat.file_path, "/f");
158
+ }
159
+
160
+ #[test]
161
+ fn error_keeps_the_table_when_the_variant_names_one() {
162
+ let event = RowEvent::Error {
163
+ table: Some("t".into()),
164
+ file_path: PathBuf::from("/f"),
165
+ error: "boom".into(),
166
+ };
167
+ let flat = flatten_row_event(&event);
168
+ assert_eq!(flat.table.as_deref(), Some("t"));
169
+ }
170
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes