vgi-python 0.26.0__tar.gz → 0.28.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. {vgi_python-0.26.0 → vgi_python-0.28.0}/PKG-INFO +2 -2
  2. vgi_python-0.28.0/RELEASE-RUNBOOK.md +69 -0
  3. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/column-statistics.md +24 -0
  4. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/how-to/function-patterns.md +25 -0
  5. vgi_python-0.28.0/examples/batch_index_worker.py +142 -0
  6. {vgi_python-0.26.0 → vgi_python-0.28.0}/pyproject.toml +1 -1
  7. vgi_python-0.28.0/tests/catalog/test_attach_option.py +192 -0
  8. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_column_statistics.py +86 -0
  9. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/client/test_broken_pipe.py +20 -9
  10. vgi_python-0.28.0/tests/client/test_stop_force.py +101 -0
  11. vgi_python-0.28.0/tests/client/test_table_column_statistics.py +73 -0
  12. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/client/test_worker_debug.py +15 -13
  13. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/test_client.py +10 -6
  14. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_examples_workers.py +20 -0
  15. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_serve.py +10 -5
  16. {vgi_python-0.26.0 → vgi_python-0.28.0}/uv.lock +525 -336
  17. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/attach_options.py +34 -3
  18. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/http_server.py +5 -14
  19. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/__init__.py +7 -0
  20. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/_descriptor_spec.py +39 -2
  21. vgi_python-0.28.0/vgi/catalog/attach_option.py +157 -0
  22. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/catalog_interface.py +68 -0
  23. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/catalog_mixin.py +53 -3
  24. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/client.py +82 -26
  25. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/http/landing.html +260 -18
  26. vgi_python-0.28.0/vgi/http/landing_page.py +112 -0
  27. vgi_python-0.28.0/vgi/http/vgi-client.js +23 -0
  28. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/serve.py +3 -10
  29. vgi_python-0.26.0/tests/landing/_checker.py +0 -153
  30. vgi_python-0.26.0/tests/landing/describe.expected.json +0 -3442
  31. vgi_python-0.26.0/tests/landing/describe.schema.json +0 -402
  32. vgi_python-0.26.0/tests/test_landing_conformance.py +0 -93
  33. vgi_python-0.26.0/vgi/catalog/attach_option.py +0 -49
  34. vgi_python-0.26.0/vgi/http/describe_json.py +0 -461
  35. vgi_python-0.26.0/vgi/http/landing_page.py +0 -95
  36. {vgi_python-0.26.0 → vgi_python-0.28.0}/.gitattributes +0 -0
  37. {vgi_python-0.26.0 → vgi_python-0.28.0}/.github/dependabot.yml +0 -0
  38. {vgi_python-0.26.0 → vgi_python-0.28.0}/.github/styles/config/vocabularies/VGI/accept.txt +0 -0
  39. {vgi_python-0.26.0 → vgi_python-0.28.0}/.github/workflows/ci.yml +0 -0
  40. {vgi_python-0.26.0 → vgi_python-0.28.0}/.github/workflows/docs.yml +0 -0
  41. {vgi_python-0.26.0 → vgi_python-0.28.0}/.github/workflows/integration.yml +0 -0
  42. {vgi_python-0.26.0 → vgi_python-0.28.0}/.github/workflows/release.yml +0 -0
  43. {vgi_python-0.26.0 → vgi_python-0.28.0}/.gitignore +0 -0
  44. {vgi_python-0.26.0 → vgi_python-0.28.0}/.python-version +0 -0
  45. {vgi_python-0.26.0 → vgi_python-0.28.0}/.vale.ini +0 -0
  46. {vgi_python-0.26.0 → vgi_python-0.28.0}/CLAUDE.md +0 -0
  47. {vgi_python-0.26.0 → vgi_python-0.28.0}/DOCS_ACCEPTANCE_CRITERIA.md +0 -0
  48. {vgi_python-0.26.0 → vgi_python-0.28.0}/DOCS_REVIEW_RUBRIC.md +0 -0
  49. {vgi_python-0.26.0 → vgi_python-0.28.0}/DOCS_USABILITY_TEST.md +0 -0
  50. {vgi_python-0.26.0 → vgi_python-0.28.0}/LICENSE +0 -0
  51. {vgi_python-0.26.0 → vgi_python-0.28.0}/README.md +0 -0
  52. {vgi_python-0.26.0 → vgi_python-0.28.0}/SECURITY.md +0 -0
  53. {vgi_python-0.26.0 → vgi_python-0.28.0}/ci/README.md +0 -0
  54. {vgi_python-0.26.0 → vgi_python-0.28.0}/ci/preprocess-require.awk +0 -0
  55. {vgi_python-0.26.0 → vgi_python-0.28.0}/ci/run-integration.sh +0 -0
  56. {vgi_python-0.26.0 → vgi_python-0.28.0}/dist-vgi/.gitignore +0 -0
  57. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/aggregate-functions.md +0 -0
  58. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/arguments.md +0 -0
  59. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/auth.md +0 -0
  60. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/catalogs.md +0 -0
  61. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/client.md +0 -0
  62. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/exceptions.md +0 -0
  63. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/filters.md +0 -0
  64. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/functions.md +0 -0
  65. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/http.md +0 -0
  66. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/index.md +0 -0
  67. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/metadata.md +0 -0
  68. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/observability.md +0 -0
  69. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/storage.md +0 -0
  70. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/transactor.md +0 -0
  71. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/api/worker.md +0 -0
  72. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/argument-serialization.md +0 -0
  73. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/apple-touch-icon.png +0 -0
  74. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/favicon-16x16.png +0 -0
  75. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/favicon-32x32.png +0 -0
  76. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/favicon.ico +0 -0
  77. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/kinds/aggregate.svg +0 -0
  78. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/kinds/buffering.svg +0 -0
  79. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/kinds/scalar.svg +0 -0
  80. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/kinds/table-in-out.svg +0 -0
  81. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/kinds/table.svg +0 -0
  82. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/logo.png +0 -0
  83. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/assets/social-card.png +0 -0
  84. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/authentication.md +0 -0
  85. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/catalog-interface.md +0 -0
  86. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/cli.md +0 -0
  87. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/concepts/index.md +0 -0
  88. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/contributing-docs.md +0 -0
  89. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/filter-pushdown.md +0 -0
  90. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/generator-api.md +0 -0
  91. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/global-functions.md +0 -0
  92. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/how-to/catalogs.md +0 -0
  93. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/how-to/http-auth.md +0 -0
  94. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/how-to/index.md +0 -0
  95. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/how-to/pushdown-and-statistics.md +0 -0
  96. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/how-to/state-storage.md +0 -0
  97. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/index.md +0 -0
  98. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/lifecycle.md +0 -0
  99. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/metadata.md +0 -0
  100. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/overrides/main.html +0 -0
  101. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/robots.txt +0 -0
  102. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/shared-storage.md +0 -0
  103. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/stylesheets/extra.css +0 -0
  104. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/tutorial/index.md +0 -0
  105. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/tutorial/scalar.md +0 -0
  106. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/tutorial/table.md +0 -0
  107. {vgi_python-0.26.0 → vgi_python-0.28.0}/docs/vgi-logo.png +0 -0
  108. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/calc_scalar_worker.py +0 -0
  109. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/calc_worker.py +0 -0
  110. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/filter_worker.py +0 -0
  111. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/greeting_scalar_worker.py +0 -0
  112. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/row_count_worker.py +0 -0
  113. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/series_streaming_worker.py +0 -0
  114. {vgi_python-0.26.0 → vgi_python-0.28.0}/examples/sum_worker.py +0 -0
  115. {vgi_python-0.26.0 → vgi_python-0.28.0}/mkdocs.yml +0 -0
  116. {vgi_python-0.26.0 → vgi_python-0.28.0}/packages/vgi-fixtures/LICENSE +0 -0
  117. {vgi_python-0.26.0 → vgi_python-0.28.0}/packages/vgi-fixtures/README.md +0 -0
  118. {vgi_python-0.26.0 → vgi_python-0.28.0}/packages/vgi-fixtures/docker/Dockerfile +0 -0
  119. {vgi_python-0.26.0 → vgi_python-0.28.0}/packages/vgi-fixtures/docker/docker-entrypoint.sh +0 -0
  120. {vgi_python-0.26.0 → vgi_python-0.28.0}/packages/vgi-fixtures/pyproject.toml +0 -0
  121. {vgi_python-0.26.0 → vgi_python-0.28.0}/scripts/measure_startup.py +0 -0
  122. {vgi_python-0.26.0 → vgi_python-0.28.0}/scripts/run_all_tests.sh +0 -0
  123. {vgi_python-0.26.0 → vgi_python-0.28.0}/test-data/generate.sh +0 -0
  124. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/__init__.py +0 -0
  125. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/_http_fixtures.py +0 -0
  126. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/__init__.py +0 -0
  127. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_catalog_interface.py +0 -0
  128. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_client_catalog.py +0 -0
  129. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_declarative.py +0 -0
  130. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_example_worker_catalog.py +0 -0
  131. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_integration.py +0 -0
  132. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_required_filters.py +0 -0
  133. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_scan_branches.py +0 -0
  134. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_serialization.py +0 -0
  135. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_setting.py +0 -0
  136. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_storage.py +0 -0
  137. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_time_travel.py +0 -0
  138. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/catalog/test_writable_table.py +0 -0
  139. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/client/__init__.py +0 -0
  140. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/client/test_cli.py +0 -0
  141. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/client/test_cli_catalog_functions.py +0 -0
  142. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/__init__.py +0 -0
  143. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/_stub.py +0 -0
  144. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/conftest.py +0 -0
  145. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_accumulate.py +0 -0
  146. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_aggregate.py +0 -0
  147. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_attach.py +0 -0
  148. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_bearer_auth.py +0 -0
  149. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_const_param_enforcement.py +0 -0
  150. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_container.py +0 -0
  151. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_copy_from.py +0 -0
  152. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_copy_to.py +0 -0
  153. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_directory_parity.py +0 -0
  154. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_function_inventory.py +0 -0
  155. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_github.py +0 -0
  156. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_http_client.py +0 -0
  157. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_http_external_location.py +0 -0
  158. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_http_upload_url.py +0 -0
  159. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_macro.py +0 -0
  160. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_overload.py +0 -0
  161. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_protocol_inventory.py +0 -0
  162. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_protocol_version.py +0 -0
  163. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_resumable_scan.py +0 -0
  164. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_scalar.py +0 -0
  165. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_scalar_attach_opaque_data.py +0 -0
  166. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_secret.py +0 -0
  167. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_settings.py +0 -0
  168. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_table.py +0 -0
  169. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_table_in_out.py +0 -0
  170. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_view.py +0 -0
  171. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conformance/test_writable.py +0 -0
  172. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/conftest.py +0 -0
  173. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/__init__.py +0 -0
  174. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_bernoulli_function.py +0 -0
  175. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_binary_packet_function.py +0 -0
  176. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_client.py +0 -0
  177. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_conditional_message_function.py +0 -0
  178. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_hash_seed_function.py +0 -0
  179. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_multiply_by_setting_function.py +0 -0
  180. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_multiply_function.py +0 -0
  181. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_random_bytes_function.py +0 -0
  182. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/scalar/test_return_secret_value_function.py +0 -0
  183. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/__init__.py +0 -0
  184. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/__init__.py +0 -0
  185. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_constant_columns_function.py +0 -0
  186. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_double_sequence_function.py +0 -0
  187. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_exception_function.py +0 -0
  188. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_filter_echo_function.py +0 -0
  189. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_logging_function.py +0 -0
  190. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_nested_sequence_function.py +0 -0
  191. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_partitioned_function.py +0 -0
  192. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_projected_data_function.py +0 -0
  193. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_sequence_function.py +0 -0
  194. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_settings_function.py +0 -0
  195. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_struct_settings_function.py +0 -0
  196. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/generator/test_ten_thousand_function.py +0 -0
  197. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table/test_wide_columns.py +0 -0
  198. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/__init__.py +0 -0
  199. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/__init__.py +0 -0
  200. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_buffer_input_function.py +0 -0
  201. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_echo_function.py +0 -0
  202. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_exception_functions.py +0 -0
  203. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_filter_by_setting_function.py +0 -0
  204. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_repeat_inputs_function.py +0 -0
  205. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_substream_partial_sum_function.py +0 -0
  206. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/generator/test_sum_all_columns_function.py +0 -0
  207. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/table_in_out/test_blended_metadata.py +0 -0
  208. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_access_log_audit.py +0 -0
  209. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_aggregate_function.py +0 -0
  210. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_argument_spec.py +0 -0
  211. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_auth.py +0 -0
  212. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_bind_exceptions.py +0 -0
  213. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_bind_request_at_clause.py +0 -0
  214. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_bound_storage_conformance.py +0 -0
  215. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_cache_control.py +0 -0
  216. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_call_state_attach.py +0 -0
  217. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_catalog_auth_binding.py +0 -0
  218. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_copy_from_function.py +0 -0
  219. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_copy_to_function.py +0 -0
  220. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_docstrings.py +0 -0
  221. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_documentation_examples.py +0 -0
  222. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_example_function_arg_types.py +0 -0
  223. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_exception_handling.py +0 -0
  224. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_exceptions.py +0 -0
  225. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_filter_pushdown.py +0 -0
  226. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_filter_pushdown_extension.py +0 -0
  227. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_function_storage.py +0 -0
  228. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_function_storage_azure_sql.py +0 -0
  229. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_function_storage_cf_do.py +0 -0
  230. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_function_storage_cf_do_integration.py +0 -0
  231. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_function_storage_conformance.py +0 -0
  232. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_cpp_constants.py +0 -0
  233. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_cpp_protocol_version.py +0 -0
  234. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_cpp_request_builders.py +0 -0
  235. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_cpp_schemas.py +0 -0
  236. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_cpp_secret.py +0 -0
  237. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_go_schemas.py +0 -0
  238. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_protocol_version.py +0 -0
  239. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_schemas_cross_lang.py +0 -0
  240. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_ts_client.py +0 -0
  241. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_generated_ts_schemas.py +0 -0
  242. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_http_demo_storage.py +0 -0
  243. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_http_s3_offload_input.py +0 -0
  244. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_http_s3_offload_output.py +0 -0
  245. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_metadata.py +0 -0
  246. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_mypy_consolidated.py +0 -0
  247. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_nest_tensor.py +0 -0
  248. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_otel.py +0 -0
  249. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_projection_enforcement.py +0 -0
  250. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_projection_repro.py +0 -0
  251. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_protocol_classes.py +0 -0
  252. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_resolved_secrets.py +0 -0
  253. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_schema_scoped_functions.py +0 -0
  254. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_schema_utils.py +0 -0
  255. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_setting_secret_annotations.py +0 -0
  256. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_signing_key.py +0 -0
  257. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_table_buffering_function.py +0 -0
  258. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_table_function_dynamic_to_string.py +0 -0
  259. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_tcp_transport.py +0 -0
  260. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_type_bounds.py +0 -0
  261. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_union_argument.py +0 -0
  262. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_worker.py +0 -0
  263. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_worker_cli.py +0 -0
  264. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/test_worker_page.py +0 -0
  265. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/transactor/__init__.py +0 -0
  266. {vgi_python-0.26.0 → vgi_python-0.28.0}/tests/transactor/test_transactor.py +0 -0
  267. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/__init__.py +0 -0
  268. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_duckdb.py +0 -0
  269. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_storage_profile.py +0 -0
  270. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/__init__.py +0 -0
  271. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/accumulate/__init__.py +0 -0
  272. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/accumulate/worker.py +0 -0
  273. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/__init__.py +0 -0
  274. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/_common.py +0 -0
  275. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/basic.py +0 -0
  276. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/dynamic.py +0 -0
  277. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/generic.py +0 -0
  278. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/listagg.py +0 -0
  279. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/percentile.py +0 -0
  280. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/same_name.py +0 -0
  281. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/secret_typed.py +0 -0
  282. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/streaming.py +0 -0
  283. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/varargs.py +0 -0
  284. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/aggregate/window.py +0 -0
  285. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/bad_enum.py +0 -0
  286. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/bad_protocol.py +0 -0
  287. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/cancellable.py +0 -0
  288. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/catalog.py +0 -0
  289. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/companion.py +0 -0
  290. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/copy_from.py +0 -0
  291. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/copy_to.py +0 -0
  292. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/global_functions.py +0 -0
  293. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/narrow_bind/__init__.py +0 -0
  294. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/narrow_bind/worker.py +0 -0
  295. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/nest_tensor.py +0 -0
  296. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/orchard_catalog.py +0 -0
  297. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/projection_repro/__init__.py +0 -0
  298. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/projection_repro/worker.py +0 -0
  299. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/__init__.py +0 -0
  300. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/_common.py +0 -0
  301. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/arithmetic.py +0 -0
  302. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/bench_ladder.py +0 -0
  303. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/binary.py +0 -0
  304. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/formatting.py +0 -0
  305. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/geo.py +0 -0
  306. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/null_handling.py +0 -0
  307. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/random_demo.py +0 -0
  308. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/same_name.py +0 -0
  309. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/settings_secrets.py +0 -0
  310. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/scalar/type_info.py +0 -0
  311. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/schema_reconcile/__init__.py +0 -0
  312. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/schema_reconcile/worker.py +0 -0
  313. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/simple_writable.py +0 -0
  314. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/__init__.py +0 -0
  315. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/_common.py +0 -0
  316. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/batch_index.py +0 -0
  317. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/batch_index_broken.py +0 -0
  318. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/cache.py +0 -0
  319. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/catalog_scans.py +0 -0
  320. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/filters.py +0 -0
  321. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/late_materialization.py +0 -0
  322. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/make_series.py +0 -0
  323. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/misc.py +0 -0
  324. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/order_modes.py +0 -0
  325. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/pairs.py +0 -0
  326. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/partition_columns.py +0 -0
  327. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/partition_columns_broken.py +0 -0
  328. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/profiling_example.py +0 -0
  329. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/required_filters.py +0 -0
  330. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/same_name_cached.py +0 -0
  331. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/sequence.py +0 -0
  332. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/settings.py +0 -0
  333. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/transaction_storage.py +0 -0
  334. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/tt_pushdown.py +0 -0
  335. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/typed_probe.py +0 -0
  336. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/versioned.py +0 -0
  337. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table/wide.py +0 -0
  338. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table_in_out.py +0 -0
  339. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/table_in_out_same_name.py +0 -0
  340. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/twin_catalogs.py +0 -0
  341. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/versioned.py +0 -0
  342. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/versioned_tables.py +0 -0
  343. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/worker.py +0 -0
  344. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/writable/__init__.py +0 -0
  345. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/writable/generic.py +0 -0
  346. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/writable/table.py +0 -0
  347. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/_test_fixtures/writable/worker.py +0 -0
  348. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/aggregate_function.py +0 -0
  349. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/argument_spec.py +0 -0
  350. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/arguments.py +0 -0
  351. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/attach_header.py +0 -0
  352. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/auth.py +0 -0
  353. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/cache_control.py +0 -0
  354. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/descriptors.py +0 -0
  355. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/duckdb_statistics.py +0 -0
  356. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/secret_type.py +0 -0
  357. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/setting.py +0 -0
  358. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/catalog/storage.py +0 -0
  359. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/__init__.py +0 -0
  360. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli.py +0 -0
  361. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli_catalog.py +0 -0
  362. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli_schema.py +0 -0
  363. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli_table.py +0 -0
  364. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli_transaction.py +0 -0
  365. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli_utils.py +0 -0
  366. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/client/cli_view.py +0 -0
  367. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/__init__.py +0 -0
  368. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/_common.py +0 -0
  369. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_constants.py +0 -0
  370. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_protocol_version.py +0 -0
  371. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_request_builders.py +0 -0
  372. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_schemas.py +0 -0
  373. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_secret_protocol_version.py +0 -0
  374. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_secret_request_builders.py +0 -0
  375. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/cpp_secret_schemas.py +0 -0
  376. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/go_schemas.py +0 -0
  377. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/protocol_version.py +0 -0
  378. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/ts_client.py +0 -0
  379. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/codegen/ts_schemas.py +0 -0
  380. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/copy_from_function.py +0 -0
  381. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/copy_to_function.py +0 -0
  382. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/exceptions.py +0 -0
  383. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/function.py +0 -0
  384. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/function_storage.py +0 -0
  385. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/function_storage_azure_sql.py +0 -0
  386. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/function_storage_cf_do.py +0 -0
  387. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/http/__init__.py +0 -0
  388. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/http/demo_storage.py +0 -0
  389. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/http/worker_page.py +0 -0
  390. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/invocation.py +0 -0
  391. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/logging_config.py +0 -0
  392. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/meta_worker.py +0 -0
  393. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/metadata.py +0 -0
  394. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/otel.py +0 -0
  395. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/profiling.py +0 -0
  396. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/protocol.py +0 -0
  397. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/protocol_version.txt +0 -0
  398. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/py.typed +0 -0
  399. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/scalar_function.py +0 -0
  400. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/schema_utils.py +0 -0
  401. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/secret_protocol.py +0 -0
  402. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/secret_service.py +0 -0
  403. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/table_buffering_function.py +0 -0
  404. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/table_filter_pushdown.py +0 -0
  405. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/table_function.py +0 -0
  406. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/table_in_out_function.py +0 -0
  407. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/transactor/__init__.py +0 -0
  408. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/transactor/_duckdb_compat.py +0 -0
  409. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/transactor/client.py +0 -0
  410. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/transactor/protocol.py +0 -0
  411. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/transactor/server.py +0 -0
  412. {vgi_python-0.26.0 → vgi_python-0.28.0}/vgi/worker.py +0 -0
  413. {vgi_python-0.26.0 → vgi_python-0.28.0}/wrangler.jsonc +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: vgi-python
3
- Version: 0.26.0
3
+ Version: 0.28.0
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
@@ -0,0 +1,69 @@
1
+ # Release runbook — describe.json retirement + attach-option `required`
2
+
3
+ All code is committed on `main` in each repo and green. Nothing is pushed.
4
+ This is the ordering and the per-repo trigger; fill in `X.Y.Z` per package.
5
+
6
+ **Version decision needed first.** Three transports drop `DescribeProvider`
7
+ from their public API (breaking): `@query-farm/vgi-rpc`, `vgi-rpc` (Rust),
8
+ `farm.query:vgirpc`. Choose major or minor per your compatibility policy — it
9
+ cascades into the implementation pins below.
10
+
11
+ ## 1. Transports (must publish first)
12
+
13
+ | Repo | Trigger | Version lives in |
14
+ |---|---|---|
15
+ | vgi-rpc-typescript | `gh release create vX.Y.Z` (release: published) | `package.json` (+ `bun install` to sync `bun.lock`) |
16
+ | vgi-rpc-rust | tag push — `gh release create vX.Y.Z` | `[workspace.package] version`; `cargo update -w` |
17
+ | vgi-rpc-java | `gh release create vX.Y.Z` (release: published) | `build.gradle.kts` |
18
+ | vgi-rpc-go | tag push — `gh release create vX.Y.Z` | none (module proxy reads the tag) |
19
+
20
+ Then wait for index propagation: npm, `https://index.crates.io/3/v/vgi-rpc`,
21
+ Maven Central, Go proxy.
22
+
23
+ ## 2. Implementation dependency pins
24
+
25
+ These currently cannot build against published transports:
26
+
27
+ - **vgi-typescript** — peer range is `>=0.18.1 <0.19.0`; widen to the new
28
+ transport version. `LandingInfo` resolves only once the transport publishes.
29
+ - **vgi-rust** — workspace dep already bumped to `vgi-rpc 0.20.0`; adjust if you
30
+ pick a different number. The `[patch.crates-io]` block stays uncommitted (it
31
+ is documented in `Cargo.toml` for local co-development only).
32
+ - **vgi-java** — bump `api("farm.query:vgirpc:X")` and the `VGI_RPC_JAVA_REF`
33
+ pin in `.github/workflows/integration.yml`. (`landing.yml` was deleted with
34
+ the contract it checked.)
35
+ - **vgi-go** — self-contained; no pin, but it needs the new vgi-rpc-go tag.
36
+
37
+ ## 3. Implementations
38
+
39
+ | Repo | Trigger | Version lives in |
40
+ |---|---|---|
41
+ | vgi-python | `gh release create vX.Y.Z --target main` (PyPI on release) | root `pyproject.toml`; `uv lock` |
42
+ | vgi-go | `gh release create vX.Y.Z` (tag → module proxy) | none |
43
+ | vgi-rust | `gh release create vX.Y.Z` (tag → crates.io) | `[workspace.package] version` |
44
+ | vgi-typescript | **`git tag vX.Y.Z && git push origin vX.Y.Z`** — pushing the bump to main does NOT publish and fails silently | `package.json`; `bun install` |
45
+ | vgi-java | `gh release create vX.Y.Z` (Maven Central on release) | `build.gradle.kts` |
46
+
47
+ ## 4. Frontend
48
+
49
+ - **vgi-web-frontend** — auto-deploys on push to `main`. Push last, so no worker
50
+ is serving a page newer than its vendored bundle.
51
+
52
+ ## Verified before release
53
+
54
+ - vgi-python 2175 pass / 99 skip; ruff + mypy clean
55
+ - vgi-go `go test ./...` clean; vet + gofmt clean
56
+ - vgi-rust tests pass; clippy `-D warnings` clean
57
+ - vgi-java `:vgi:test` passes
58
+ - vgi-rpc-typescript 715 pass / 26 fail — exactly the pre-change baseline
59
+ (confirmed by running the suite at the parent commit in a worktree)
60
+ - Landing page verified in a browser against all four language workers:
61
+ Python 221 functions, Go 221, Rust 654, Java 219 — each 2 schemas, 59 tables,
62
+ 3 views, with lazy columns loading.
63
+
64
+ ## Not done, tracked
65
+
66
+ - `vgi-rpc-typescript` has 26 pre-existing test failures (integration/streaming;
67
+ unrelated to this work, present before it).
68
+ - `~/vgi-java/CLAUDE.md` still references the landing-conformance workflow in
69
+ its historical notes; the workflow itself is deleted.
@@ -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()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vgi-python"
3
- version = "0.26.0"
3
+ version = "0.28.0"
4
4
  description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -0,0 +1,192 @@
1
+ # Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
+
3
+ """Tests for the AttachOption descriptor, its ``required`` flag, and validation."""
4
+
5
+ from typing import Annotated
6
+
7
+ import pyarrow as pa
8
+ import pytest
9
+ from vgi_rpc.utils import deserialize_record_batch, serialize_record_batch_bytes
10
+
11
+ from vgi.catalog._descriptor_spec import _SpecBase
12
+ from vgi.catalog.attach_option import (
13
+ AttachOption,
14
+ AttachOptionSpec,
15
+ MissingAttachOptionsError,
16
+ extract_attach_option_specs,
17
+ validate_required_attach_options,
18
+ )
19
+ from vgi.catalog.catalog_interface import ReadOnlyCatalogInterface
20
+ from vgi.catalog.setting import SettingSpec
21
+
22
+
23
+ class _Options:
24
+ """Attach options mixing defaulted and required entries."""
25
+
26
+ region: Annotated[str, AttachOption(desc="AWS region")] = "us-east-1"
27
+ # No class-level assignment: nothing to fall back to, so the caller must supply it.
28
+ api_key: Annotated[str, AttachOption(desc="API key", required=True)]
29
+
30
+
31
+ def _roundtrip(spec: AttachOptionSpec) -> AttachOptionSpec:
32
+ batch, _ = deserialize_record_batch(spec.serialize())
33
+ return AttachOptionSpec.deserialize(batch)
34
+
35
+
36
+ class TestRequiredExtraction:
37
+ """Extraction of the ``required`` flag from Annotated declarations."""
38
+
39
+ def test_defaults_to_false(self) -> None:
40
+ """An option that isn't declared required isn't required."""
41
+ specs = {s.name: s for s in extract_attach_option_specs(_Options)}
42
+ assert specs["region"].required is False
43
+ assert specs["region"].default == "us-east-1"
44
+
45
+ def test_required_option_extracted(self) -> None:
46
+ """``required=True`` survives extraction, with no default."""
47
+ specs = {s.name: s for s in extract_attach_option_specs(_Options)}
48
+ assert specs["api_key"].required is True
49
+ assert specs["api_key"].default is None
50
+
51
+ def test_required_with_default_rejected(self) -> None:
52
+ """A required option that also defaults is contradictory."""
53
+ with pytest.raises(ValueError, match="required but also declares a default"):
54
+ AttachOptionSpec(name="api_key", desc="", type=pa.string(), default="fallback", required=True)
55
+
56
+
57
+ class TestWireFormat:
58
+ """Serialization of the added ``required`` column."""
59
+
60
+ @pytest.mark.parametrize("required", [True, False], ids=["required", "optional"])
61
+ def test_roundtrip_preserves_required(self, required: bool) -> None:
62
+ """``required`` survives an Arrow IPC round trip in both states."""
63
+ spec = AttachOptionSpec(name="api_key", desc="API key", type=pa.string(), default=None, required=required)
64
+ assert _roundtrip(spec).required is required
65
+
66
+ def test_roundtrip_preserves_shared_fields(self) -> None:
67
+ """Widening the schema doesn't disturb the four shared columns."""
68
+ spec = AttachOptionSpec(name="region", desc="AWS region", type=pa.string(), default="us-east-1")
69
+ restored = _roundtrip(spec)
70
+ assert (restored.name, restored.desc, restored.type, restored.default) == (
71
+ "region",
72
+ "AWS region",
73
+ pa.string(),
74
+ "us-east-1",
75
+ )
76
+
77
+ def test_setting_wire_format_unchanged(self) -> None:
78
+ """``Setting`` keeps the original four columns; only AttachOption widened."""
79
+ assert SettingSpec.ARROW_SCHEMA.names == ["name", "description", "type", "default_value"]
80
+ assert AttachOptionSpec.ARROW_SCHEMA.names == [
81
+ "name",
82
+ "description",
83
+ "type",
84
+ "default_value",
85
+ "required",
86
+ ]
87
+
88
+ def test_reads_batch_without_required_column(self) -> None:
89
+ """A peer that predates the column yields ``required=False``, not an error."""
90
+ type_bytes = pa.schema([pa.field("value", pa.string())]).serialize().to_pybytes()
91
+ legacy = pa.RecordBatch.from_pylist(
92
+ [{"name": "region", "description": "AWS region", "type": type_bytes, "default_value": None}],
93
+ schema=_SpecBase.ARROW_SCHEMA,
94
+ )
95
+ restored = AttachOptionSpec.deserialize(legacy)
96
+ assert restored.required is False
97
+ assert restored.name == "region"
98
+
99
+ def test_older_reader_ignores_required_column(self) -> None:
100
+ """A four-column reader still reads a five-column batch (forward compatible)."""
101
+ spec = AttachOptionSpec(name="api_key", desc="API key", type=pa.string(), default=None, required=True)
102
+ batch, _ = deserialize_record_batch(spec.serialize())
103
+ legacy_read = SettingSpec.deserialize(batch)
104
+ assert legacy_read.name == "api_key"
105
+ assert legacy_read.default is None
106
+
107
+ def test_null_required_reads_as_false(self) -> None:
108
+ """An explicit null in the nullable column means "not required"."""
109
+ type_bytes = pa.schema([pa.field("value", pa.string())]).serialize().to_pybytes()
110
+ batch = pa.RecordBatch.from_pylist(
111
+ [
112
+ {
113
+ "name": "region",
114
+ "description": "",
115
+ "type": type_bytes,
116
+ "default_value": None,
117
+ "required": None,
118
+ }
119
+ ],
120
+ schema=AttachOptionSpec.ARROW_SCHEMA,
121
+ )
122
+ # Round-trip through IPC the way a real peer's bytes would arrive.
123
+ restored, _ = deserialize_record_batch(serialize_record_batch_bytes(batch))
124
+ assert AttachOptionSpec.deserialize(restored).required is False
125
+
126
+
127
+ class TestValidateRequiredAttachOptions:
128
+ """The shared validation helper."""
129
+
130
+ SPECS = [
131
+ AttachOptionSpec(name="region", desc="", type=pa.string(), default="us-east-1"),
132
+ AttachOptionSpec(name="api_key", desc="", type=pa.string(), default=None, required=True),
133
+ ]
134
+
135
+ def test_missing_required_raises(self) -> None:
136
+ """Omitting a required option raises with the names attached."""
137
+ with pytest.raises(MissingAttachOptionsError) as exc:
138
+ validate_required_attach_options("demo", self.SPECS, {"region": "eu-west-1"})
139
+ assert exc.value.missing == ["api_key"]
140
+ assert "'api_key'" in str(exc.value)
141
+
142
+ def test_supplied_required_passes(self) -> None:
143
+ """Supplying the required option is enough; defaults cover the rest."""
144
+ validate_required_attach_options("demo", self.SPECS, {"api_key": "secret"})
145
+
146
+ def test_case_insensitive_match(self) -> None:
147
+ """DuckDB lowercases ATTACH option keys; matching follows."""
148
+ validate_required_attach_options("demo", self.SPECS, {"API_KEY": "secret"})
149
+
150
+ def test_no_required_specs_always_passes(self) -> None:
151
+ """A catalog with no required options accepts an empty mapping."""
152
+ validate_required_attach_options("demo", [self.SPECS[0]], {})
153
+
154
+ def test_multiple_missing_reported_together(self) -> None:
155
+ """Every missing option is reported in one error, not one at a time."""
156
+ specs = [
157
+ AttachOptionSpec(name="api_key", desc="", type=pa.string(), default=None, required=True),
158
+ AttachOptionSpec(name="tenant", desc="", type=pa.string(), default=None, required=True),
159
+ ]
160
+ with pytest.raises(MissingAttachOptionsError) as exc:
161
+ validate_required_attach_options("demo", specs, {})
162
+ assert exc.value.missing == ["api_key", "tenant"]
163
+
164
+
165
+ class _GatedCatalog(ReadOnlyCatalogInterface):
166
+ """Read-only catalog that cannot be attached without ``api_key``."""
167
+
168
+ catalog_name = "gated"
169
+ attach_option_specs = [
170
+ AttachOptionSpec(name="api_key", desc="API key", type=pa.string(), default=None, required=True),
171
+ ]
172
+
173
+
174
+ class TestCatalogAttachEnforcement:
175
+ """The default ``catalog_attach`` refuses an attach missing required options."""
176
+
177
+ def test_attach_without_required_option_raises(self) -> None:
178
+ """The attach fails loudly rather than yielding an empty-looking catalog."""
179
+ with pytest.raises(MissingAttachOptionsError, match="api_key"):
180
+ _GatedCatalog().catalog_attach(
181
+ name="gated", options={}, data_version_spec=None, implementation_version=None
182
+ )
183
+
184
+ def test_attach_with_required_option_succeeds(self) -> None:
185
+ """Supplying the option attaches normally."""
186
+ result = _GatedCatalog().catalog_attach(
187
+ name="gated",
188
+ options={"api_key": "secret"},
189
+ data_version_spec=None,
190
+ implementation_version=None,
191
+ )
192
+ assert result.attach_opaque_data is not None
@@ -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
- # Use 'exit 1' which immediately terminates without reading stdin
31
- # pool=None required because 'exit 1' is a shell command, not a binary
32
- client = Client("exit 1", pool=None)
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
- # pool=None required because 'exit 42' is a shell command, not a binary
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
- # Use 'exit 1' which immediately terminates without reading stdin
88
- client = Client("exit 1")
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("exit 42")
108
+ client = Client(_exits_with(42))
98
109
 
99
110
  try:
100
111
  list(client.catalogs())