dirsql 0.3.118__tar.gz → 0.3.119__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.3.118 → dirsql-0.3.119}/Cargo.lock +1 -1
- {dirsql-0.3.118 → dirsql-0.3.119}/PKG-INFO +1 -1
- {dirsql-0.3.118/packages/rust → dirsql-0.3.119}/docs/reference/cli.md +19 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/reference/path-tables.md +33 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/cli.md +19 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/path-tables.md +33 -0
- dirsql-0.3.119/packages/python/e2e-attestations/feat-631-on-file-flag.json +7 -0
- dirsql-0.3.119/packages/rust/changelog.d/2026-07-21-on-file-flag.md +14 -0
- {dirsql-0.3.118 → dirsql-0.3.119/packages/rust}/docs/reference/cli.md +19 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/reference/path-tables.md +33 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/bin/dirsql.rs +131 -6
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/db.rs +103 -12
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/lib.rs +69 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/parsed_vtab.rs +139 -63
- dirsql-0.3.118/packages/python/e2e-attestations/feat-628-path-table-glob-semantics.json +0 -7
- {dirsql-0.3.118 → dirsql-0.3.119}/Cargo.toml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/_async.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/discover_plugins/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/discover_plugins/discovered_fragments.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/discover_plugins/discovery_disabled.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/discover_plugins/fragment_path.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/discover_plugins/user_passed_config.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/discover_plugins/with_discovered_plugins.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/py.typed +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/AGENTS.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/explanation.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/getting-started.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/embed.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/persist.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/query-without-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/index.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/migrations.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/package.json +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/reference/columns.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/reference/config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/reference/hooks.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/reference/http-api.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/docs/reference/sdk.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/CHANGELOG.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/MIGRATIONS.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-13-plugin-discovery.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-13-repeatable-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/changelog.d/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/conftest.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/query-without-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/docs/reference/sdk.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/e2e-attestations/feat-655-drop-glob-captures.json +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/migrations.d/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/e2e/fixtures/dirsql_plugin_fixture/dirsql.toml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/integration/binding/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/python/tests/integration/hermetic/__init__.py +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/CHANGELOG.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/MIGRATIONS.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-batch-ingest-transaction.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-chained-pre-post-query-hooks.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-fan-out-file-table-matching.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-persist-wal.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-repeatable-builder-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-repeatable-cli-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-13-subcommand-local-config-flags.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-19-path-table-query-fallback.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-19-path-table-vtab.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-20-path-table-glob-semantics.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-20-schema-inference.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/changelog.d/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/define-tables.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/query-without-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/howto/write-a-plugin.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/reference/config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/docs/reference/sdk.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-13-drop-cwd-config-auto-detect.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-13-fan-out-file-table-matching.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-13-persist-wal.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-13-rename-extract-to-on-file.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-13-sdk-default-baked-in-config.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-13-subcommand-local-config-flags.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-20-drop-implicit-files-table.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-20-path-table-glob-semantics.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/2026-07-21-remove-glob-captures.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/migrations.d/README.md +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/cli/execute.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/default_config.toml +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/infer.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/path_table.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/vtab.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.118 → dirsql-0.3.119}/pyproject.toml +0 -0
|
@@ -151,6 +151,25 @@ uses**, so behavior is identical to `POST /query` by construction:
|
|
|
151
151
|
statements; dirsql separately never modifies the files it indexes — see
|
|
152
152
|
[Read-only by design](../explanation#read-only-by-design).
|
|
153
153
|
|
|
154
|
+
#### `--on-file <command>`
|
|
155
|
+
|
|
156
|
+
Attach a parser to every [path-table](./path-tables.md#parsing-rows-with-on-file)
|
|
157
|
+
in the query, so each matched file yields the rows the command prints (a JSON
|
|
158
|
+
array of row objects) instead of the stat columns:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
162
|
+
--on-file 'extract.py {path}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The command follows the [`on-file` hook contract](./hooks.md#on-file) — argv
|
|
166
|
+
splitting, `{path}`/`{root}` placeholders, per-file failure isolation, and the
|
|
167
|
+
timeout. The parser's output is the whole schema; the stat columns are not
|
|
168
|
+
reachable on a parsed path-table. `--on-file` may be given **at most once** (a
|
|
169
|
+
repeat is an error pointing at config files) and never touches config-declared
|
|
170
|
+
tables. It is a `query`-only flag — server mode rejects it as an unknown
|
|
171
|
+
argument.
|
|
172
|
+
|
|
154
173
|
Errors print the same diagnostic the HTTP `{"error": …}` body carries —
|
|
155
174
|
config failures, SQL errors, rejected reads, hook failures, timeouts — to
|
|
156
175
|
stderr, with exit code `1`.
|
|
@@ -114,6 +114,39 @@ SELECT path FROM './docs/*.md' WHERE content LIKE '%deprecated%';
|
|
|
114
114
|
A file that cannot be read, or is not valid UTF-8, yields `NULL` content rather
|
|
115
115
|
than failing the query.
|
|
116
116
|
|
|
117
|
+
## Parsing rows with `--on-file`
|
|
118
|
+
|
|
119
|
+
By default a path-table's columns are the stat columns above — one row per
|
|
120
|
+
file. When you want *one row per record inside* each file, attach a parser with
|
|
121
|
+
the `dirsql query` flag [`--on-file`](/reference/cli):
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
125
|
+
--on-file 'extract.py {path}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The command runs once per matched file and prints a JSON array of row objects,
|
|
129
|
+
exactly like a declared table's [`on-file` hook](/reference/hooks) — same argv
|
|
130
|
+
splitting, same `{path}`/`{root}` placeholders, same timeout. Its output *is*
|
|
131
|
+
the table:
|
|
132
|
+
|
|
133
|
+
- **The parser supplies the whole schema.** Columns are inferred from the keys
|
|
134
|
+
across the emitted rows. The stat columns (`path`, `size`, …) are **not**
|
|
135
|
+
reachable on a parsed path-table — a parser that wants the path emits it (it
|
|
136
|
+
has `{path}`). The two modes stay cleanly separate.
|
|
137
|
+
- **Failures are isolated per file.** A file whose parser fails (spawn, non-zero
|
|
138
|
+
exit, timeout, or no output) or whose output is not a JSON array of rows
|
|
139
|
+
contributes no rows; a one-line warning naming the file goes to stderr and the
|
|
140
|
+
scan continues. The schema is inferred from the files that did parse.
|
|
141
|
+
- **The skip rules still apply.** A parsed scan honors the same `node_modules`
|
|
142
|
+
/`.git`/`ignore` rules a stat scan does (see below).
|
|
143
|
+
|
|
144
|
+
`--on-file` applies to **every** path-table in the query and may be given **at
|
|
145
|
+
most once**. For different parsers per file set, define named tables in a
|
|
146
|
+
`.dirsql.toml` with their own `on-file` keys and pass it with `-c` — the flag
|
|
147
|
+
never touches config-declared tables. It is a `query`-subcommand flag; server
|
|
148
|
+
mode does not accept it.
|
|
149
|
+
|
|
117
150
|
## Freshness and scope
|
|
118
151
|
|
|
119
152
|
A path-table is scanned when the statement runs, so it always reflects the
|
|
@@ -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.3.
|
|
7
|
+
version = "0.3.119"
|
|
8
8
|
# abi3 note: the `abi3-py311` pyo3 feature (below) builds ONE stable-ABI
|
|
9
9
|
# `cp311-abi3` wheel per platform that loads on every CPython >= 3.11
|
|
10
10
|
# (matching `pyproject.toml`'s `requires-python = ">=3.11"`), instead of
|
|
@@ -151,6 +151,25 @@ uses**, so behavior is identical to `POST /query` by construction:
|
|
|
151
151
|
statements; dirsql separately never modifies the files it indexes — see
|
|
152
152
|
[Read-only by design](../explanation#read-only-by-design).
|
|
153
153
|
|
|
154
|
+
#### `--on-file <command>`
|
|
155
|
+
|
|
156
|
+
Attach a parser to every [path-table](./path-tables.md#parsing-rows-with-on-file)
|
|
157
|
+
in the query, so each matched file yields the rows the command prints (a JSON
|
|
158
|
+
array of row objects) instead of the stat columns:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
162
|
+
--on-file 'extract.py {path}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The command follows the [`on-file` hook contract](./hooks.md#on-file) — argv
|
|
166
|
+
splitting, `{path}`/`{root}` placeholders, per-file failure isolation, and the
|
|
167
|
+
timeout. The parser's output is the whole schema; the stat columns are not
|
|
168
|
+
reachable on a parsed path-table. `--on-file` may be given **at most once** (a
|
|
169
|
+
repeat is an error pointing at config files) and never touches config-declared
|
|
170
|
+
tables. It is a `query`-only flag — server mode rejects it as an unknown
|
|
171
|
+
argument.
|
|
172
|
+
|
|
154
173
|
Errors print the same diagnostic the HTTP `{"error": …}` body carries —
|
|
155
174
|
config failures, SQL errors, rejected reads, hook failures, timeouts — to
|
|
156
175
|
stderr, with exit code `1`.
|
|
@@ -114,6 +114,39 @@ SELECT path FROM './docs/*.md' WHERE content LIKE '%deprecated%';
|
|
|
114
114
|
A file that cannot be read, or is not valid UTF-8, yields `NULL` content rather
|
|
115
115
|
than failing the query.
|
|
116
116
|
|
|
117
|
+
## Parsing rows with `--on-file`
|
|
118
|
+
|
|
119
|
+
By default a path-table's columns are the stat columns above — one row per
|
|
120
|
+
file. When you want *one row per record inside* each file, attach a parser with
|
|
121
|
+
the `dirsql query` flag [`--on-file`](/reference/cli):
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
125
|
+
--on-file 'extract.py {path}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The command runs once per matched file and prints a JSON array of row objects,
|
|
129
|
+
exactly like a declared table's [`on-file` hook](/reference/hooks) — same argv
|
|
130
|
+
splitting, same `{path}`/`{root}` placeholders, same timeout. Its output *is*
|
|
131
|
+
the table:
|
|
132
|
+
|
|
133
|
+
- **The parser supplies the whole schema.** Columns are inferred from the keys
|
|
134
|
+
across the emitted rows. The stat columns (`path`, `size`, …) are **not**
|
|
135
|
+
reachable on a parsed path-table — a parser that wants the path emits it (it
|
|
136
|
+
has `{path}`). The two modes stay cleanly separate.
|
|
137
|
+
- **Failures are isolated per file.** A file whose parser fails (spawn, non-zero
|
|
138
|
+
exit, timeout, or no output) or whose output is not a JSON array of rows
|
|
139
|
+
contributes no rows; a one-line warning naming the file goes to stderr and the
|
|
140
|
+
scan continues. The schema is inferred from the files that did parse.
|
|
141
|
+
- **The skip rules still apply.** A parsed scan honors the same `node_modules`
|
|
142
|
+
/`.git`/`ignore` rules a stat scan does (see below).
|
|
143
|
+
|
|
144
|
+
`--on-file` applies to **every** path-table in the query and may be given **at
|
|
145
|
+
most once**. For different parsers per file set, define named tables in a
|
|
146
|
+
`.dirsql.toml` with their own `on-file` keys and pass it with `-c` — the flag
|
|
147
|
+
never touches config-declared tables. It is a `query`-subcommand flag; server
|
|
148
|
+
mode does not accept it.
|
|
149
|
+
|
|
117
150
|
## Freshness and scope
|
|
118
151
|
|
|
119
152
|
A path-table is scanned when the statement runs, so it always reflects the
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
**Added**
|
|
2
|
+
|
|
3
|
+
`dirsql query "<sql>" --on-file '<command>'` attaches a parser to every
|
|
4
|
+
[path-table](https://thekevinscott.github.io/dirsql/reference/path-tables) in
|
|
5
|
+
the query. With the flag set, a path-table's rows and schema come from the
|
|
6
|
+
command's JSON output (the `on-file` hook contract — argv splitting,
|
|
7
|
+
`{path}`/`{root}` placeholders, timeout) instead of the stat columns. The
|
|
8
|
+
parser's output is the whole schema; stat columns are not reachable on a parsed
|
|
9
|
+
path-table. Failures are isolated per file — a file whose parser fails or whose
|
|
10
|
+
output does not parse contributes no rows and a stderr warning, and the scan
|
|
11
|
+
continues. Parsed scans honor the same `node_modules`/`.git`/`ignore` skip
|
|
12
|
+
rules stat scans do. The flag may be given at most once (a repeat errors,
|
|
13
|
+
pointing at config files), applies only to path-tables (config `[[table]]`
|
|
14
|
+
`on-file` hooks are untouched), and is `query`-only (server mode rejects it).
|
|
@@ -151,6 +151,25 @@ uses**, so behavior is identical to `POST /query` by construction:
|
|
|
151
151
|
statements; dirsql separately never modifies the files it indexes — see
|
|
152
152
|
[Read-only by design](../explanation#read-only-by-design).
|
|
153
153
|
|
|
154
|
+
#### `--on-file <command>`
|
|
155
|
+
|
|
156
|
+
Attach a parser to every [path-table](./path-tables.md#parsing-rows-with-on-file)
|
|
157
|
+
in the query, so each matched file yields the rows the command prints (a JSON
|
|
158
|
+
array of row objects) instead of the stat columns:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
162
|
+
--on-file 'extract.py {path}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The command follows the [`on-file` hook contract](./hooks.md#on-file) — argv
|
|
166
|
+
splitting, `{path}`/`{root}` placeholders, per-file failure isolation, and the
|
|
167
|
+
timeout. The parser's output is the whole schema; the stat columns are not
|
|
168
|
+
reachable on a parsed path-table. `--on-file` may be given **at most once** (a
|
|
169
|
+
repeat is an error pointing at config files) and never touches config-declared
|
|
170
|
+
tables. It is a `query`-only flag — server mode rejects it as an unknown
|
|
171
|
+
argument.
|
|
172
|
+
|
|
154
173
|
Errors print the same diagnostic the HTTP `{"error": …}` body carries —
|
|
155
174
|
config failures, SQL errors, rejected reads, hook failures, timeouts — to
|
|
156
175
|
stderr, with exit code `1`.
|
|
@@ -114,6 +114,39 @@ SELECT path FROM './docs/*.md' WHERE content LIKE '%deprecated%';
|
|
|
114
114
|
A file that cannot be read, or is not valid UTF-8, yields `NULL` content rather
|
|
115
115
|
than failing the query.
|
|
116
116
|
|
|
117
|
+
## Parsing rows with `--on-file`
|
|
118
|
+
|
|
119
|
+
By default a path-table's columns are the stat columns above — one row per
|
|
120
|
+
file. When you want *one row per record inside* each file, attach a parser with
|
|
121
|
+
the `dirsql query` flag [`--on-file`](/reference/cli):
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
125
|
+
--on-file 'extract.py {path}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The command runs once per matched file and prints a JSON array of row objects,
|
|
129
|
+
exactly like a declared table's [`on-file` hook](/reference/hooks) — same argv
|
|
130
|
+
splitting, same `{path}`/`{root}` placeholders, same timeout. Its output *is*
|
|
131
|
+
the table:
|
|
132
|
+
|
|
133
|
+
- **The parser supplies the whole schema.** Columns are inferred from the keys
|
|
134
|
+
across the emitted rows. The stat columns (`path`, `size`, …) are **not**
|
|
135
|
+
reachable on a parsed path-table — a parser that wants the path emits it (it
|
|
136
|
+
has `{path}`). The two modes stay cleanly separate.
|
|
137
|
+
- **Failures are isolated per file.** A file whose parser fails (spawn, non-zero
|
|
138
|
+
exit, timeout, or no output) or whose output is not a JSON array of rows
|
|
139
|
+
contributes no rows; a one-line warning naming the file goes to stderr and the
|
|
140
|
+
scan continues. The schema is inferred from the files that did parse.
|
|
141
|
+
- **The skip rules still apply.** A parsed scan honors the same `node_modules`
|
|
142
|
+
/`.git`/`ignore` rules a stat scan does (see below).
|
|
143
|
+
|
|
144
|
+
`--on-file` applies to **every** path-table in the query and may be given **at
|
|
145
|
+
most once**. For different parsers per file set, define named tables in a
|
|
146
|
+
`.dirsql.toml` with their own `on-file` keys and pass it with `-c` — the flag
|
|
147
|
+
never touches config-declared tables. It is a `query`-subcommand flag; server
|
|
148
|
+
mode does not accept it.
|
|
149
|
+
|
|
117
150
|
## Freshness and scope
|
|
118
151
|
|
|
119
152
|
A path-table is scanned when the statement runs, so it always reflects the
|
|
@@ -146,10 +146,42 @@ struct QueryArgs {
|
|
|
146
146
|
/// The SQL to run (a single read-only statement).
|
|
147
147
|
sql: String,
|
|
148
148
|
|
|
149
|
+
/// Attach a parser to every path-table in the query. The command follows
|
|
150
|
+
/// the `on-file` hook contract (`docs/reference/hooks.md`): argv splitting,
|
|
151
|
+
/// `{path}`/`{root}` placeholders, a JSON array of row objects on stdout,
|
|
152
|
+
/// per-file failure isolation, and the hook timeout. With it set, a
|
|
153
|
+
/// path-table's rows and schema come from the parser instead of the stat
|
|
154
|
+
/// columns. One `--on-file` max; for multiple tables use a config file.
|
|
155
|
+
#[arg(long = "on-file")]
|
|
156
|
+
on_file: Vec<String>,
|
|
157
|
+
|
|
149
158
|
#[command(flatten)]
|
|
150
159
|
common: ConfigArgs,
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
/// Reduce the repeatable `--on-file` occurrences to at most one parser command.
|
|
163
|
+
///
|
|
164
|
+
/// `clap` collects repeats into a `Vec` so the error can name config files
|
|
165
|
+
/// (its default "cannot be used multiple times" cannot). Empty → no parser;
|
|
166
|
+
/// exactly one → that command; more than one → an error pointing at config
|
|
167
|
+
/// files, where per-table parsers belong. A whitespace-only command is rejected
|
|
168
|
+
/// up front (the hook contract forbids an empty command).
|
|
169
|
+
fn resolve_on_file(occurrences: &[String]) -> std::result::Result<Option<String>, String> {
|
|
170
|
+
match occurrences {
|
|
171
|
+
[] => Ok(None),
|
|
172
|
+
[command] if command.trim().is_empty() => {
|
|
173
|
+
Err("--on-file needs a non-empty command".to_string())
|
|
174
|
+
}
|
|
175
|
+
[command] => Ok(Some(command.clone())),
|
|
176
|
+
_ => Err(
|
|
177
|
+
"--on-file may be given at most once; it applies to every path-table in the \
|
|
178
|
+
query. For per-table parsers, define tables in a config file with an \
|
|
179
|
+
`on-file` key and pass it with -c."
|
|
180
|
+
.to_string(),
|
|
181
|
+
),
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
153
185
|
#[derive(Debug, Args)]
|
|
154
186
|
struct InitArgs {
|
|
155
187
|
/// Directory the default `--output` path is resolved against (default:
|
|
@@ -185,7 +217,14 @@ async fn main() -> ExitCode {
|
|
|
185
217
|
/// message — the same string the HTTP `{"error": …}` body carries — to
|
|
186
218
|
/// stderr with a non-zero exit.
|
|
187
219
|
async fn run_query(args: QueryArgs) -> ExitCode {
|
|
188
|
-
let
|
|
220
|
+
let parser = match resolve_on_file(&args.on_file) {
|
|
221
|
+
Ok(parser) => parser,
|
|
222
|
+
Err(message) => {
|
|
223
|
+
eprintln!("dirsql query: {message}");
|
|
224
|
+
return ExitCode::from(1);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
let state = load_state(&args.common, parser);
|
|
189
228
|
let pre_query = load_pre_queries(&args.common);
|
|
190
229
|
let post_query = load_post_queries(&args.common);
|
|
191
230
|
// Same default the server binds with; the pipeline enforces it.
|
|
@@ -246,7 +285,9 @@ fn run_init(args: InitArgs) -> ExitCode {
|
|
|
246
285
|
}
|
|
247
286
|
|
|
248
287
|
async fn run_server(cli: Cli) -> ExitCode {
|
|
249
|
-
|
|
288
|
+
// The server has no `--on-file`: clap rejects it as an unknown flag before
|
|
289
|
+
// reaching here. Path-tables served over HTTP keep their stat columns.
|
|
290
|
+
let state = load_state(&cli.common, None);
|
|
250
291
|
let mut server_config = ServerConfig::bind(cli.host.clone(), cli.port);
|
|
251
292
|
for pre_query in load_pre_queries(&cli.common) {
|
|
252
293
|
server_config = server_config.with_pre_query(pre_query);
|
|
@@ -278,12 +319,12 @@ async fn run_server(cli: Cli) -> ExitCode {
|
|
|
278
319
|
ExitCode::SUCCESS
|
|
279
320
|
}
|
|
280
321
|
|
|
281
|
-
fn load_state(cfg: &ConfigArgs) -> AppState {
|
|
322
|
+
fn load_state(cfg: &ConfigArgs, path_table_parser: Option<String>) -> AppState {
|
|
282
323
|
// Neither a `-c` nor the launcher's `--include-default` -> index the
|
|
283
324
|
// invocation directory with no named tables. A `./.dirsql.toml` on disk is
|
|
284
325
|
// NOT consulted (#602); pass it explicitly with `-c` to use it.
|
|
285
326
|
if cfg.config.is_empty() && !cfg.include_default {
|
|
286
|
-
return load_configless_state(cfg);
|
|
327
|
+
return load_configless_state(cfg, path_table_parser);
|
|
287
328
|
}
|
|
288
329
|
|
|
289
330
|
let mut builder = DirSQL::builder();
|
|
@@ -324,6 +365,12 @@ fn load_state(cfg: &ConfigArgs) -> AppState {
|
|
|
324
365
|
.suppress_config_extensions(true);
|
|
325
366
|
}
|
|
326
367
|
builder = cfg.apply_persist(builder);
|
|
368
|
+
// `--on-file` touches path-tables only: config `[[table]]` definitions keep
|
|
369
|
+
// their own `on-file` hooks; a path-table named in the query gets this
|
|
370
|
+
// parser regardless of any `-c`.
|
|
371
|
+
if let Some(command) = path_table_parser {
|
|
372
|
+
builder = builder.path_table_parser(command);
|
|
373
|
+
}
|
|
327
374
|
match builder.build() {
|
|
328
375
|
Ok(db) => AppState::Ready(db),
|
|
329
376
|
Err(err) => AppState::Unavailable(format!("failed to load config: {err}")),
|
|
@@ -420,7 +467,7 @@ fn load_post_queries(cfg: &ConfigArgs) -> Vec<PostQuery> {
|
|
|
420
467
|
/// tables: filesystem queries go through path-tables (`SELECT * FROM './'`),
|
|
421
468
|
/// and a `files` query fails with a hint pointing at that form (#636). A
|
|
422
469
|
/// `./.dirsql.toml` in the cwd is not consulted (#602).
|
|
423
|
-
fn load_configless_state(cfg: &ConfigArgs) -> AppState {
|
|
470
|
+
fn load_configless_state(cfg: &ConfigArgs, path_table_parser: Option<String>) -> AppState {
|
|
424
471
|
// Canonicalize for the same reason `load_state` does: `notify` misbehaves
|
|
425
472
|
// when watching relative paths.
|
|
426
473
|
let root = match PathBuf::from(".").canonicalize() {
|
|
@@ -430,7 +477,10 @@ fn load_configless_state(cfg: &ConfigArgs) -> AppState {
|
|
|
430
477
|
}
|
|
431
478
|
};
|
|
432
479
|
|
|
433
|
-
let builder = cfg.apply_persist(DirSQL::builder().root(root));
|
|
480
|
+
let mut builder = cfg.apply_persist(DirSQL::builder().root(root));
|
|
481
|
+
if let Some(command) = path_table_parser {
|
|
482
|
+
builder = builder.path_table_parser(command);
|
|
483
|
+
}
|
|
434
484
|
match builder.build() {
|
|
435
485
|
Ok(db) => AppState::Ready(db),
|
|
436
486
|
Err(err) => AppState::Unavailable(format!("failed to build the index: {err}")),
|
|
@@ -606,6 +656,81 @@ mod tests {
|
|
|
606
656
|
);
|
|
607
657
|
}
|
|
608
658
|
|
|
659
|
+
/// The `on_file` occurrences parsed from a `query` invocation.
|
|
660
|
+
fn query_on_file(argv: &[&str]) -> Vec<String> {
|
|
661
|
+
match Cli::parse_from(argv).command {
|
|
662
|
+
Some(Command::Query(args)) => args.on_file,
|
|
663
|
+
other => panic!("expected a query subcommand, got {other:?}"),
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
#[test]
|
|
668
|
+
fn on_file_absent_leaves_no_occurrences() {
|
|
669
|
+
assert!(query_on_file(&["dirsql", "query", "SELECT 1"]).is_empty());
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
#[test]
|
|
673
|
+
fn on_file_parses_after_the_query_subcommand() {
|
|
674
|
+
assert_eq!(
|
|
675
|
+
query_on_file(&["dirsql", "query", "SELECT 1", "--on-file", "cat {path}"]),
|
|
676
|
+
vec!["cat {path}".to_string()]
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#[test]
|
|
681
|
+
fn on_file_collects_every_repeat_for_the_arity_check() {
|
|
682
|
+
// clap collects repeats; `resolve_on_file` turns >1 into the pointed
|
|
683
|
+
// error rather than clap's generic "cannot be used multiple times".
|
|
684
|
+
assert_eq!(
|
|
685
|
+
query_on_file(&[
|
|
686
|
+
"dirsql",
|
|
687
|
+
"query",
|
|
688
|
+
"SELECT 1",
|
|
689
|
+
"--on-file",
|
|
690
|
+
"a",
|
|
691
|
+
"--on-file",
|
|
692
|
+
"b"
|
|
693
|
+
]),
|
|
694
|
+
vec!["a".to_string(), "b".to_string()]
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
#[test]
|
|
699
|
+
fn on_file_is_rejected_before_a_query_subcommand() {
|
|
700
|
+
// Like the other subcommand-local flags, `--on-file` ahead of the
|
|
701
|
+
// subcommand is not a server flag: clap rejects it.
|
|
702
|
+
assert!(Cli::try_parse_from(["dirsql", "--on-file", "cat", "query", "SELECT 1"]).is_err());
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#[test]
|
|
706
|
+
fn resolve_on_file_is_none_without_the_flag() {
|
|
707
|
+
assert_eq!(resolve_on_file(&[]), Ok(None));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#[test]
|
|
711
|
+
fn resolve_on_file_returns_the_single_command() {
|
|
712
|
+
assert_eq!(
|
|
713
|
+
resolve_on_file(&["cat {path}".to_string()]),
|
|
714
|
+
Ok(Some("cat {path}".to_string()))
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#[test]
|
|
719
|
+
fn resolve_on_file_rejects_a_blank_command() {
|
|
720
|
+
let err = resolve_on_file(&[" ".to_string()]).unwrap_err();
|
|
721
|
+
assert!(err.contains("non-empty"), "got: {err}");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
#[test]
|
|
725
|
+
fn resolve_on_file_rejects_a_repeat_and_points_at_config_files() {
|
|
726
|
+
let err = resolve_on_file(&["a".to_string(), "b".to_string()]).unwrap_err();
|
|
727
|
+
assert!(err.contains("at most once"), "got: {err}");
|
|
728
|
+
assert!(
|
|
729
|
+
err.contains("config file"),
|
|
730
|
+
"the error must point at config files, got: {err}"
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
609
734
|
#[test]
|
|
610
735
|
fn parse_extension_specs_handles_bare_path_and_entrypoint() {
|
|
611
736
|
let specs = vec![
|
|
@@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet};
|
|
|
3
3
|
use std::path::{Path, PathBuf};
|
|
4
4
|
use thiserror::Error;
|
|
5
5
|
|
|
6
|
+
use crate::parsed_vtab;
|
|
6
7
|
use crate::path_table::{self, PathTable, Resolution};
|
|
7
8
|
use crate::vtab;
|
|
8
9
|
|
|
@@ -107,18 +108,41 @@ fn quote_identifier(s: &str) -> String {
|
|
|
107
108
|
/// persistent cache file — so a path-table can never leak into a persisted
|
|
108
109
|
/// `sqlite_master`. `IF NOT EXISTS` makes a repeat reference a no-op, which is
|
|
109
110
|
/// why no cross-call registry is needed.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
///
|
|
112
|
+
/// With a `parser` present (the CLI's `--on-file`), the table is minted over
|
|
113
|
+
/// the parsed module instead: its rows and schema come from the parser, not
|
|
114
|
+
/// the stat columns, and the `path_prefix` is irrelevant (a parser wanting a
|
|
115
|
+
/// path emits it). Both forms carry the same ignore rules.
|
|
116
|
+
fn path_table_ddl(
|
|
117
|
+
name: &str,
|
|
118
|
+
table: &PathTable,
|
|
119
|
+
ignore: &[String],
|
|
120
|
+
parser: Option<&str>,
|
|
121
|
+
) -> String {
|
|
122
|
+
let (module, mut args) = match parser {
|
|
123
|
+
Some(command) => (
|
|
124
|
+
parsed_vtab::MODULE_NAME,
|
|
125
|
+
vec![
|
|
126
|
+
quote_literal(&table.root.to_string_lossy()),
|
|
127
|
+
quote_literal(&table.glob),
|
|
128
|
+
quote_literal(command),
|
|
129
|
+
],
|
|
130
|
+
),
|
|
131
|
+
None => (
|
|
132
|
+
vtab::MODULE_NAME,
|
|
133
|
+
vec![
|
|
134
|
+
quote_literal(&table.root.to_string_lossy()),
|
|
135
|
+
quote_literal(&table.glob),
|
|
136
|
+
quote_literal(&table.path_prefix),
|
|
137
|
+
],
|
|
138
|
+
),
|
|
139
|
+
};
|
|
116
140
|
args.extend(ignore.iter().map(|p| quote_literal(p)));
|
|
117
141
|
|
|
118
142
|
format!(
|
|
119
143
|
"CREATE VIRTUAL TABLE IF NOT EXISTS temp.{} USING {}({})",
|
|
120
144
|
quote_identifier(name),
|
|
121
|
-
|
|
145
|
+
module,
|
|
122
146
|
args.join(", "),
|
|
123
147
|
)
|
|
124
148
|
}
|
|
@@ -184,6 +208,10 @@ pub struct Db {
|
|
|
184
208
|
hint_legacy_files_table: bool,
|
|
185
209
|
/// Skip rules a path-table scan applies, seeded with the built-in defaults.
|
|
186
210
|
path_table_ignore: Vec<String>,
|
|
211
|
+
/// When set (the CLI's `--on-file`), every path-table is minted over the
|
|
212
|
+
/// parser instead of the stat columns: its rows and schema come from the
|
|
213
|
+
/// command's output. `None` keeps the stat path-table behavior.
|
|
214
|
+
path_table_parser: Option<String>,
|
|
187
215
|
}
|
|
188
216
|
|
|
189
217
|
/// The skip rules a fresh `Db` starts with.
|
|
@@ -210,11 +238,13 @@ impl Db {
|
|
|
210
238
|
let conn = Connection::open("")?;
|
|
211
239
|
ensure_internal_rows_table(&conn)?;
|
|
212
240
|
vtab::load_module(&conn)?;
|
|
241
|
+
parsed_vtab::load_module(&conn)?;
|
|
213
242
|
Ok(Self {
|
|
214
243
|
conn,
|
|
215
244
|
path_table_root: None,
|
|
216
245
|
hint_legacy_files_table: false,
|
|
217
246
|
path_table_ignore: default_path_table_ignore(),
|
|
247
|
+
path_table_parser: None,
|
|
218
248
|
})
|
|
219
249
|
}
|
|
220
250
|
|
|
@@ -228,11 +258,13 @@ impl Db {
|
|
|
228
258
|
conn.pragma_update(None, "synchronous", "NORMAL")?;
|
|
229
259
|
ensure_internal_rows_table(&conn)?;
|
|
230
260
|
vtab::load_module(&conn)?;
|
|
261
|
+
parsed_vtab::load_module(&conn)?;
|
|
231
262
|
Ok(Self {
|
|
232
263
|
conn,
|
|
233
264
|
path_table_root: None,
|
|
234
265
|
hint_legacy_files_table: false,
|
|
235
266
|
path_table_ignore: default_path_table_ignore(),
|
|
267
|
+
path_table_parser: None,
|
|
236
268
|
})
|
|
237
269
|
}
|
|
238
270
|
|
|
@@ -255,6 +287,13 @@ impl Db {
|
|
|
255
287
|
self.path_table_ignore.extend(patterns);
|
|
256
288
|
}
|
|
257
289
|
|
|
290
|
+
/// Attach a parser command to every path-table minted on this connection
|
|
291
|
+
/// (the CLI's `--on-file`). With it set, a path-table's rows and schema
|
|
292
|
+
/// come from the command's output instead of the stat columns.
|
|
293
|
+
pub fn set_path_table_parser(&mut self, command: String) {
|
|
294
|
+
self.path_table_parser = Some(command);
|
|
295
|
+
}
|
|
296
|
+
|
|
258
297
|
/// Borrow the underlying SQLite connection. Internal use only — exposed
|
|
259
298
|
/// to the `persist` module so it can manage the sidecar tables.
|
|
260
299
|
#[doc(hidden)]
|
|
@@ -710,8 +749,12 @@ impl Db {
|
|
|
710
749
|
/// straight on the connection rather than through [`query`](Self::query),
|
|
711
750
|
/// which classifies `CREATE VIRTUAL TABLE` as a write and would reject it.
|
|
712
751
|
fn create_path_table(&self, name: &str, table: &PathTable) -> Result<()> {
|
|
713
|
-
self.conn
|
|
714
|
-
|
|
752
|
+
self.conn.execute_batch(&path_table_ddl(
|
|
753
|
+
name,
|
|
754
|
+
table,
|
|
755
|
+
&self.path_table_ignore,
|
|
756
|
+
self.path_table_parser.as_deref(),
|
|
757
|
+
))?;
|
|
715
758
|
Ok(())
|
|
716
759
|
}
|
|
717
760
|
}
|
|
@@ -2095,7 +2138,7 @@ mod tests {
|
|
|
2095
2138
|
|
|
2096
2139
|
#[test]
|
|
2097
2140
|
fn path_table_ddl_creates_in_temp_if_not_exists() {
|
|
2098
|
-
let ddl = path_table_ddl("./docs/*.md", &docs_path_table(), &[]);
|
|
2141
|
+
let ddl = path_table_ddl("./docs/*.md", &docs_path_table(), &[], None);
|
|
2099
2142
|
assert_eq!(
|
|
2100
2143
|
ddl,
|
|
2101
2144
|
"CREATE VIRTUAL TABLE IF NOT EXISTS temp.\"./docs/*.md\" \
|
|
@@ -2111,10 +2154,10 @@ mod tests {
|
|
|
2111
2154
|
path_prefix: "/var/log".to_string(),
|
|
2112
2155
|
};
|
|
2113
2156
|
assert!(
|
|
2114
|
-
path_table_ddl("/var/log/*.log", &table, &[])
|
|
2157
|
+
path_table_ddl("/var/log/*.log", &table, &[], None)
|
|
2115
2158
|
.ends_with("'/var/log', '*.log', '/var/log')"),
|
|
2116
2159
|
"got: {}",
|
|
2117
|
-
path_table_ddl("/var/log/*.log", &table, &[])
|
|
2160
|
+
path_table_ddl("/var/log/*.log", &table, &[], None)
|
|
2118
2161
|
);
|
|
2119
2162
|
}
|
|
2120
2163
|
|
|
@@ -2124,6 +2167,7 @@ mod tests {
|
|
|
2124
2167
|
"./",
|
|
2125
2168
|
&docs_path_table(),
|
|
2126
2169
|
&["node_modules/**".to_string(), "*.tmp".to_string()],
|
|
2170
|
+
None,
|
|
2127
2171
|
);
|
|
2128
2172
|
assert!(
|
|
2129
2173
|
ddl.ends_with("'', 'node_modules/**', '*.tmp')"),
|
|
@@ -2131,6 +2175,53 @@ mod tests {
|
|
|
2131
2175
|
);
|
|
2132
2176
|
}
|
|
2133
2177
|
|
|
2178
|
+
#[test]
|
|
2179
|
+
fn path_table_ddl_uses_the_parsed_module_when_a_parser_is_set() {
|
|
2180
|
+
let ddl = path_table_ddl("./docs/*.md", &docs_path_table(), &[], Some("cat {path}"));
|
|
2181
|
+
assert_eq!(
|
|
2182
|
+
ddl,
|
|
2183
|
+
"CREATE VIRTUAL TABLE IF NOT EXISTS temp.\"./docs/*.md\" \
|
|
2184
|
+
USING dirsql_parsed('/root', 'docs/*.md', 'cat {path}')"
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
#[test]
|
|
2189
|
+
fn path_table_ddl_parser_form_omits_the_path_prefix_and_carries_ignore() {
|
|
2190
|
+
let table = PathTable {
|
|
2191
|
+
root: PathBuf::from("/var/log"),
|
|
2192
|
+
glob: "*.log".to_string(),
|
|
2193
|
+
path_prefix: "/var/log".to_string(),
|
|
2194
|
+
};
|
|
2195
|
+
let ddl = path_table_ddl(
|
|
2196
|
+
"/var/log/*.log",
|
|
2197
|
+
&table,
|
|
2198
|
+
&["node_modules/**".to_string()],
|
|
2199
|
+
Some("parse.py {path}"),
|
|
2200
|
+
);
|
|
2201
|
+
assert!(
|
|
2202
|
+
ddl.ends_with(
|
|
2203
|
+
"USING dirsql_parsed('/var/log', '*.log', 'parse.py {path}', 'node_modules/**')"
|
|
2204
|
+
),
|
|
2205
|
+
"the parser form drops the path prefix and keeps ignore rules, got: {ddl}"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
#[test]
|
|
2210
|
+
fn path_table_ddl_parser_form_quotes_a_command_with_a_quote() {
|
|
2211
|
+
let ddl = path_table_ddl("./", &docs_path_table(), &[], Some("sh -c 'echo hi'"));
|
|
2212
|
+
assert!(
|
|
2213
|
+
ddl.contains("'sh -c ''echo hi'''"),
|
|
2214
|
+
"an embedded quote must be doubled, got: {ddl}"
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
#[test]
|
|
2219
|
+
fn set_path_table_parser_arms_the_parsed_module() {
|
|
2220
|
+
let mut db = Db::new().unwrap();
|
|
2221
|
+
db.set_path_table_parser("cat {path}".to_string());
|
|
2222
|
+
assert_eq!(db.path_table_parser.as_deref(), Some("cat {path}"));
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2134
2225
|
#[test]
|
|
2135
2226
|
fn query_leaves_a_path_name_alone_without_a_root() {
|
|
2136
2227
|
let db = Db::new().unwrap();
|