vgi-python 0.27.0__tar.gz → 0.28.1__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.
- {vgi_python-0.27.0 → vgi_python-0.28.1}/PKG-INFO +2 -2
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/column-statistics.md +24 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/function-patterns.md +25 -0
- vgi_python-0.28.1/examples/batch_index_worker.py +142 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/pyproject.toml +1 -1
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_column_statistics.py +86 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_broken_pipe.py +20 -9
- vgi_python-0.28.1/tests/client/test_stop_force.py +101 -0
- vgi_python-0.28.1/tests/client/test_table_column_statistics.py +73 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_worker_debug.py +15 -13
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/test_client.py +10 -6
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_examples_workers.py +20 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/uv.lock +525 -336
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/__init__.py +7 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/catalog_interface.py +65 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/catalog_mixin.py +53 -3
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/client.py +82 -26
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/landing.html +61 -13
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/vgi-client.js +9 -9
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.gitattributes +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/dependabot.yml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/styles/config/vocabularies/VGI/accept.txt +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/ci.yml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/docs.yml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/integration.yml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.github/workflows/release.yml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.gitignore +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.python-version +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/.vale.ini +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/CLAUDE.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/DOCS_ACCEPTANCE_CRITERIA.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/DOCS_REVIEW_RUBRIC.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/DOCS_USABILITY_TEST.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/LICENSE +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/README.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/RELEASE-RUNBOOK.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/SECURITY.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/ci/README.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/ci/preprocess-require.awk +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/ci/run-integration.sh +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/dist-vgi/.gitignore +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/aggregate-functions.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/arguments.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/auth.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/catalogs.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/client.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/exceptions.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/filters.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/functions.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/http.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/index.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/metadata.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/observability.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/storage.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/transactor.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/api/worker.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/argument-serialization.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/apple-touch-icon.png +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/favicon-16x16.png +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/favicon-32x32.png +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/favicon.ico +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/aggregate.svg +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/buffering.svg +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/scalar.svg +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/table-in-out.svg +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/kinds/table.svg +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/logo.png +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/assets/social-card.png +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/authentication.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/catalog-interface.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/cli.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/concepts/index.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/contributing-docs.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/filter-pushdown.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/generator-api.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/global-functions.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/catalogs.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/http-auth.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/index.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/pushdown-and-statistics.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/how-to/state-storage.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/index.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/lifecycle.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/metadata.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/overrides/main.html +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/robots.txt +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/shared-storage.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/stylesheets/extra.css +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/tutorial/index.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/tutorial/scalar.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/tutorial/table.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/docs/vgi-logo.png +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/calc_scalar_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/calc_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/filter_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/greeting_scalar_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/row_count_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/series_streaming_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/examples/sum_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/mkdocs.yml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/LICENSE +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/README.md +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/docker/Dockerfile +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/docker/docker-entrypoint.sh +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/packages/vgi-fixtures/pyproject.toml +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/scripts/measure_startup.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/scripts/run_all_tests.sh +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/test-data/generate.sh +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/_http_fixtures.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_attach_option.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_catalog_interface.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_client_catalog.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_declarative.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_example_worker_catalog.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_integration.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_required_filters.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_scan_branches.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_serialization.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_setting.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_time_travel.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/catalog/test_writable_table.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_cli.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/client/test_cli_catalog_functions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/_stub.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/conftest.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_accumulate.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_aggregate.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_attach.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_bearer_auth.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_const_param_enforcement.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_container.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_copy_from.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_copy_to.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_directory_parity.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_function_inventory.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_github.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_http_client.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_http_external_location.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_http_upload_url.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_macro.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_overload.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_protocol_inventory.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_protocol_version.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_resumable_scan.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_scalar.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_scalar_attach_opaque_data.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_secret.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_settings.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_table.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_table_in_out.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_view.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conformance/test_writable.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/conftest.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_bernoulli_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_binary_packet_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_client.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_conditional_message_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_hash_seed_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_multiply_by_setting_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_multiply_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_random_bytes_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/scalar/test_return_secret_value_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_constant_columns_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_double_sequence_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_exception_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_filter_echo_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_logging_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_nested_sequence_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_partitioned_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_projected_data_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_sequence_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_settings_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_struct_settings_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/generator/test_ten_thousand_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table/test_wide_columns.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_buffer_input_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_echo_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_exception_functions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_filter_by_setting_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_repeat_inputs_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_substream_partial_sum_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/generator/test_sum_all_columns_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/table_in_out/test_blended_metadata.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_access_log_audit.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_aggregate_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_argument_spec.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_auth.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_bind_exceptions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_bind_request_at_clause.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_bound_storage_conformance.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_cache_control.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_call_state_attach.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_catalog_auth_binding.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_copy_from_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_copy_to_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_docstrings.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_documentation_examples.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_example_function_arg_types.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_exception_handling.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_exceptions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_filter_pushdown.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_filter_pushdown_extension.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_azure_sql.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_cf_do.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_cf_do_integration.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_function_storage_conformance.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_constants.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_protocol_version.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_request_builders.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_cpp_secret.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_go_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_protocol_version.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_schemas_cross_lang.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_ts_client.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_generated_ts_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_http_demo_storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_http_s3_offload_input.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_http_s3_offload_output.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_metadata.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_mypy_consolidated.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_nest_tensor.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_otel.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_projection_enforcement.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_projection_repro.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_protocol_classes.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_resolved_secrets.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_schema_scoped_functions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_schema_utils.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_serve.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_setting_secret_annotations.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_signing_key.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_table_buffering_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_table_function_dynamic_to_string.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_tcp_transport.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_type_bounds.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_union_argument.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_worker_cli.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/test_worker_page.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/transactor/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/tests/transactor/test_transactor.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_duckdb.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_storage_profile.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/accumulate/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/accumulate/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/_common.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/basic.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/dynamic.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/generic.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/listagg.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/percentile.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/same_name.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/secret_typed.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/streaming.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/varargs.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/aggregate/window.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/attach_options.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/bad_enum.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/bad_protocol.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/cancellable.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/catalog.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/companion.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/copy_from.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/copy_to.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/global_functions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/http_server.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/narrow_bind/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/narrow_bind/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/nest_tensor.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/orchard_catalog.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/projection_repro/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/projection_repro/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/_common.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/arithmetic.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/bench_ladder.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/binary.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/formatting.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/geo.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/null_handling.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/random_demo.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/same_name.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/settings_secrets.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/scalar/type_info.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/schema_reconcile/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/schema_reconcile/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/simple_writable.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/_common.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/batch_index.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/batch_index_broken.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/cache.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/catalog_scans.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/filters.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/late_materialization.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/make_series.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/misc.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/order_modes.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/pairs.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/partition_columns.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/partition_columns_broken.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/profiling_example.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/required_filters.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/same_name_cached.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/sequence.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/settings.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/transaction_storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/tt_pushdown.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/typed_probe.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/versioned.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table/wide.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table_in_out.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/table_in_out_same_name.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/twin_catalogs.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/versioned.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/versioned_tables.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/generic.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/table.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/_test_fixtures/writable/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/aggregate_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/argument_spec.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/arguments.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/attach_header.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/auth.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/cache_control.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/_descriptor_spec.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/attach_option.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/descriptors.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/duckdb_statistics.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/secret_type.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/setting.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/catalog/storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_catalog.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_schema.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_table.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_transaction.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_utils.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/client/cli_view.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/_common.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_constants.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_protocol_version.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_request_builders.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_secret_protocol_version.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_secret_request_builders.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/cpp_secret_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/go_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/protocol_version.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/ts_client.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/codegen/ts_schemas.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/copy_from_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/copy_to_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/exceptions.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function_storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function_storage_azure_sql.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/function_storage_cf_do.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/demo_storage.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/landing_page.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/http/worker_page.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/invocation.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/logging_config.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/meta_worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/metadata.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/otel.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/profiling.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/protocol.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/protocol_version.txt +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/py.typed +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/scalar_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/schema_utils.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/secret_protocol.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/secret_service.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/serve.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_buffering_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_filter_pushdown.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/table_in_out_function.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/__init__.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/_duckdb_compat.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/client.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/protocol.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/transactor/server.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/vgi/worker.py +0 -0
- {vgi_python-0.27.0 → vgi_python-0.28.1}/wrangler.jsonc +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vgi-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.1
|
|
4
4
|
Summary: Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow
|
|
5
5
|
Project-URL: Homepage, https://query.farm
|
|
6
6
|
Project-URL: Repository, https://github.com/Query-farm/vgi-python
|
|
@@ -180,6 +180,30 @@ Statistics are transmitted via the `catalog_table_column_statistics_get` RPC met
|
|
|
180
180
|
|
|
181
181
|
Cache TTL is carried as IPC batch `custom_metadata` with key `cache_max_age_seconds`.
|
|
182
182
|
|
|
183
|
+
## Reading Statistics From a Client
|
|
184
|
+
|
|
185
|
+
DuckDB is not the only caller. A pure-Python [`Client`][vgi.client.Client] reads the same statistics with
|
|
186
|
+
`table_column_statistics`, which issues the RPC and decodes the sparse-union batch for you:
|
|
187
|
+
|
|
188
|
+
```python test="skip"
|
|
189
|
+
stats = client.table_column_statistics(
|
|
190
|
+
attach_opaque_data=attach.attach_opaque_data,
|
|
191
|
+
schema_name="main",
|
|
192
|
+
name="events",
|
|
193
|
+
)
|
|
194
|
+
for column in stats:
|
|
195
|
+
print(column.column_name, column.min, column.max, column.distinct_count)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Workers may also inline statistics on `TableInfo.column_statistics` rather than serving them
|
|
199
|
+
lazily; `TableInfo.supports_column_statistics` advertises which. Prefer the inlined copy when
|
|
200
|
+
it is present and fall back to the call above.
|
|
201
|
+
|
|
202
|
+
To decode bytes you already hold, use `deserialize_column_statistics` — the inverse of
|
|
203
|
+
`serialize_column_statistics`. The `cache_max_age_seconds` TTL travels in the batch's custom
|
|
204
|
+
metadata rather than in the statistics, so read it with `deserialize_record_batch` if you
|
|
205
|
+
need it.
|
|
206
|
+
|
|
183
207
|
## Capability Flags
|
|
184
208
|
|
|
185
209
|
Statistics are opt-in at two levels:
|
|
@@ -213,6 +213,31 @@ ATTACH 'buffers' (TYPE vgi, LOCATION 'uv run row_count_worker.py');
|
|
|
213
213
|
SELECT * FROM buffers.row_count((SELECT * FROM big_table));
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
+
### Getting the input in order
|
|
217
|
+
|
|
218
|
+
The sink runs in parallel across DuckDB threads, so `process` sees batches in no particular order.
|
|
219
|
+
When output depends on input *order* — a running total, a row pattern match — set
|
|
220
|
+
`Meta.requires_input_batch_index = True` and each `process` call receives `params.batch_index`, a
|
|
221
|
+
monotonic ordinal you can sort the buffered batches by in `finalize`.
|
|
222
|
+
|
|
223
|
+
Not every source can supply one (a base table scan can; `range()` and `VALUES` cannot). When it
|
|
224
|
+
cannot, the extension serializes the sink and numbers the batches itself, so a worker always gets a
|
|
225
|
+
valid index and callers never have to wrap their input. This example reports the indices it saw, one
|
|
226
|
+
row per buffered batch:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
--8<-- "examples/batch_index_worker.py"
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```sql
|
|
233
|
+
ATTACH 'bi' (TYPE vgi, LOCATION 'uv run batch_index_worker.py');
|
|
234
|
+
SELECT * FROM bi.batch_indexes((SELECT * FROM range(5000))) ORDER BY batch_index;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The alternative, `Meta.sink_order_dependent = True`, delivers ordered input by forcing a
|
|
238
|
+
single-threaded sink. It works with any source but gives up parallel ingest, which is usually the
|
|
239
|
+
bulk of a buffering query's wall time. The two flags are mutually exclusive.
|
|
240
|
+
|
|
216
241
|
??? info "Buffering vs. table-in-out"
|
|
217
242
|
Both consume a relation, but a **table-in-out** function emits *per input batch* and never
|
|
218
243
|
holds the whole input — use it for streaming transforms (filter, enrich, reshape). Reach for
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.13"
|
|
3
|
+
# dependencies = ["vgi-python"]
|
|
4
|
+
# ///
|
|
5
|
+
"""A buffering function that asks for input order, and reports what it got.
|
|
6
|
+
|
|
7
|
+
A buffering sink normally runs in parallel across DuckDB threads, so batches
|
|
8
|
+
arrive in no particular order. Setting ``Meta.requires_input_batch_index`` asks
|
|
9
|
+
for DuckDB's per-chunk index alongside each batch, which is what lets a worker
|
|
10
|
+
put the input back in order — the prerequisite for anything order-sensitive,
|
|
11
|
+
like row pattern matching or a running total.
|
|
12
|
+
|
|
13
|
+
Not every source can supply one: a base table scan can, while ``range()`` and
|
|
14
|
+
``VALUES`` cannot. When it cannot, the extension serializes the sink and numbers
|
|
15
|
+
the batches itself, so a worker sees a valid monotonic index either way and never
|
|
16
|
+
has to care which route produced it.
|
|
17
|
+
|
|
18
|
+
This function emits one row per buffered batch — its index and its row count — so
|
|
19
|
+
the guarantee is directly observable:
|
|
20
|
+
|
|
21
|
+
ATTACH 'bi' (TYPE vgi, LOCATION 'uv run batch_index_worker.py');
|
|
22
|
+
SELECT * FROM bi.batch_indexes((SELECT * FROM range(5000))) ORDER BY batch_index;
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Annotated
|
|
27
|
+
|
|
28
|
+
import pyarrow as pa
|
|
29
|
+
from vgi_rpc import ArrowSerializableDataclass
|
|
30
|
+
|
|
31
|
+
from vgi import Arg, Worker
|
|
32
|
+
from vgi.arguments import TableInput
|
|
33
|
+
from vgi.catalog import Catalog, Schema
|
|
34
|
+
from vgi.invocation import BindResponse
|
|
35
|
+
from vgi.table_buffering_function import OutputCollector, TableBufferingFunction, TableBufferingParams
|
|
36
|
+
from vgi.table_function import BindParams
|
|
37
|
+
|
|
38
|
+
_RESULT = pa.schema([("batch_index", pa.int64()), ("rows", pa.int64())])
|
|
39
|
+
|
|
40
|
+
# One log entry per buffered batch: the index DuckDB gave us, and the batch size.
|
|
41
|
+
_NS = b"batches"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True, frozen=True, kw_only=True)
|
|
45
|
+
class BatchIndexArgs:
|
|
46
|
+
"""Arguments: the input table whose batches should be reported."""
|
|
47
|
+
|
|
48
|
+
data: Annotated[TableInput, Arg(0, doc="Input table")]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(kw_only=True)
|
|
52
|
+
class DrainState(ArrowSerializableDataclass):
|
|
53
|
+
"""Per-finalize-stream cursor: emit the report once, then finish."""
|
|
54
|
+
|
|
55
|
+
done: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BatchIndexes(TableBufferingFunction[BatchIndexArgs, DrainState]):
|
|
59
|
+
"""Report the batch index and row count of every buffered input batch."""
|
|
60
|
+
|
|
61
|
+
class Meta:
|
|
62
|
+
"""Function metadata."""
|
|
63
|
+
|
|
64
|
+
name = "batch_indexes"
|
|
65
|
+
# Ask for DuckDB's per-chunk index. Mutually exclusive with
|
|
66
|
+
# sink_order_dependent, which orders the input by serializing the sink
|
|
67
|
+
# instead of by numbering it.
|
|
68
|
+
requires_input_batch_index = True
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def on_bind(cls, params: BindParams[BatchIndexArgs]) -> BindResponse:
|
|
72
|
+
"""Output shape is fixed: one row per input batch."""
|
|
73
|
+
return BindResponse(output_schema=_RESULT)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def process(cls, batch: pa.RecordBatch, params: TableBufferingParams[BatchIndexArgs]) -> bytes:
|
|
77
|
+
"""Sink: record this batch's index and size.
|
|
78
|
+
|
|
79
|
+
``params.batch_index`` is populated because ``Meta`` asked for it; -1
|
|
80
|
+
stands in for the absent case so an older host that does not supply one
|
|
81
|
+
degrades to a visible marker instead of a crash.
|
|
82
|
+
"""
|
|
83
|
+
index = params.batch_index if params.batch_index is not None else -1
|
|
84
|
+
payload = index.to_bytes(8, "little", signed=True) + batch.num_rows.to_bytes(8, "little")
|
|
85
|
+
params.storage.state_append(_NS, b"", payload)
|
|
86
|
+
return params.execution_id
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def combine(cls, state_ids: list[bytes], params: TableBufferingParams[BatchIndexArgs]) -> list[bytes]:
|
|
90
|
+
"""Nothing to reduce: the log already holds one entry per batch."""
|
|
91
|
+
return [params.execution_id]
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def initial_finalize_state(
|
|
95
|
+
cls, finalize_state_id: bytes, params: TableBufferingParams[BatchIndexArgs]
|
|
96
|
+
) -> DrainState:
|
|
97
|
+
"""One cursor per finalize stream."""
|
|
98
|
+
return DrainState()
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def finalize(
|
|
102
|
+
cls,
|
|
103
|
+
params: TableBufferingParams[BatchIndexArgs],
|
|
104
|
+
finalize_state_id: bytes,
|
|
105
|
+
state: DrainState,
|
|
106
|
+
out: OutputCollector,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Source: emit the report, ordered by batch index."""
|
|
109
|
+
if state.done:
|
|
110
|
+
out.finish()
|
|
111
|
+
return
|
|
112
|
+
entries = [
|
|
113
|
+
(
|
|
114
|
+
int.from_bytes(value[:8], "little", signed=True),
|
|
115
|
+
int.from_bytes(value[8:16], "little"),
|
|
116
|
+
)
|
|
117
|
+
for _id, value in params.storage.state_log_scan(_NS, b"")
|
|
118
|
+
]
|
|
119
|
+
entries.sort()
|
|
120
|
+
out.emit(
|
|
121
|
+
pa.RecordBatch.from_pydict(
|
|
122
|
+
{
|
|
123
|
+
"batch_index": [index for index, _rows in entries],
|
|
124
|
+
"rows": [rows for _index, rows in entries],
|
|
125
|
+
},
|
|
126
|
+
schema=params.output_schema,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
state.done = True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BatchIndexWorker(Worker):
|
|
133
|
+
"""A worker exposing the ``bi`` catalog."""
|
|
134
|
+
|
|
135
|
+
catalog = Catalog(
|
|
136
|
+
name="bi",
|
|
137
|
+
schemas=[Schema(name="main", functions=[BatchIndexes])],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
BatchIndexWorker().run()
|
|
@@ -12,6 +12,7 @@ from vgi_rpc.utils import deserialize_record_batch
|
|
|
12
12
|
from vgi.catalog.catalog_interface import (
|
|
13
13
|
ColumnStatistics,
|
|
14
14
|
TableColumnStatisticsResult,
|
|
15
|
+
deserialize_column_statistics,
|
|
15
16
|
serialize_column_statistics,
|
|
16
17
|
)
|
|
17
18
|
from vgi.catalog.descriptors import ColumnStatisticsInput, Table
|
|
@@ -199,6 +200,91 @@ class TestSerializeColumnStatistics:
|
|
|
199
200
|
assert min_type.num_fields == 1
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
# ============================================================================
|
|
204
|
+
# deserialize_column_statistics
|
|
205
|
+
# ============================================================================
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _roundtrip(stats: list[ColumnStatistics]) -> list[ColumnStatistics]:
|
|
209
|
+
return deserialize_column_statistics(serialize_column_statistics(stats))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestDeserializeColumnStatistics:
|
|
213
|
+
"""Tests that deserialization is a faithful inverse of serialization."""
|
|
214
|
+
|
|
215
|
+
def test_empty_batch(self) -> None:
|
|
216
|
+
assert _roundtrip([]) == []
|
|
217
|
+
|
|
218
|
+
def test_int_column_roundtrips(self) -> None:
|
|
219
|
+
stats = [
|
|
220
|
+
ColumnStatistics(
|
|
221
|
+
column_name="id",
|
|
222
|
+
min=pa.scalar(1, pa.int64()),
|
|
223
|
+
max=pa.scalar(100, pa.int64()),
|
|
224
|
+
has_null=False,
|
|
225
|
+
has_not_null=True,
|
|
226
|
+
distinct_count=100,
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
(got,) = _roundtrip(stats)
|
|
230
|
+
assert got.column_name == "id"
|
|
231
|
+
assert got.min.as_py() == 1 # type: ignore[union-attr]
|
|
232
|
+
assert got.max.as_py() == 100 # type: ignore[union-attr]
|
|
233
|
+
assert got.has_null is False
|
|
234
|
+
assert got.has_not_null is True
|
|
235
|
+
assert got.distinct_count == 100
|
|
236
|
+
|
|
237
|
+
def test_string_column_roundtrips_with_string_fields(self) -> None:
|
|
238
|
+
stats = [
|
|
239
|
+
ColumnStatistics(
|
|
240
|
+
column_name="name",
|
|
241
|
+
min=pa.scalar("Alice"),
|
|
242
|
+
max=pa.scalar("Zoe"),
|
|
243
|
+
contains_unicode=True,
|
|
244
|
+
max_string_length=5,
|
|
245
|
+
)
|
|
246
|
+
]
|
|
247
|
+
(got,) = _roundtrip(stats)
|
|
248
|
+
assert got.min.as_py() == "Alice" # type: ignore[union-attr]
|
|
249
|
+
assert got.max.as_py() == "Zoe" # type: ignore[union-attr]
|
|
250
|
+
assert got.contains_unicode is True
|
|
251
|
+
assert got.max_string_length == 5
|
|
252
|
+
|
|
253
|
+
def test_unknown_min_max_stays_none(self) -> None:
|
|
254
|
+
"""A column with no min/max must come back as None, not a null scalar."""
|
|
255
|
+
(got,) = _roundtrip([ColumnStatistics(column_name="opaque")])
|
|
256
|
+
assert got.min is None
|
|
257
|
+
assert got.max is None
|
|
258
|
+
assert got.distinct_count is None
|
|
259
|
+
assert got.contains_unicode is None
|
|
260
|
+
assert got.max_string_length is None
|
|
261
|
+
|
|
262
|
+
def test_mixed_types_keep_their_own_union_arm(self) -> None:
|
|
263
|
+
"""Several column types share one sparse union; each must decode to its own type."""
|
|
264
|
+
stats = [
|
|
265
|
+
ColumnStatistics(column_name="i", min=pa.scalar(1, pa.int64()), max=pa.scalar(9, pa.int64())),
|
|
266
|
+
ColumnStatistics(column_name="s", min=pa.scalar("a"), max=pa.scalar("z")),
|
|
267
|
+
ColumnStatistics(column_name="f", min=pa.scalar(0.5, pa.float64()), max=pa.scalar(9.5, pa.float64())),
|
|
268
|
+
]
|
|
269
|
+
got = _roundtrip(stats)
|
|
270
|
+
assert [s.column_name for s in got] == ["i", "s", "f"]
|
|
271
|
+
assert got[0].min.as_py() == 1 # type: ignore[union-attr]
|
|
272
|
+
assert got[1].min.as_py() == "a" # type: ignore[union-attr]
|
|
273
|
+
assert got[2].max.as_py() == 9.5 # type: ignore[union-attr]
|
|
274
|
+
|
|
275
|
+
def test_order_is_preserved(self) -> None:
|
|
276
|
+
stats = [ColumnStatistics(column_name=f"c{i}", min=pa.scalar(i, pa.int64())) for i in range(6)]
|
|
277
|
+
assert [s.column_name for s in _roundtrip(stats)] == [f"c{i}" for i in range(6)]
|
|
278
|
+
|
|
279
|
+
def test_cache_ttl_rides_in_batch_metadata_not_the_stats(self) -> None:
|
|
280
|
+
"""The TTL is out-of-band; deserialization returns only the statistics."""
|
|
281
|
+
data = serialize_column_statistics([ColumnStatistics(column_name="a")], cache_max_age_seconds=60)
|
|
282
|
+
_, metadata = deserialize_record_batch(data)
|
|
283
|
+
assert metadata is not None
|
|
284
|
+
assert metadata[b"cache_max_age_seconds"] == b"60"
|
|
285
|
+
assert len(deserialize_column_statistics(data)) == 1
|
|
286
|
+
|
|
287
|
+
|
|
202
288
|
# ============================================================================
|
|
203
289
|
# ColumnStatisticsInput
|
|
204
290
|
# ============================================================================
|
|
@@ -9,6 +9,8 @@ input or producing output).
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import sys
|
|
13
|
+
|
|
12
14
|
import pyarrow as pa
|
|
13
15
|
import pytest
|
|
14
16
|
|
|
@@ -17,6 +19,16 @@ from vgi.client.catalog_mixin import CatalogClientError
|
|
|
17
19
|
from vgi.client.client import Client, ClientError
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
def _exits_with(code: int) -> list[str]:
|
|
23
|
+
"""Argv for a worker that exits immediately with ``code``, reading no input.
|
|
24
|
+
|
|
25
|
+
A real process rather than the shell's ``exit`` builtin: workers are spawned
|
|
26
|
+
without a shell, so a builtin would not resolve. Returned as argv because no
|
|
27
|
+
one string quoting convention survives both POSIX and Windows.
|
|
28
|
+
"""
|
|
29
|
+
return [sys.executable, "-c", f"raise SystemExit({code})"]
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
def _make_test_batch() -> pa.RecordBatch:
|
|
21
33
|
"""Create a simple test batch for invoking functions."""
|
|
22
34
|
return pa.RecordBatch.from_pydict({"x": [1, 2, 3]})
|
|
@@ -27,9 +39,9 @@ class TestClientWorkerExitHandling:
|
|
|
27
39
|
|
|
28
40
|
def test_client_raises_error_when_worker_exits_immediately(self) -> None:
|
|
29
41
|
"""Client raises ClientError when worker exits before accepting input."""
|
|
30
|
-
#
|
|
31
|
-
# pool=None
|
|
32
|
-
client = Client(
|
|
42
|
+
# A worker that terminates without ever reading stdin.
|
|
43
|
+
# pool=None: the pool is for real, reusable workers.
|
|
44
|
+
client = Client(_exits_with(1), pool=None)
|
|
33
45
|
|
|
34
46
|
# Worker exits before sending any output - detected either via BrokenPipeError
|
|
35
47
|
# or via early exit check when reading fails
|
|
@@ -48,9 +60,8 @@ class TestClientWorkerExitHandling:
|
|
|
48
60
|
|
|
49
61
|
def test_client_raises_error_with_useful_message(self) -> None:
|
|
50
62
|
"""Client error message includes useful debugging information."""
|
|
51
|
-
# Use a specific exit code
|
|
52
|
-
|
|
53
|
-
client = Client("exit 42", pool=None)
|
|
63
|
+
# Use a specific exit code so the message can be checked for it.
|
|
64
|
+
client = Client(_exits_with(42), pool=None)
|
|
54
65
|
|
|
55
66
|
try:
|
|
56
67
|
client.start()
|
|
@@ -84,8 +95,8 @@ class TestCatalogMixinWorkerExitHandling:
|
|
|
84
95
|
|
|
85
96
|
def test_catalog_invoke_raises_error_when_worker_exits(self) -> None:
|
|
86
97
|
"""Catalog invoke raises CatalogClientError when worker exits."""
|
|
87
|
-
#
|
|
88
|
-
client = Client(
|
|
98
|
+
# A worker that terminates without ever reading stdin.
|
|
99
|
+
client = Client(_exits_with(1))
|
|
89
100
|
|
|
90
101
|
# Worker exits - detected either via BrokenPipeError (shows "terminated
|
|
91
102
|
# unexpectedly") or via read failure (shows "Failed to read catalog result")
|
|
@@ -94,7 +105,7 @@ class TestCatalogMixinWorkerExitHandling:
|
|
|
94
105
|
|
|
95
106
|
def test_catalog_invoke_error_includes_useful_info(self) -> None:
|
|
96
107
|
"""Catalog error message includes useful debugging information."""
|
|
97
|
-
client = Client(
|
|
108
|
+
client = Client(_exits_with(42))
|
|
98
109
|
|
|
99
110
|
try:
|
|
100
111
|
list(client.catalogs())
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
|
|
3
|
+
# ruff: noqa: S101, D102, D103
|
|
4
|
+
"""Tests for ``Client.stop(force=True)``.
|
|
5
|
+
|
|
6
|
+
A graceful ``stop()`` shuts the RPC connection down and waits for the worker, so it cannot
|
|
7
|
+
be used to abandon a call that has overrun its budget — the wait is exactly what the caller
|
|
8
|
+
is trying to escape. ``force=True`` SIGKILLs a direct subprocess worker first, which unblocks
|
|
9
|
+
any thread waiting on its output immediately.
|
|
10
|
+
|
|
11
|
+
Only *direct* subprocess workers own their process; pooled workers are returned to the pool
|
|
12
|
+
and HTTP/TCP connections are already prompt to close, so those paths must accept ``force``
|
|
13
|
+
without changing behaviour.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import pyarrow as pa
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
from vgi.arguments import Arguments
|
|
26
|
+
from vgi.client.client import Client, _default_pool
|
|
27
|
+
|
|
28
|
+
# Long enough that a graceful teardown would have to wait on it if the worker were stuck.
|
|
29
|
+
_SLEEP_MS = 5_000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _started_client(**kwargs: Any) -> Client:
|
|
33
|
+
client = Client("vgi-fixture-worker", **kwargs)
|
|
34
|
+
client.start()
|
|
35
|
+
return client
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_force_kills_a_direct_subprocess(tmp_path: Any) -> None:
|
|
39
|
+
"""The worker process is signalled, not asked to exit."""
|
|
40
|
+
client = _started_client(pool=None)
|
|
41
|
+
assert client._primary is not None # noqa: SLF001 - a started client always has one
|
|
42
|
+
proc = client._primary.proc # noqa: SLF001 - asserting the process really died
|
|
43
|
+
assert proc is not None
|
|
44
|
+
assert proc.poll() is None, "worker should be running"
|
|
45
|
+
|
|
46
|
+
returncode = client.stop(force=True)
|
|
47
|
+
assert returncode is not None
|
|
48
|
+
if sys.platform == "win32":
|
|
49
|
+
# Windows has no signals: TerminateProcess sets an ordinary non-zero code.
|
|
50
|
+
assert returncode != 0, "a killed process should not report a clean exit"
|
|
51
|
+
else:
|
|
52
|
+
assert returncode < 0, "a killed process reports a negative (signal) exit code"
|
|
53
|
+
assert proc.poll() is not None, "worker process should be reaped"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_force_returns_promptly_mid_stream(tmp_path: Any) -> None:
|
|
57
|
+
"""Killing while a slow scan is in flight must not wait for the scan."""
|
|
58
|
+
probe = tmp_path / "probe.txt"
|
|
59
|
+
client = _started_client(pool=None)
|
|
60
|
+
gen = client.table_function(
|
|
61
|
+
function_name="slow_cancellable",
|
|
62
|
+
schema_name="main",
|
|
63
|
+
arguments=Arguments(positional=(pa.scalar(str(probe)),), named={"sleep_ms": pa.scalar(_SLEEP_MS)}),
|
|
64
|
+
)
|
|
65
|
+
next(gen) # worker is now mid-stream, sleeping between batches
|
|
66
|
+
|
|
67
|
+
started = time.monotonic()
|
|
68
|
+
client.stop(force=True)
|
|
69
|
+
elapsed = time.monotonic() - started
|
|
70
|
+
assert elapsed < _SLEEP_MS / 1000.0, f"forced stop waited {elapsed:.2f}s on a sleeping worker"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_graceful_stop_is_unchanged() -> None:
|
|
74
|
+
"""Force defaults to False and still reports a clean exit."""
|
|
75
|
+
client = _started_client(pool=None)
|
|
76
|
+
assert client.stop() == 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_force_is_accepted_by_pooled_workers() -> None:
|
|
80
|
+
"""Pooled workers have no proc to kill, so force must be inert rather than an error."""
|
|
81
|
+
client = _started_client(pool=_default_pool)
|
|
82
|
+
assert client._primary is not None # noqa: SLF001 - a started client always has one
|
|
83
|
+
assert client._primary.proc is None # noqa: SLF001 - documents why force is inert here
|
|
84
|
+
assert client.stop(force=True) == 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_stop_still_requires_a_started_client() -> None:
|
|
88
|
+
client = Client("vgi-fixture-worker", pool=None)
|
|
89
|
+
with pytest.raises(Exception, match="not started"):
|
|
90
|
+
client.stop(force=True)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_client_can_restart_after_a_forced_stop() -> None:
|
|
94
|
+
"""Force must leave the client in the same reusable state as a graceful stop."""
|
|
95
|
+
client = _started_client(pool=None)
|
|
96
|
+
client.stop(force=True)
|
|
97
|
+
client.start()
|
|
98
|
+
try:
|
|
99
|
+
assert client._primary is not None # noqa: SLF001 - restart sanity check
|
|
100
|
+
finally:
|
|
101
|
+
client.stop()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
|
|
3
|
+
# ruff: noqa: S101, D102, D103
|
|
4
|
+
"""Tests for ``Client.table_column_statistics``.
|
|
5
|
+
|
|
6
|
+
Workers may inline statistics on ``TableInfo`` or serve them lazily through the per-table
|
|
7
|
+
``catalog_table_column_statistics_get`` RPC. This wrapper fetches the lazy form and decodes
|
|
8
|
+
it, so a client never has to reach past the public API to reimplement the sparse-union wire
|
|
9
|
+
layout. Driven against the real ``vgi-fixture-worker`` over every client transport.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from vgi.catalog import ColumnStatistics, deserialize_column_statistics
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def attached(client_transport: Any) -> Any:
|
|
23
|
+
"""Yield ``(client, attach_opaque_data)`` for the example catalog."""
|
|
24
|
+
with client_transport() as client:
|
|
25
|
+
res = client.catalog_attach(name="example", data_version_spec=None, implementation_version=None)
|
|
26
|
+
yield client, res.attach_opaque_data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_returns_typed_statistics(attached: Any) -> None:
|
|
30
|
+
client, aod = attached
|
|
31
|
+
stats = client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="numbers")
|
|
32
|
+
assert stats
|
|
33
|
+
assert all(isinstance(s, ColumnStatistics) for s in stats)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_decodes_min_max_for_a_known_table(attached: Any) -> None:
|
|
37
|
+
"""`numbers` holds integers 0-99, so its bounds are known exactly."""
|
|
38
|
+
client, aod = attached
|
|
39
|
+
stats = client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="numbers")
|
|
40
|
+
value = next(s for s in stats if s.column_name == "value")
|
|
41
|
+
assert value.min is not None
|
|
42
|
+
assert value.max is not None
|
|
43
|
+
assert value.min.as_py() == 0
|
|
44
|
+
assert value.max.as_py() == 99
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_matches_the_inlined_statistics(attached: Any) -> None:
|
|
48
|
+
"""The lazy RPC and the copy inlined on TableInfo must agree.
|
|
49
|
+
|
|
50
|
+
``TableInfo.column_statistics`` arrives client-side as the raw wire bytes, so this is
|
|
51
|
+
also the case that motivates a public deserializer: without one, a caller holding an
|
|
52
|
+
inlined blob has no supported way to read it.
|
|
53
|
+
"""
|
|
54
|
+
client, aod = attached
|
|
55
|
+
info = client.table_get(attach_opaque_data=aod, schema_name="data", name="numbers")
|
|
56
|
+
assert info is not None
|
|
57
|
+
if not info.column_statistics:
|
|
58
|
+
pytest.skip("worker does not inline statistics for this table")
|
|
59
|
+
fetched = client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="numbers")
|
|
60
|
+
inlined = {s.column_name: s for s in deserialize_column_statistics(info.column_statistics)}
|
|
61
|
+
assert {s.column_name for s in fetched} == set(inlined)
|
|
62
|
+
for s in fetched:
|
|
63
|
+
other = inlined[s.column_name]
|
|
64
|
+
assert (s.min is None) == (other.min is None)
|
|
65
|
+
if s.min is not None and other.min is not None:
|
|
66
|
+
assert s.min.as_py() == other.min.as_py()
|
|
67
|
+
assert s.has_null == other.has_null
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_unknown_table_returns_empty_list(attached: Any) -> None:
|
|
71
|
+
"""A missing table yields no statistics rather than raising."""
|
|
72
|
+
client, aod = attached
|
|
73
|
+
assert client.table_column_statistics(attach_opaque_data=aod, schema_name="data", name="no_such_table") == []
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import sys
|
|
8
9
|
from unittest.mock import patch
|
|
9
10
|
|
|
10
11
|
import pyarrow as pa
|
|
@@ -14,6 +15,16 @@ from vgi.arguments import Arguments
|
|
|
14
15
|
from vgi.client.client import Client, ClientError
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
def _stderr_worker(*lines: str) -> list[str]:
|
|
19
|
+
"""Argv for a worker that writes ``lines`` to stderr and exits non-zero.
|
|
20
|
+
|
|
21
|
+
An argv list rather than a command string: the ``-c`` body carries spaces and
|
|
22
|
+
quotes, which no single quoting convention survives on both POSIX and Windows.
|
|
23
|
+
"""
|
|
24
|
+
body = "import sys; " + "".join(f"sys.stderr.write({line!r} + chr(10)); " for line in lines)
|
|
25
|
+
return [sys.executable, "-c", body + "sys.stderr.flush(); sys.exit(1)"]
|
|
26
|
+
|
|
27
|
+
|
|
17
28
|
class TestWorkerDebugEnvVar:
|
|
18
29
|
"""Tests for VGI_WORKER_DEBUG environment variable behavior."""
|
|
19
30
|
|
|
@@ -53,14 +64,7 @@ class TestStderrInErrorMessages:
|
|
|
53
64
|
|
|
54
65
|
def test_error_includes_stderr_on_worker_failure(self) -> None:
|
|
55
66
|
"""Error messages should include stderr when worker fails (non-pooled)."""
|
|
56
|
-
worker_script = (
|
|
57
|
-
'python -c "'
|
|
58
|
-
"import sys; "
|
|
59
|
-
"sys.stderr.write('Debug: bind starting\\n'); "
|
|
60
|
-
"sys.stderr.write('Error: function not found\\n'); "
|
|
61
|
-
"sys.stderr.flush(); "
|
|
62
|
-
'sys.exit(1)"'
|
|
63
|
-
)
|
|
67
|
+
worker_script = _stderr_worker("Debug: bind starting", "Error: function not found")
|
|
64
68
|
|
|
65
69
|
client = Client(worker_script, pool=None)
|
|
66
70
|
with pytest.raises(ClientError) as exc_info:
|
|
@@ -80,7 +84,7 @@ class TestStderrInErrorMessages:
|
|
|
80
84
|
|
|
81
85
|
def test_error_no_stderr_section_when_passthrough(self) -> None:
|
|
82
86
|
"""Error messages should NOT include 'Worker stderr:' when passthrough is enabled."""
|
|
83
|
-
worker_script = "
|
|
87
|
+
worker_script = _stderr_worker("Debug info")
|
|
84
88
|
|
|
85
89
|
client = Client(worker_script, passthrough_stderr=True, pool=None)
|
|
86
90
|
with pytest.raises(ClientError) as exc_info:
|
|
@@ -99,9 +103,7 @@ class TestStderrInErrorMessages:
|
|
|
99
103
|
|
|
100
104
|
def test_stderr_enrichment_on_table_in_out_function(self) -> None:
|
|
101
105
|
"""table_in_out_function errors should include stderr."""
|
|
102
|
-
worker_script = (
|
|
103
|
-
"python -c \"import sys; sys.stderr.write('worker log line\\n'); sys.stderr.flush(); sys.exit(1)\""
|
|
104
|
-
)
|
|
106
|
+
worker_script = _stderr_worker("worker log line")
|
|
105
107
|
|
|
106
108
|
batch = pa.RecordBatch.from_pydict({"x": [1, 2, 3]})
|
|
107
109
|
client = Client(worker_script, pool=None)
|
|
@@ -120,7 +122,7 @@ class TestStderrInErrorMessages:
|
|
|
120
122
|
|
|
121
123
|
def test_stderr_enrichment_on_scalar_function(self) -> None:
|
|
122
124
|
"""scalar_function errors should include stderr."""
|
|
123
|
-
worker_script = "
|
|
125
|
+
worker_script = _stderr_worker("scalar debug")
|
|
124
126
|
|
|
125
127
|
batch = pa.RecordBatch.from_pydict({"x": [1, 2, 3]})
|
|
126
128
|
client = Client(worker_script, pool=None)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import sys
|
|
7
8
|
import time
|
|
8
9
|
|
|
9
10
|
import pyarrow as pa
|
|
@@ -249,14 +250,17 @@ class TestWorkerStderrCapture:
|
|
|
249
250
|
def test_stderr_available_on_error(self) -> None:
|
|
250
251
|
"""Should be able to access stderr after an error occurs."""
|
|
251
252
|
# Use a worker script that writes to stderr then fails
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
# Argv, not a command string: the -c body carries spaces and quotes, which
|
|
254
|
+
# no single quoting convention survives on both POSIX and Windows.
|
|
255
|
+
worker_script = [
|
|
256
|
+
sys.executable,
|
|
257
|
+
"-c",
|
|
254
258
|
"import sys; "
|
|
255
|
-
"sys.stderr.write('Debug: worker starting
|
|
256
|
-
"sys.stderr.write('Error: something went wrong
|
|
259
|
+
"sys.stderr.write('Debug: worker starting' + chr(10)); "
|
|
260
|
+
"sys.stderr.write('Error: something went wrong' + chr(10)); "
|
|
257
261
|
"sys.stderr.flush(); "
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
"sys.exit(1)",
|
|
263
|
+
]
|
|
260
264
|
|
|
261
265
|
client = Client(worker_script, pool=None)
|
|
262
266
|
client.start()
|