e6data-python-connector 3.0.1rc1__tar.gz → 3.0.1rc2__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 (125) hide show
  1. {e6data_python_connector-3.0.1rc1/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc2}/PKG-INFO +113 -63
  2. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/README.md +112 -62
  3. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection.py +47 -0
  4. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection_pool.py +2 -0
  5. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cursor.py +234 -30
  6. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_manager.py +3 -1
  7. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/connection_pool.py +11 -9
  8. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6data_grpc.py +482 -63
  9. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode.py +469 -0
  10. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode_worker.py +50 -0
  11. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_prefetch.py +421 -0
  12. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2/e6data_python_connector.egg-info}/PKG-INFO +113 -63
  13. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/SOURCES.txt +3 -0
  14. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/setup.py +1 -1
  15. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool.py +3 -0
  16. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/LICENSE +0 -0
  17. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/MANIFEST.in +0 -0
  18. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/__init__.py +0 -0
  19. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/aio.py +0 -0
  20. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cluster_manager.py +0 -0
  21. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_dialect.py +0 -0
  22. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_oauth.py +0 -0
  23. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_work.py +0 -0
  24. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/__init__.py +0 -0
  25. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  26. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  27. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/common.py +0 -0
  28. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/constants.py +0 -0
  29. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/datainputstream.py +0 -0
  30. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/date_time_utils.py +0 -0
  31. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/dialect.py +0 -0
  32. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  33. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/constants.py +0 -0
  34. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  35. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/exceptions.py +0 -0
  36. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth.py +0 -0
  37. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_cache.py +0 -0
  38. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_common.py +0 -0
  39. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/result_batch.py +0 -0
  40. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/QueryEngineService.py +0 -0
  41. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/__init__.py +0 -0
  42. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/constants.py +0 -0
  43. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  44. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  45. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  46. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/ttypes.py +0 -0
  47. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/strategy.py +0 -0
  48. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/typeId.py +0 -0
  49. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  50. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  51. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/requires.txt +0 -0
  52. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/top_level.txt +0 -0
  53. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/pyproject.toml +0 -0
  54. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/setup.cfg +0 -0
  55. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/__init__.py +0 -0
  56. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_38_nines.py +0 -0
  57. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_all_cases.py +0 -0
  58. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_binary.py +0 -0
  59. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_correct_value.py +0 -0
  60. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_fields.py +0 -0
  61. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/check_decimal_errors.py +0 -0
  62. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/cleanup_test_files.py +0 -0
  63. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/conftest.py +0 -0
  64. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/debug_38_nines.py +0 -0
  65. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/debug_binary.py +0 -0
  66. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/final_test.py +0 -0
  67. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/__init__.py +0 -0
  68. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_preflight.py +0 -0
  69. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_result_batch_v2.py +0 -0
  70. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_sync_async.py +0 -0
  71. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/expiry_preflight.py +0 -0
  72. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/result_batch_benchmark_support.py +0 -0
  73. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/support.py +0 -0
  74. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_connection.py +0 -0
  75. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_pool.py +0 -0
  76. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_queries.py +0 -0
  77. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_resume.py +0 -0
  78. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_sqlalchemy.py +0 -0
  79. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_token_lifecycle.py +0 -0
  80. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_auth_boundary.py +0 -0
  81. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_oauth_continuation.py +0 -0
  82. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_readonly_matrix.py +0 -0
  83. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_result_batch_v2.py +0 -0
  84. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_shared_token_issuance.py +0 -0
  85. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  86. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/token_observation.py +0 -0
  87. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/move_tests.py +0 -0
  88. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/quick_test.py +0 -0
  89. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_38_nines.py +0 -0
  90. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_all_decimal128_cases.py +0 -0
  91. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_efficiency.py +0 -0
  92. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_none_strategy.py +0 -0
  93. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_oauth.py +0 -0
  94. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_strategy.py +0 -0
  95. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_code_optimizations.py +0 -0
  96. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_comprehensive.py +0 -0
  97. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool_e2e.py +0 -0
  98. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_current_implementation.py +0 -0
  99. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_binary_parsing.py +0 -0
  100. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_parsing.py +0 -0
  101. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_dialect_columns.py +0 -0
  102. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_fix.py +0 -0
  103. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_improved_parsing.py +0 -0
  104. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_known_case.py +0 -0
  105. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_manual_analysis.py +0 -0
  106. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_mock_server.py +0 -0
  107. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_multiprocessing_fix.py +0 -0
  108. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_new_implementation.py +0 -0
  109. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_oauth_authentication.py +0 -0
  110. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_oauth_auto_resume.py +0 -0
  111. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_oauth_resume_integration.py +0 -0
  112. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_pool_concurrency_simple.py +0 -0
  113. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_pool_threading_multiprocessing.py +0 -0
  114. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_simple_connection_pool.py +0 -0
  115. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_specific_binary.py +0 -0
  116. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy.py +0 -0
  117. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy_logic.py +0 -0
  118. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy_persistence_fix.py +0 -0
  119. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy_sharing_fix.py +0 -0
  120. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_user_binary_value.py +0 -0
  121. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/tests.py +0 -0
  122. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/tests_grpc.py +0 -0
  123. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/validate_decimal128.py +0 -0
  124. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/validate_implementation.py +0 -0
  125. {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/verify_decimal_fix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e6data-python-connector
3
- Version: 3.0.1rc1
3
+ Version: 3.0.1rc2
4
4
  Summary: Client for the e6data distributed SQL Engine.
5
5
  Home-page: https://github.com/e6x-labs/e6data-python-connector
6
6
  Author: e6data, Inc.
@@ -471,85 +471,119 @@ cursor = conn.cursor(database, catalog_name)
471
471
 
472
472
  ### Opt in to multi-chunk result batches
473
473
 
474
- `enable_result_batch_v2=True` lets a compatible planner return several result
475
- chunks in one `getNextResultBatchV2` response. The default is `False`, which keeps
476
- the existing V1 protocol. Increasing `arraysize` or `fetchmany(size)` changes how
477
- many rows your application receives per call; it does not change the planner's
478
- response size.
474
+ Set `enable_result_batch_v2=True` to enable V2 batches, continuous result downloading,
475
+ and bounded parallel chunk decoding together. It defaults to `False`. There
476
+ are no separate public prefetch or decoding switches. `arraysize` and
477
+ `fetchmany(size)` only change the number of rows returned to the application.
479
478
 
480
479
  The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
481
- `ENABLE_GET_NEXT_CHUNK_V2` controls the separate executor-to-planner boundary.
482
- Record both flags when qualifying a deployment. V2 can reduce network round
483
- trips, but it does not reduce the number of result bytes or extend the planner's
484
- query lifetime. Completion of a 20-million-row result within 900 seconds must be
485
- measured against the actual workload and deployment.
486
-
487
- The following examples use your existing `connection_options`, `sql`, and
488
- `consume` function. Choose a positive `result_receive_limit_bytes` from measured
489
- V2 response sizes and the client's memory budget. The sync API preserves its
490
- existing gRPC options, so supply a finite receive limit for a V2 rollout:
480
+ `ENABLE_GET_NEXT_CHUNK_V2` controls a separate executor-to-planner boundary.
481
+ These flags do not extend the planner's query lifetime. Finishing a large result
482
+ within 900 seconds still needs a measurement on the target deployment.
483
+
484
+ The optimized path requires ordinary GIL-enabled CPython 3.11 through 3.13 and
485
+ an import-safe application entry point. Put application startup inside
486
+ `if __name__ == "__main__":`, including when using SQLAlchemy. Interactive,
487
+ daemon and frozen applications are rejected before query submission. Two
488
+ workers use the explicit `spawn` method; the application's global multiprocessing
489
+ start method is unchanged. An unguarded main script can run its other import
490
+ side effects, so keep all application startup inside the guard.
491
+
492
+ Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
493
+ custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
494
+ flag-off path keeps its existing options. For async, set
495
+ `max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
496
+ Choose limits from measured envelope sizes and the client's memory budget.
497
+
498
+ These examples use your existing `connection_options`, `sql`, and `consume`:
491
499
 
492
500
  ```python
493
501
  from e6data_python_connector import Connection
494
502
 
495
- sync_options = {
496
- **connection_options,
497
- "enable_result_batch_v2": True,
498
- "grpc_options": {
499
- **connection_options.get("grpc_options", {}),
500
- "max_receive_message_length": result_receive_limit_bytes,
501
- },
502
- }
503
- with Connection(**sync_options) as connection:
504
- with connection.cursor() as cursor:
505
- cursor.execute(sql)
506
- for rows in cursor.fetchall_buffer():
507
- consume(rows)
508
- ```
509
503
 
510
- The async API keeps its finite 64 MiB default. Set
511
- `max_receive_message_bytes` to change that limit. If `grpc_options` also contains
512
- a receive limit, it must agree with this value.
504
+ def main():
505
+ options = {**connection_options, "enable_result_batch_v2": True}
506
+ with Connection(**options) as connection:
507
+ with connection.cursor() as cursor:
508
+ cursor.execute(sql)
509
+ for rows in cursor.fetchall_buffer():
510
+ consume(rows)
511
+
512
+
513
+ if __name__ == "__main__":
514
+ main()
515
+ ```
513
516
 
514
517
  ```python
518
+ import asyncio
515
519
  from e6data_python_connector.aio import AsyncConnection
516
520
 
517
- async_options = {
518
- **connection_options,
519
- "enable_result_batch_v2": True,
520
- "max_receive_message_bytes": result_receive_limit_bytes,
521
- }
522
- async with AsyncConnection(**async_options) as connection:
523
- async with connection.cursor() as cursor:
524
- await cursor.execute(sql)
525
- async for rows in cursor.fetchall_buffer():
526
- consume(rows)
527
- ```
528
521
 
529
- SQLAlchemy accepts the same opt-in through `connect_args`:
522
+ async def main():
523
+ options = {**connection_options, "enable_result_batch_v2": True}
524
+ async with AsyncConnection(**options) as connection:
525
+ async with connection.cursor() as cursor:
526
+ await cursor.execute(sql)
527
+ async for rows in cursor.fetchall_buffer():
528
+ consume(rows)
530
529
 
531
- ```python
532
- from sqlalchemy import create_engine
533
- from sqlalchemy.ext.asyncio import create_async_engine
534
530
 
535
- engine = create_engine("e6data://", connect_args=sync_options)
536
- async_engine = create_async_engine("e6data+asyncio://", connect_args=async_options)
531
+ if __name__ == "__main__":
532
+ asyncio.run(main())
537
533
  ```
538
534
 
539
- Fetch return shapes and row order stay the same. `fetchall_buffer()` yields
540
- decoded chunks; a V2 response can contain several chunks. All chunks in an
541
- envelope are decoded before any are exposed, so V2 may use more memory and delay
542
- the first chunk compared with V1. `fetchall()` still retains the full result.
543
- There is no prefetch or parallel fetching for a query.
535
+ SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
536
+ inside the guarded application entry point:
544
537
 
545
- An `UNIMPLEMENTED` response switches that query to V1. A later query may try V2
546
- again. Other fetch failures do not trigger a protocol fallback or query replay.
547
- To disable V2 for new connections, omit the option or set it to `False`.
538
+ ```python
539
+ connect_args = {**connection_options, "enable_result_batch_v2": True}
540
+ engine = create_engine("e6data://", connect_args=connect_args)
541
+ async_engine = create_async_engine("e6data+asyncio://", connect_args=connect_args)
542
+ ```
548
543
 
549
- The [result batch qualification instructions](test/README.md#result-batch-v2-qualification)
550
- cover protocol parity and the explicit large-result benchmark. Real-engine
551
- qualification and the 900-second acceptance measurement have not been run for
552
- this change.
544
+ After the first V2 response, a download thread (sync) or task (async) keeps
545
+ fetching serialized batches until the engine returns end-of-stream. Decoding
546
+ runs concurrently and takes batches from the queue in order. Downloading does
547
+ not wait for decoding or application row processing. There is no queue-size
548
+ limit and no process-wide limit on active downloaders. Result RPCs for the same
549
+ query never overlap.
550
+
551
+ Each background RPC gets a fresh configured transport budget when it starts;
552
+ that deadline never extends after dispatch. An empty nonterminal response uses
553
+ backoff with a finite no-progress deadline. Completed responses can wait while
554
+ the application is idle. Public fetch deadlines still cover waiting and decoding,
555
+ and expiry or cancellation stops the downloader. Async `fetchall` keeps one
556
+ deadline for its entire operation.
557
+
558
+ Two shared worker processes can decode one multi-chunk envelope at a time.
559
+ Other envelopes use the sequential path. Workers receive chunk bytes and column
560
+ positions, not connections or credentials. Process startup and copying add cost,
561
+ so parallel decoding is not a promise of better performance for every result.
562
+
563
+ Fetch return shapes and row order stay the same. `fetchall_buffer()` yields the
564
+ original decoded chunks. All chunks in an envelope must decode successfully
565
+ before any are exposed. The serialized queue can hold the entire result when
566
+ downloading is faster than decoding or application reads. Workers add Python
567
+ objects and process copies; the receive limit is not a limit on total memory.
568
+ `fetchall()` still retains the full result.
569
+
570
+ Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
571
+ optimizations. Other RPC failures reach the caller as the original
572
+ `grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
573
+ its envelope is needed, after current valid chunks have been consumed. Failed
574
+ or cancelled queries are not replayed. Later fetches on an incomplete result
575
+ raise `IncompleteResultError`. Clear, cancel, close and pool return retire
576
+ pending work; unsafe connections must not be reused.
577
+
578
+ Use `debug=True` for payload-free RPC, decode and worker diagnostics. Serialized
579
+ Protobuf bytes are uncompressed payload size, not bytes sent over the network.
580
+
581
+ For rollback, close old cursors and connections, then omit
582
+ `enable_result_batch_v2` or set it to `False` on new connections. Closing the
583
+ last V2 connection also stops its shared workers. See the
584
+ [qualification instructions](test/README.md#result-batch-v2-qualification).
585
+ Local synthetic results do not prove customer performance or the 900-second
586
+ query requirement.
553
587
 
554
588
  ### Get Query Time Metrics
555
589
  ```python
@@ -996,11 +1030,27 @@ conn = Connection(
996
1030
  ```
997
1031
 
998
1032
  When `debug=True`, the following features are enabled:
999
- - Python logging at DEBUG level for all operations
1033
+ - Connector logging at DEBUG level, for both sync and async connections. Existing
1034
+ application logging handlers and the root logger level are kept.
1000
1035
  - Blue-green strategy transition logging
1001
1036
  - Connection lifecycle logging
1002
1037
  - Query execution detailed logging
1003
1038
 
1039
+ Each result fetch writes a normal debug message with `protocol`, `rpc_seconds`,
1040
+ `decode_seconds`, `chunk_count`, `serialized_bytes`, and `status`. The timing values
1041
+ are seconds. `rpc_seconds` measures the client RPC call, including waiting for its
1042
+ response. `decode_seconds` measures client response processing. `serialized_bytes`
1043
+ is the protobuf response size, not the number of bytes on the network. A failed
1044
+ attempt reports zero response size and chunk count. The same values remain available
1045
+ as `result_batch_*` attributes for structured log handlers.
1046
+
1047
+ These metrics do not add SQL, credentials, session IDs, result values, or raw metadata
1048
+ to logs. `debug=False` keeps the normal quiet default. Applications can also enable
1049
+ DEBUG on the `e6data_python_connector` logger themselves. `debug=True` adds a connector
1050
+ stream handler only when no existing handler in its logger path accepts DEBUG.
1051
+ As with normal Python logging, enabling the package logger also affects other open
1052
+ connector connections in that process.
1053
+
1004
1054
  ### gRPC Network Tracing
1005
1055
 
1006
1056
  For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
@@ -425,85 +425,119 @@ cursor = conn.cursor(database, catalog_name)
425
425
 
426
426
  ### Opt in to multi-chunk result batches
427
427
 
428
- `enable_result_batch_v2=True` lets a compatible planner return several result
429
- chunks in one `getNextResultBatchV2` response. The default is `False`, which keeps
430
- the existing V1 protocol. Increasing `arraysize` or `fetchmany(size)` changes how
431
- many rows your application receives per call; it does not change the planner's
432
- response size.
428
+ Set `enable_result_batch_v2=True` to enable V2 batches, continuous result downloading,
429
+ and bounded parallel chunk decoding together. It defaults to `False`. There
430
+ are no separate public prefetch or decoding switches. `arraysize` and
431
+ `fetchmany(size)` only change the number of rows returned to the application.
433
432
 
434
433
  The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
435
- `ENABLE_GET_NEXT_CHUNK_V2` controls the separate executor-to-planner boundary.
436
- Record both flags when qualifying a deployment. V2 can reduce network round
437
- trips, but it does not reduce the number of result bytes or extend the planner's
438
- query lifetime. Completion of a 20-million-row result within 900 seconds must be
439
- measured against the actual workload and deployment.
440
-
441
- The following examples use your existing `connection_options`, `sql`, and
442
- `consume` function. Choose a positive `result_receive_limit_bytes` from measured
443
- V2 response sizes and the client's memory budget. The sync API preserves its
444
- existing gRPC options, so supply a finite receive limit for a V2 rollout:
434
+ `ENABLE_GET_NEXT_CHUNK_V2` controls a separate executor-to-planner boundary.
435
+ These flags do not extend the planner's query lifetime. Finishing a large result
436
+ within 900 seconds still needs a measurement on the target deployment.
437
+
438
+ The optimized path requires ordinary GIL-enabled CPython 3.11 through 3.13 and
439
+ an import-safe application entry point. Put application startup inside
440
+ `if __name__ == "__main__":`, including when using SQLAlchemy. Interactive,
441
+ daemon and frozen applications are rejected before query submission. Two
442
+ workers use the explicit `spawn` method; the application's global multiprocessing
443
+ start method is unchanged. An unguarded main script can run its other import
444
+ side effects, so keep all application startup inside the guard.
445
+
446
+ Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
447
+ custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
448
+ flag-off path keeps its existing options. For async, set
449
+ `max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
450
+ Choose limits from measured envelope sizes and the client's memory budget.
451
+
452
+ These examples use your existing `connection_options`, `sql`, and `consume`:
445
453
 
446
454
  ```python
447
455
  from e6data_python_connector import Connection
448
456
 
449
- sync_options = {
450
- **connection_options,
451
- "enable_result_batch_v2": True,
452
- "grpc_options": {
453
- **connection_options.get("grpc_options", {}),
454
- "max_receive_message_length": result_receive_limit_bytes,
455
- },
456
- }
457
- with Connection(**sync_options) as connection:
458
- with connection.cursor() as cursor:
459
- cursor.execute(sql)
460
- for rows in cursor.fetchall_buffer():
461
- consume(rows)
462
- ```
463
457
 
464
- The async API keeps its finite 64 MiB default. Set
465
- `max_receive_message_bytes` to change that limit. If `grpc_options` also contains
466
- a receive limit, it must agree with this value.
458
+ def main():
459
+ options = {**connection_options, "enable_result_batch_v2": True}
460
+ with Connection(**options) as connection:
461
+ with connection.cursor() as cursor:
462
+ cursor.execute(sql)
463
+ for rows in cursor.fetchall_buffer():
464
+ consume(rows)
465
+
466
+
467
+ if __name__ == "__main__":
468
+ main()
469
+ ```
467
470
 
468
471
  ```python
472
+ import asyncio
469
473
  from e6data_python_connector.aio import AsyncConnection
470
474
 
471
- async_options = {
472
- **connection_options,
473
- "enable_result_batch_v2": True,
474
- "max_receive_message_bytes": result_receive_limit_bytes,
475
- }
476
- async with AsyncConnection(**async_options) as connection:
477
- async with connection.cursor() as cursor:
478
- await cursor.execute(sql)
479
- async for rows in cursor.fetchall_buffer():
480
- consume(rows)
481
- ```
482
475
 
483
- SQLAlchemy accepts the same opt-in through `connect_args`:
476
+ async def main():
477
+ options = {**connection_options, "enable_result_batch_v2": True}
478
+ async with AsyncConnection(**options) as connection:
479
+ async with connection.cursor() as cursor:
480
+ await cursor.execute(sql)
481
+ async for rows in cursor.fetchall_buffer():
482
+ consume(rows)
484
483
 
485
- ```python
486
- from sqlalchemy import create_engine
487
- from sqlalchemy.ext.asyncio import create_async_engine
488
484
 
489
- engine = create_engine("e6data://", connect_args=sync_options)
490
- async_engine = create_async_engine("e6data+asyncio://", connect_args=async_options)
485
+ if __name__ == "__main__":
486
+ asyncio.run(main())
491
487
  ```
492
488
 
493
- Fetch return shapes and row order stay the same. `fetchall_buffer()` yields
494
- decoded chunks; a V2 response can contain several chunks. All chunks in an
495
- envelope are decoded before any are exposed, so V2 may use more memory and delay
496
- the first chunk compared with V1. `fetchall()` still retains the full result.
497
- There is no prefetch or parallel fetching for a query.
489
+ SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
490
+ inside the guarded application entry point:
498
491
 
499
- An `UNIMPLEMENTED` response switches that query to V1. A later query may try V2
500
- again. Other fetch failures do not trigger a protocol fallback or query replay.
501
- To disable V2 for new connections, omit the option or set it to `False`.
492
+ ```python
493
+ connect_args = {**connection_options, "enable_result_batch_v2": True}
494
+ engine = create_engine("e6data://", connect_args=connect_args)
495
+ async_engine = create_async_engine("e6data+asyncio://", connect_args=connect_args)
496
+ ```
502
497
 
503
- The [result batch qualification instructions](test/README.md#result-batch-v2-qualification)
504
- cover protocol parity and the explicit large-result benchmark. Real-engine
505
- qualification and the 900-second acceptance measurement have not been run for
506
- this change.
498
+ After the first V2 response, a download thread (sync) or task (async) keeps
499
+ fetching serialized batches until the engine returns end-of-stream. Decoding
500
+ runs concurrently and takes batches from the queue in order. Downloading does
501
+ not wait for decoding or application row processing. There is no queue-size
502
+ limit and no process-wide limit on active downloaders. Result RPCs for the same
503
+ query never overlap.
504
+
505
+ Each background RPC gets a fresh configured transport budget when it starts;
506
+ that deadline never extends after dispatch. An empty nonterminal response uses
507
+ backoff with a finite no-progress deadline. Completed responses can wait while
508
+ the application is idle. Public fetch deadlines still cover waiting and decoding,
509
+ and expiry or cancellation stops the downloader. Async `fetchall` keeps one
510
+ deadline for its entire operation.
511
+
512
+ Two shared worker processes can decode one multi-chunk envelope at a time.
513
+ Other envelopes use the sequential path. Workers receive chunk bytes and column
514
+ positions, not connections or credentials. Process startup and copying add cost,
515
+ so parallel decoding is not a promise of better performance for every result.
516
+
517
+ Fetch return shapes and row order stay the same. `fetchall_buffer()` yields the
518
+ original decoded chunks. All chunks in an envelope must decode successfully
519
+ before any are exposed. The serialized queue can hold the entire result when
520
+ downloading is faster than decoding or application reads. Workers add Python
521
+ objects and process copies; the receive limit is not a limit on total memory.
522
+ `fetchall()` still retains the full result.
523
+
524
+ Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
525
+ optimizations. Other RPC failures reach the caller as the original
526
+ `grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
527
+ its envelope is needed, after current valid chunks have been consumed. Failed
528
+ or cancelled queries are not replayed. Later fetches on an incomplete result
529
+ raise `IncompleteResultError`. Clear, cancel, close and pool return retire
530
+ pending work; unsafe connections must not be reused.
531
+
532
+ Use `debug=True` for payload-free RPC, decode and worker diagnostics. Serialized
533
+ Protobuf bytes are uncompressed payload size, not bytes sent over the network.
534
+
535
+ For rollback, close old cursors and connections, then omit
536
+ `enable_result_batch_v2` or set it to `False` on new connections. Closing the
537
+ last V2 connection also stops its shared workers. See the
538
+ [qualification instructions](test/README.md#result-batch-v2-qualification).
539
+ Local synthetic results do not prove customer performance or the 900-second
540
+ query requirement.
507
541
 
508
542
  ### Get Query Time Metrics
509
543
  ```python
@@ -950,11 +984,27 @@ conn = Connection(
950
984
  ```
951
985
 
952
986
  When `debug=True`, the following features are enabled:
953
- - Python logging at DEBUG level for all operations
987
+ - Connector logging at DEBUG level, for both sync and async connections. Existing
988
+ application logging handlers and the root logger level are kept.
954
989
  - Blue-green strategy transition logging
955
990
  - Connection lifecycle logging
956
991
  - Query execution detailed logging
957
992
 
993
+ Each result fetch writes a normal debug message with `protocol`, `rpc_seconds`,
994
+ `decode_seconds`, `chunk_count`, `serialized_bytes`, and `status`. The timing values
995
+ are seconds. `rpc_seconds` measures the client RPC call, including waiting for its
996
+ response. `decode_seconds` measures client response processing. `serialized_bytes`
997
+ is the protobuf response size, not the number of bytes on the network. A failed
998
+ attempt reports zero response size and chunk count. The same values remain available
999
+ as `result_batch_*` attributes for structured log handlers.
1000
+
1001
+ These metrics do not add SQL, credentials, session IDs, result values, or raw metadata
1002
+ to logs. `debug=False` keeps the normal quiet default. Applications can also enable
1003
+ DEBUG on the `e6data_python_connector` logger themselves. `debug=True` adds a connector
1004
+ stream handler only when no existing handler in its logger path accepts DEBUG.
1005
+ As with normal Python logging, enabling the package logger also affects other open
1006
+ connector connections in that process.
1007
+
958
1008
  ### gRPC Network Tracing
959
1009
 
960
1010
  For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
@@ -52,6 +52,9 @@ class AsyncConnection:
52
52
  oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
53
53
  max_receive_message_bytes=64 * 1024 * 1024,
54
54
  enable_result_batch_v2=False):
55
+ from .result_decode_worker import is_decode_worker
56
+ if is_decode_worker():
57
+ raise RuntimeError('A result decode worker cannot create a connection.')
55
58
  if sys.version_info < (3, 11):
56
59
  raise RuntimeError('The async API requires Python 3.11 or newer.')
57
60
  try:
@@ -81,6 +84,9 @@ class AsyncConnection:
81
84
  raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
82
85
  if not isinstance(enable_result_batch_v2, bool):
83
86
  raise ValueError('enable_result_batch_v2 must be a boolean.')
87
+ if enable_result_batch_v2:
88
+ from .result_decode import validate_decode_runtime
89
+ validate_decode_runtime()
84
90
  settings = dict(host=host, port=port, username=username, password=password,
85
91
  catalog=catalog, database=database, cluster_name=cluster_name,
86
92
  secure=bool(secure), ssl_cert=ssl_cert, auto_resume=auto_resume,
@@ -107,6 +113,9 @@ class AsyncConnection:
107
113
  settings['grpc_options'] = MappingProxyType(options)
108
114
  settings['max_receive_message_bytes'] = max_receive_message_bytes
109
115
  self._config = MappingProxyType(settings)
116
+ if debug:
117
+ from .e6data_grpc import _configure_debug_logging
118
+ _configure_debug_logging()
110
119
  self._oauth_enabled = not modes[0]
111
120
  self._channel = self._client = self._token_provider = None
112
121
  self._owns_token_provider = True
@@ -129,6 +138,11 @@ class AsyncConnection:
129
138
  self._cleanup_error = None
130
139
  self._cluster_manager = None
131
140
  self._owns_cluster_manager = True
141
+ self._decoder_started = False
142
+ self._decoder_lease = None
143
+ if enable_result_batch_v2:
144
+ from .result_decode import DecoderLease
145
+ self._decoder_lease = DecoderLease()
132
146
 
133
147
  def __getattr__(self, name):
134
148
  config = self.__dict__.get('_config', {})
@@ -304,6 +318,24 @@ class AsyncConnection:
304
318
  self._check_owner(_cleanup=_cleanup, _internal=True)
305
319
  return metadata
306
320
 
321
+ async def _ensure_decoder(self, deadline):
322
+ self._check_owner()
323
+ if self._decoder_lease is not None and not self._decoder_started:
324
+ from .async_work import run_blocking
325
+ await run_blocking(self._decoder_lease.start, deadline, deadline=deadline)
326
+ self._check_owner()
327
+ self._decoder_started = True
328
+
329
+ async def _raw_result_rpc(self, method_name, request, metadata, deadline):
330
+ """Transport only: the owning task already froze identity and metadata."""
331
+ call = getattr(self._client, method_name)(
332
+ request, metadata=metadata, timeout=self._remaining(deadline))
333
+ self._calls.add(call)
334
+ try:
335
+ return await call
336
+ finally:
337
+ self._calls.discard(call)
338
+
307
339
  async def _rpc(self, method_name, request, *, deadline, route=None, safe_retry=False, _cleanup=False, _on_dispatch=None, _response_metadata=None):
308
340
  self._check_owner(_cleanup=_cleanup, _internal=True)
309
341
  refreshed = switched = False
@@ -504,6 +536,15 @@ class AsyncConnection:
504
536
  # Native channel close with no grace cancels transports without waiting.
505
537
  if self._channel is not None:
506
538
  await self._channel.close()
539
+ if self._decoder_lease is not None:
540
+ # Retirement must survive loop shutdown and saturated work slots.
541
+ self._decoder_lease.retire(deadline)
542
+ try:
543
+ async with asyncio.timeout_at(deadline):
544
+ while self._decoder_lease.cleanup_pending:
545
+ await asyncio.sleep(min(0.005, self._remaining(deadline)))
546
+ except (Exception, asyncio.CancelledError):
547
+ self._cleanup_error = OperationalError('Result decoder cleanup remains pending.')
507
548
  for resource, owned in ((self._cluster_manager, self._owns_cluster_manager),
508
549
  (self._token_provider, self._owns_token_provider)):
509
550
  if owned and resource is not None:
@@ -542,6 +583,12 @@ class AsyncConnection:
542
583
  if self._owner is not None and self._owner != (os.getpid(), threading.get_ident(), asyncio.get_running_loop()):
543
584
  raise ProgrammingError('Connection belongs to another owner.')
544
585
  await self.close()
586
+ if self._decoder_lease is not None and self._decoder_lease.cleanup_pending:
587
+ raise ProgrammingError('Cannot reopen while result decoder cleanup remains pending.')
588
+ if self.enable_result_batch_v2:
589
+ from .result_decode import DecoderLease
590
+ self._decoder_lease = DecoderLease()
591
+ self._decoder_started = False
545
592
  self._state = 'new'
546
593
  self._session_id = self._session_task = self._close_task = self._open_task = None
547
594
  if self._owns_token_provider:
@@ -316,6 +316,8 @@ class AsyncConnectionPool:
316
316
  healthy = healthy and cursor._state != 'SUBMISSION_UNKNOWN'
317
317
  await cursor._close_owned(deadline)
318
318
  healthy = healthy and cursor.cleanup_error is None
319
+ healthy = (healthy and cursor._pending_result is None
320
+ and not cursor._retired_results)
319
321
  healthy = (healthy and not connection._routes and not connection._ambiguous_submissions
320
322
  and connection._state == 'open')
321
323
  except (Exception, asyncio.CancelledError):