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.
- {dirsql-0.4.48 → dirsql-0.4.50}/Cargo.lock +10 -2
- {dirsql-0.4.48 → dirsql-0.4.50}/PKG-INFO +1 -1
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/Cargo.toml +1 -1
- dirsql-0.4.50/packages/python/changelog.d/2026-09-18-flatten-row-event-in-core.md +5 -0
- dirsql-0.4.50/packages/python/e2e-attestations/claude-1130-codec-crates.json +7 -0
- dirsql-0.4.50/packages/python/e2e-attestations/claude-1131-flatten-row-event.json +7 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/src/lib.rs +8 -150
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/Cargo.toml +6 -1
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/serialize.rs +3 -15
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/functions.rs +6 -100
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/lib.rs +4 -0
- dirsql-0.4.50/packages/rust/src/row_event_flat.rs +170 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/Cargo.toml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/README.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/_async.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/main.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/plan_entry_path.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/py.typed +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/read_config.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/resolve_configs_extension_specs.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/dirsql/resolve_package.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/AGENTS.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/explanation.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/getting-started.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/define-tables.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/embed.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/load-extension.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/parse-files-into-columns.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/persist.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/query-json.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/query-without-config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/search-indexes.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/skip-files.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/index.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/package.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/plugins.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/cli.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/columns.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/hooks.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/http-api.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/path-tables.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/docs/reference/sdk.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/CHANGELOG.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/MIGRATIONS.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/README.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-24-hookless-table-config-error.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-24-remove-stat-fact-injection.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-07-29-restore-python-310.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-02-release-profile.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-02-scan-failures.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-03-discovery-ext-resolution.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-03-manylinux-dynamic-binary.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-03-python-no-ignore.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-04-cast-lints.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-04-cli-in-process.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-20-curtaincall-dev-dependency.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-08-20-declared-table-name.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-04-config-paths-from-argv-module.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-05-config-extension-loader-split.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-05-resolve-config-extensions-split.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-05-resolve-extension-split.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-09-launcher-config-path-scan.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-17-extension-resolution-via-core.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/2026-09-18-programmatic-extension-plan.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/changelog.d/README.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/conftest.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/define-tables.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/parse-files-into-columns.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/query-json.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/query-without-config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/search-indexes.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/index.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/package.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/plugins.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/cli.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/path-tables.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/docs/reference/sdk.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/cc-keen-knuth-f11gyq.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1056-py-ofpf9.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1066-p1-resolve-extension.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1078-argv-config-paths-core.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1079-p2-resolve-config-extensions.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1080-share-vtab-scaffolding.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1081-indicatif-progress.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1096-watch-moved-dir.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1109-watch-moved-out.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1119-ext-plan.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1120-py-ext.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-1122-ext-cleanup.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-819-oneshot-timeout.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-827-unquote-doubled-quotes.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-epic-953-slice2-reedline.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-epic-953-slice3-format.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-epic-953-stacked-prs-x59wm3.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-github-issue-825-e4mhhp.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-issue-951-0eease.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-issue-962-0jjrfv.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-issue-986-red-test-e7sjrb.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-1034-cachesplit.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-1042-py12.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-956-xzen74.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6-worker-calls.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-957-lrm0z6.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/e2e-attestations/claude-tackle-975-n8mc2h.json +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-24-hookless-table-config-error.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-24-remove-stat-fact-injection.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-07-29-restore-python-310.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-08-04-cli-in-process.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/2026-08-20-declared-table-name.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/migrations.d/README.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/testing-conventions.toml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/integration/binding/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/python/tests/integration/hermetic/__init__.py +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/CHANGELOG.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/MIGRATIONS.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/README.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/define-tables.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/parse-files-into-columns.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/query-json.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/query-without-config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/search-indexes.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/index.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/plugins.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/cli.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/config.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/path-tables.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/docs/reference/sdk.md +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/execute.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/repl.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/run.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/cli/table.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/command.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/config.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/db.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/default_config.toml +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/extension_resolution.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/infer.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/launcher.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/parsed_cache.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/parsed_vtab.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/path_table.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/progress.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/sql_literal.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/vtab.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/vtab_scaffold.rs +0 -0
- {dirsql-0.4.48 → dirsql-0.4.50}/packages/rust/src/watcher.rs +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -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.
|
|
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
|
|
@@ -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
|
|
283
|
+
let flat = flatten_row_event(event);
|
|
351
284
|
Ok(PyRowEvent {
|
|
352
|
-
table:
|
|
353
|
-
action:
|
|
354
|
-
row:
|
|
355
|
-
|
|
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:
|
|
365
|
-
file_path: Some(
|
|
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.
|
|
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(
|
|
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":
|
|
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 =
|
|
432
|
-
.
|
|
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
|
|
File without changes
|