e6data-python-connector 3.0.0__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 (126) hide show
  1. {e6data_python_connector-3.0.0/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc2}/PKG-INFO +134 -2
  2. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/README.md +133 -1
  3. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection.py +53 -2
  4. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection_pool.py +2 -0
  5. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cursor.py +379 -19
  6. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_dialect.py +2 -1
  7. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_manager.py +3 -1
  8. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/connection_pool.py +11 -9
  9. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/dialect.py +7 -2
  10. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6data_grpc.py +715 -108
  11. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_batch.py +49 -0
  12. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode.py +469 -0
  13. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode_worker.py +50 -0
  14. e6data_python_connector-3.0.1rc2/e6data_python_connector/result_prefetch.py +421 -0
  15. e6data_python_connector-3.0.1rc2/e6data_python_connector/server/e6x_engine_pb2.py +209 -0
  16. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +43 -0
  17. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2/e6data_python_connector.egg-info}/PKG-INFO +134 -2
  18. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/SOURCES.txt +7 -0
  19. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/setup.py +1 -1
  20. e6data_python_connector-3.0.1rc2/test/integration/benchmark_result_batch_v2.py +149 -0
  21. e6data_python_connector-3.0.1rc2/test/integration/result_batch_benchmark_support.py +196 -0
  22. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/support.py +3 -0
  23. e6data_python_connector-3.0.1rc2/test/integration/test_result_batch_v2.py +120 -0
  24. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool.py +3 -0
  25. e6data_python_connector-3.0.0/e6data_python_connector/server/e6x_engine_pb2.py +0 -207
  26. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/LICENSE +0 -0
  27. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/MANIFEST.in +0 -0
  28. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/__init__.py +0 -0
  29. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/aio.py +0 -0
  30. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cluster_manager.py +0 -0
  31. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_oauth.py +0 -0
  32. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_work.py +0 -0
  33. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/__init__.py +0 -0
  34. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  35. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  36. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/common.py +0 -0
  37. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/constants.py +0 -0
  38. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/datainputstream.py +0 -0
  39. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/date_time_utils.py +0 -0
  40. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  41. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/constants.py +0 -0
  42. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  43. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/exceptions.py +0 -0
  44. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth.py +0 -0
  45. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_cache.py +0 -0
  46. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_common.py +0 -0
  47. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/QueryEngineService.py +0 -0
  48. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/__init__.py +0 -0
  49. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/constants.py +0 -0
  50. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  51. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/ttypes.py +0 -0
  52. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/strategy.py +0 -0
  53. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/typeId.py +0 -0
  54. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  55. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  56. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/requires.txt +0 -0
  57. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/top_level.txt +0 -0
  58. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/pyproject.toml +0 -0
  59. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/setup.cfg +0 -0
  60. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/__init__.py +0 -0
  61. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_38_nines.py +0 -0
  62. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_all_cases.py +0 -0
  63. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_binary.py +0 -0
  64. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_correct_value.py +0 -0
  65. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_fields.py +0 -0
  66. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/check_decimal_errors.py +0 -0
  67. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/cleanup_test_files.py +0 -0
  68. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/conftest.py +0 -0
  69. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/debug_38_nines.py +0 -0
  70. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/debug_binary.py +0 -0
  71. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/final_test.py +0 -0
  72. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/__init__.py +0 -0
  73. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_preflight.py +0 -0
  74. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_sync_async.py +0 -0
  75. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/expiry_preflight.py +0 -0
  76. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_connection.py +0 -0
  77. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_pool.py +0 -0
  78. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_queries.py +0 -0
  79. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_resume.py +0 -0
  80. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_sqlalchemy.py +0 -0
  81. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_token_lifecycle.py +0 -0
  82. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_auth_boundary.py +0 -0
  83. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_oauth_continuation.py +0 -0
  84. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_readonly_matrix.py +0 -0
  85. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_shared_token_issuance.py +0 -0
  86. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  87. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/token_observation.py +0 -0
  88. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/move_tests.py +0 -0
  89. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/quick_test.py +0 -0
  90. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_38_nines.py +0 -0
  91. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_all_decimal128_cases.py +0 -0
  92. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_efficiency.py +0 -0
  93. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_none_strategy.py +0 -0
  94. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_oauth.py +0 -0
  95. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_strategy.py +0 -0
  96. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_code_optimizations.py +0 -0
  97. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_comprehensive.py +0 -0
  98. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool_e2e.py +0 -0
  99. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_current_implementation.py +0 -0
  100. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_binary_parsing.py +0 -0
  101. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_parsing.py +0 -0
  102. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_dialect_columns.py +0 -0
  103. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_fix.py +0 -0
  104. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_improved_parsing.py +0 -0
  105. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_known_case.py +0 -0
  106. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_manual_analysis.py +0 -0
  107. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_mock_server.py +0 -0
  108. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_multiprocessing_fix.py +0 -0
  109. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_new_implementation.py +0 -0
  110. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_oauth_authentication.py +0 -0
  111. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_oauth_auto_resume.py +0 -0
  112. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_oauth_resume_integration.py +0 -0
  113. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_pool_concurrency_simple.py +0 -0
  114. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_pool_threading_multiprocessing.py +0 -0
  115. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_simple_connection_pool.py +0 -0
  116. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_specific_binary.py +0 -0
  117. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy.py +0 -0
  118. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy_logic.py +0 -0
  119. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy_persistence_fix.py +0 -0
  120. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy_sharing_fix.py +0 -0
  121. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_user_binary_value.py +0 -0
  122. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/tests.py +0 -0
  123. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/tests_grpc.py +0 -0
  124. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/validate_decimal128.py +0 -0
  125. {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/validate_implementation.py +0 -0
  126. {e6data_python_connector-3.0.0 → 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.0
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.
@@ -469,6 +469,122 @@ database = '<new_database_name>' # Replace with the new database.
469
469
  cursor = conn.cursor(database, catalog_name)
470
470
  ```
471
471
 
472
+ ### Opt in to multi-chunk result batches
473
+
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.
478
+
479
+ The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
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`:
499
+
500
+ ```python
501
+ from e6data_python_connector import Connection
502
+
503
+
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
+ ```
516
+
517
+ ```python
518
+ import asyncio
519
+ from e6data_python_connector.aio import AsyncConnection
520
+
521
+
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)
529
+
530
+
531
+ if __name__ == "__main__":
532
+ asyncio.run(main())
533
+ ```
534
+
535
+ SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
536
+ inside the guarded application entry point:
537
+
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
+ ```
543
+
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.
587
+
472
588
  ### Get Query Time Metrics
473
589
  ```python
474
590
  import json
@@ -914,11 +1030,27 @@ conn = Connection(
914
1030
  ```
915
1031
 
916
1032
  When `debug=True`, the following features are enabled:
917
- - 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.
918
1035
  - Blue-green strategy transition logging
919
1036
  - Connection lifecycle logging
920
1037
  - Query execution detailed logging
921
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
+
922
1054
  ### gRPC Network Tracing
923
1055
 
924
1056
  For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
@@ -423,6 +423,122 @@ database = '<new_database_name>' # Replace with the new database.
423
423
  cursor = conn.cursor(database, catalog_name)
424
424
  ```
425
425
 
426
+ ### Opt in to multi-chunk result batches
427
+
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.
432
+
433
+ The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
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`:
453
+
454
+ ```python
455
+ from e6data_python_connector import Connection
456
+
457
+
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
+ ```
470
+
471
+ ```python
472
+ import asyncio
473
+ from e6data_python_connector.aio import AsyncConnection
474
+
475
+
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)
483
+
484
+
485
+ if __name__ == "__main__":
486
+ asyncio.run(main())
487
+ ```
488
+
489
+ SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
490
+ inside the guarded application entry point:
491
+
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
+ ```
497
+
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.
541
+
426
542
  ### Get Query Time Metrics
427
543
  ```python
428
544
  import json
@@ -868,11 +984,27 @@ conn = Connection(
868
984
  ```
869
985
 
870
986
  When `debug=True`, the following features are enabled:
871
- - 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.
872
989
  - Blue-green strategy transition logging
873
990
  - Connection lifecycle logging
874
991
  - Query execution detailed logging
875
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
+
876
1008
  ### gRPC Network Tracing
877
1009
 
878
1010
  For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
@@ -50,7 +50,11 @@ class AsyncConnection:
50
50
  token_url=None, oauth_scope=None, access_token=None,
51
51
  client_auth_method='basic', *, operation_timeout=600.0,
52
52
  oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
53
- max_receive_message_bytes=64 * 1024 * 1024):
53
+ max_receive_message_bytes=64 * 1024 * 1024,
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.')
54
58
  if sys.version_info < (3, 11):
55
59
  raise RuntimeError('The async API requires Python 3.11 or newer.')
56
60
  try:
@@ -78,13 +82,19 @@ class AsyncConnection:
78
82
  raise ValueError('Async OAuth requires verified TLS (secure=True).')
79
83
  if ssl_cert is not None and not isinstance(ssl_cert, (str, bytes)):
80
84
  raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
85
+ if not isinstance(enable_result_batch_v2, bool):
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()
81
90
  settings = dict(host=host, port=port, username=username, password=password,
82
91
  catalog=catalog, database=database, cluster_name=cluster_name,
83
92
  secure=bool(secure), ssl_cert=ssl_cert, auto_resume=auto_resume,
84
93
  scheme=scheme, debug=debug, require_fastbinary=require_fastbinary,
85
94
  client_id=client_id, client_secret=client_secret, token_url=token_url,
86
95
  oauth_scope=oauth_scope, access_token=access_token,
87
- client_auth_method=client_auth_method)
96
+ client_auth_method=client_auth_method,
97
+ enable_result_batch_v2=enable_result_batch_v2)
88
98
  for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
89
99
  ('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
90
100
  settings[name] = validate_positive_timeout(value, name)
@@ -103,6 +113,9 @@ class AsyncConnection:
103
113
  settings['grpc_options'] = MappingProxyType(options)
104
114
  settings['max_receive_message_bytes'] = max_receive_message_bytes
105
115
  self._config = MappingProxyType(settings)
116
+ if debug:
117
+ from .e6data_grpc import _configure_debug_logging
118
+ _configure_debug_logging()
106
119
  self._oauth_enabled = not modes[0]
107
120
  self._channel = self._client = self._token_provider = None
108
121
  self._owns_token_provider = True
@@ -125,6 +138,11 @@ class AsyncConnection:
125
138
  self._cleanup_error = None
126
139
  self._cluster_manager = None
127
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()
128
146
 
129
147
  def __getattr__(self, name):
130
148
  config = self.__dict__.get('_config', {})
@@ -300,6 +318,24 @@ class AsyncConnection:
300
318
  self._check_owner(_cleanup=_cleanup, _internal=True)
301
319
  return metadata
302
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
+
303
339
  async def _rpc(self, method_name, request, *, deadline, route=None, safe_retry=False, _cleanup=False, _on_dispatch=None, _response_metadata=None):
304
340
  self._check_owner(_cleanup=_cleanup, _internal=True)
305
341
  refreshed = switched = False
@@ -500,6 +536,15 @@ class AsyncConnection:
500
536
  # Native channel close with no grace cancels transports without waiting.
501
537
  if self._channel is not None:
502
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.')
503
548
  for resource, owned in ((self._cluster_manager, self._owns_cluster_manager),
504
549
  (self._token_provider, self._owns_token_provider)):
505
550
  if owned and resource is not None:
@@ -538,6 +583,12 @@ class AsyncConnection:
538
583
  if self._owner is not None and self._owner != (os.getpid(), threading.get_ident(), asyncio.get_running_loop()):
539
584
  raise ProgrammingError('Connection belongs to another owner.')
540
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
541
592
  self._state = 'new'
542
593
  self._session_id = self._session_task = self._close_task = self._open_task = None
543
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):