e6data-python-connector 3.0.1rc3__tar.gz → 3.0.2__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.1rc3/e6data_python_connector.egg-info → e6data_python_connector-3.0.2}/PKG-INFO +19 -6
  2. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/README.md +18 -5
  3. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_cluster_manager.py +9 -4
  4. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_connection.py +30 -8
  5. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_connection_pool.py +2 -0
  6. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_cursor.py +3 -2
  7. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/cluster_manager.py +4 -3
  8. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/datainputstream.py +11 -0
  9. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/e6data_grpc.py +18 -13
  10. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/e6x_vector/ttypes.py +3 -0
  11. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/result_decode.py +155 -65
  12. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2/e6data_python_connector.egg-info}/PKG-INFO +19 -6
  13. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/setup.py +1 -1
  14. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_oauth_auto_resume.py +7 -2
  15. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/LICENSE +0 -0
  16. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/MANIFEST.in +0 -0
  17. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/__init__.py +0 -0
  18. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/aio.py +0 -0
  19. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_dialect.py +0 -0
  20. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_oauth.py +0 -0
  21. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/async_work.py +0 -0
  22. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/cluster_server/__init__.py +0 -0
  23. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  24. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  25. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/common.py +0 -0
  26. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/connection_pool.py +0 -0
  27. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/constants.py +0 -0
  28. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/date_time_utils.py +0 -0
  29. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/dialect.py +0 -0
  30. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  31. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/e6x_vector/constants.py +0 -0
  32. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/exceptions.py +0 -0
  33. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/oauth.py +0 -0
  34. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/oauth_cache.py +0 -0
  35. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/oauth_common.py +0 -0
  36. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/result_batch.py +0 -0
  37. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/result_decode_worker.py +0 -0
  38. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/result_prefetch.py +0 -0
  39. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/QueryEngineService.py +0 -0
  40. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/__init__.py +0 -0
  41. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/constants.py +0 -0
  42. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  43. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  44. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  45. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/server/ttypes.py +0 -0
  46. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/strategy.py +0 -0
  47. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector/typeId.py +0 -0
  48. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
  49. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  50. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  51. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector.egg-info/requires.txt +0 -0
  52. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/e6data_python_connector.egg-info/top_level.txt +0 -0
  53. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/pyproject.toml +0 -0
  54. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/setup.cfg +0 -0
  55. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/__init__.py +0 -0
  56. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/analyze_38_nines.py +0 -0
  57. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/analyze_all_cases.py +0 -0
  58. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/analyze_binary.py +0 -0
  59. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/analyze_correct_value.py +0 -0
  60. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/analyze_fields.py +0 -0
  61. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/check_decimal_errors.py +0 -0
  62. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/cleanup_test_files.py +0 -0
  63. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/conftest.py +0 -0
  64. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/debug_38_nines.py +0 -0
  65. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/debug_binary.py +0 -0
  66. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/final_test.py +0 -0
  67. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/__init__.py +0 -0
  68. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/benchmark_preflight.py +0 -0
  69. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/benchmark_result_batch_v2.py +0 -0
  70. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/benchmark_sync_async.py +0 -0
  71. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/expiry_preflight.py +0 -0
  72. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/result_batch_benchmark_support.py +0 -0
  73. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/support.py +0 -0
  74. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_async_connection.py +0 -0
  75. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_async_pool.py +0 -0
  76. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_async_queries.py +0 -0
  77. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_async_resume.py +0 -0
  78. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_async_sqlalchemy.py +0 -0
  79. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_async_token_lifecycle.py +0 -0
  80. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_e2e_auth_boundary.py +0 -0
  81. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_e2e_oauth_continuation.py +0 -0
  82. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_e2e_readonly_matrix.py +0 -0
  83. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_result_batch_v2.py +0 -0
  84. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_shared_token_issuance.py +0 -0
  85. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  86. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/integration/token_observation.py +0 -0
  87. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/move_tests.py +0 -0
  88. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/quick_test.py +0 -0
  89. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_38_nines.py +0 -0
  90. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_all_decimal128_cases.py +0 -0
  91. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_cluster_manager_efficiency.py +0 -0
  92. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_cluster_manager_none_strategy.py +0 -0
  93. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_cluster_manager_oauth.py +0 -0
  94. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_cluster_manager_strategy.py +0 -0
  95. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_code_optimizations.py +0 -0
  96. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_comprehensive.py +0 -0
  97. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_connection_pool.py +0 -0
  98. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_connection_pool_e2e.py +0 -0
  99. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_current_implementation.py +0 -0
  100. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_decimal128_binary_parsing.py +0 -0
  101. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_decimal128_parsing.py +0 -0
  102. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_dialect_columns.py +0 -0
  103. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_fix.py +0 -0
  104. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_improved_parsing.py +0 -0
  105. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_known_case.py +0 -0
  106. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_manual_analysis.py +0 -0
  107. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_mock_server.py +0 -0
  108. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_multiprocessing_fix.py +0 -0
  109. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_new_implementation.py +0 -0
  110. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_oauth_authentication.py +0 -0
  111. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_oauth_resume_integration.py +0 -0
  112. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_pool_concurrency_simple.py +0 -0
  113. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_pool_threading_multiprocessing.py +0 -0
  114. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_simple_connection_pool.py +0 -0
  115. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_specific_binary.py +0 -0
  116. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_strategy.py +0 -0
  117. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_strategy_logic.py +0 -0
  118. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_strategy_persistence_fix.py +0 -0
  119. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_strategy_sharing_fix.py +0 -0
  120. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/test_user_binary_value.py +0 -0
  121. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/tests.py +0 -0
  122. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/tests_grpc.py +0 -0
  123. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/validate_decimal128.py +0 -0
  124. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/test/validate_implementation.py +0 -0
  125. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.2}/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.1rc3
3
+ Version: 3.0.2
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.
@@ -489,11 +489,15 @@ workers use the explicit `spawn` method; the application's global multiprocessin
489
489
  start method is unchanged. An unguarded main script can run its other import
490
490
  side effects, so keep all application startup inside the guard.
491
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.
492
+ Both APIs default to unlimited gRPC send and receive message sizes (`-1`).
493
+ Explicit positive byte limits are still honored. For sync, set
494
+ `max_receive_message_length` and `max_send_message_length` in `grpc_options`.
495
+ For async, set the receive limit with `max_receive_message_bytes` or
496
+ `max_receive_message_length` in `grpc_options`; if both are supplied, they must
497
+ agree. Set `max_send_message_length` in `grpc_options` for the send limit.
498
+ The receive limit applies to each message after transport decompression, not the
499
+ total query result. Unlimited receiving does not limit client memory use;
500
+ continuous downloading can retain serialized batches while decoding catches up.
497
501
 
498
502
  These examples use your existing `connection_options`, `sql`, and `consume`:
499
503
 
@@ -567,6 +571,15 @@ downloading is faster than decoding or application reads. Workers add Python
567
571
  objects and process copies; the receive limit is not a limit on total memory.
568
572
  `fetchall()` still retains the full result.
569
573
 
574
+ If a decoder worker exits or its local pipe breaks, the connector stops the
575
+ failed workers and decodes that complete serialized envelope once in the parent
576
+ process. It uses the same deadline and makes no extra result RPC. Continuous
577
+ downloading stays active and uncapped during this recovery. Later envelopes use
578
+ sequential decoding until the decoder runtime is replaced through its normal
579
+ lifecycle. Cancellation, expired deadlines, invalid worker replies, and errors
580
+ reported while decoding data are still failures. Their original cause is kept
581
+ on the first error delivered to the caller.
582
+
570
583
  Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
571
584
  optimizations. Other RPC failures reach the caller as the original
572
585
  `grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
@@ -443,11 +443,15 @@ workers use the explicit `spawn` method; the application's global multiprocessin
443
443
  start method is unchanged. An unguarded main script can run its other import
444
444
  side effects, so keep all application startup inside the guard.
445
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.
446
+ Both APIs default to unlimited gRPC send and receive message sizes (`-1`).
447
+ Explicit positive byte limits are still honored. For sync, set
448
+ `max_receive_message_length` and `max_send_message_length` in `grpc_options`.
449
+ For async, set the receive limit with `max_receive_message_bytes` or
450
+ `max_receive_message_length` in `grpc_options`; if both are supplied, they must
451
+ agree. Set `max_send_message_length` in `grpc_options` for the send limit.
452
+ The receive limit applies to each message after transport decompression, not the
453
+ total query result. Unlimited receiving does not limit client memory use;
454
+ continuous downloading can retain serialized batches while decoding catches up.
451
455
 
452
456
  These examples use your existing `connection_options`, `sql`, and `consume`:
453
457
 
@@ -521,6 +525,15 @@ downloading is faster than decoding or application reads. Workers add Python
521
525
  objects and process copies; the receive limit is not a limit on total memory.
522
526
  `fetchall()` still retains the full result.
523
527
 
528
+ If a decoder worker exits or its local pipe breaks, the connector stops the
529
+ failed workers and decodes that complete serialized envelope once in the parent
530
+ process. It uses the same deadline and makes no extra result RPC. Continuous
531
+ downloading stays active and uncapped during this recovery. Later envelopes use
532
+ sequential decoding until the decoder runtime is replaced through its normal
533
+ lifecycle. Cancellation, expired deadlines, invalid worker replies, and errors
534
+ reported while decoding data are still failures. Their original cause is kept
535
+ on the first error delivered to the caller.
536
+
524
537
  Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
525
538
  optimizations. Other RPC failures reach the caller as the original
526
539
  `grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
@@ -123,10 +123,15 @@ class AsyncClusterManager:
123
123
  options = dict(grpc_options or {})
124
124
  if any(key.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for key in options):
125
125
  raise ValueError('TLS authority overrides are unsupported.')
126
- limit = options.get('grpc.max_receive_message_length', 64 * 1024 * 1024)
127
- if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
128
- raise ValueError('A finite positive receive limit is required.')
129
- options['grpc.max_receive_message_length'] = limit
126
+ for name in ('max_receive_message_length', 'max_send_message_length'):
127
+ limits = [options.pop(key) for key in (name, 'grpc.' + name) if key in options]
128
+ for limit in limits:
129
+ if (isinstance(limit, bool) or not isinstance(limit, int)
130
+ or (limit != -1 and limit <= 0)):
131
+ raise ValueError(name + ' must be -1 (unlimited) or a positive integer.')
132
+ if len(limits) == 2 and limits[0] != limits[1]:
133
+ raise ValueError('Conflicting ' + name + ' options.')
134
+ options['grpc.' + name] = limits[0] if limits else -1
130
135
  self._host, self._port = host, port
131
136
  self._user, self._password = user, password
132
137
  self._secure_channel, self._ssl_cert = secure_channel, ssl_cert
@@ -14,6 +14,9 @@ from .oauth_common import validate_positive_timeout, validate_token_endpoint
14
14
  from .server import e6x_engine_pb2 as pb, e6x_engine_pb2_grpc as bindings
15
15
 
16
16
 
17
+ _UNSET_RECEIVE_MESSAGE_BYTES = object()
18
+
19
+
17
20
  @dataclass(frozen=True)
18
21
  class QueryRoute:
19
22
  target: str
@@ -50,7 +53,7 @@ class AsyncConnection:
50
53
  token_url=None, oauth_scope=None, access_token=None,
51
54
  client_auth_method='basic', *, operation_timeout=600.0,
52
55
  oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
53
- max_receive_message_bytes=64 * 1024 * 1024,
56
+ max_receive_message_bytes=_UNSET_RECEIVE_MESSAGE_BYTES,
54
57
  enable_result_batch_v2=False):
55
58
  from .result_decode_worker import is_decode_worker
56
59
  if is_decode_worker():
@@ -98,14 +101,31 @@ class AsyncConnection:
98
101
  for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
99
102
  ('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
100
103
  settings[name] = validate_positive_timeout(value, name)
101
- if isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int) or max_receive_message_bytes <= 0:
102
- raise ValueError('max_receive_message_bytes must be a positive integer.')
103
104
  options = dict(grpc_options or {})
104
- for name in ('max_receive_message_length', 'grpc.max_receive_message_length'):
105
- if name in options:
106
- if options[name] != max_receive_message_bytes:
107
- raise ValueError('Set the finite receive limit using max_receive_message_bytes.')
108
- del options[name]
105
+ receive_names = ('max_receive_message_length', 'grpc.max_receive_message_length')
106
+ receive_values = [options.pop(name) for name in receive_names if name in options]
107
+ for value in receive_values:
108
+ if (isinstance(value, bool) or not isinstance(value, int)
109
+ or (value != -1 and value <= 0)):
110
+ raise ValueError('max_receive_message_length must be -1 (unlimited) or a positive integer.')
111
+ if len(receive_values) == 2 and receive_values[0] != receive_values[1]:
112
+ raise ValueError('Conflicting max_receive_message_length options.')
113
+ if max_receive_message_bytes is _UNSET_RECEIVE_MESSAGE_BYTES:
114
+ max_receive_message_bytes = receive_values[0] if receive_values else -1
115
+ if (isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int)
116
+ or (max_receive_message_bytes != -1 and max_receive_message_bytes <= 0)):
117
+ raise ValueError('max_receive_message_bytes must be -1 (unlimited) or a positive integer.')
118
+ if receive_values and receive_values[0] != max_receive_message_bytes:
119
+ raise ValueError('Set the receive limit using max_receive_message_bytes; supplied options must agree.')
120
+ send_names = ('max_send_message_length', 'grpc.max_send_message_length')
121
+ send_values = [options.pop(name) for name in send_names if name in options]
122
+ for value in send_values:
123
+ if (isinstance(value, bool) or not isinstance(value, int)
124
+ or (value != -1 and value <= 0)):
125
+ raise ValueError('max_send_message_length must be -1 (unlimited) or a positive integer.')
126
+ if len(send_values) == 2 and send_values[0] != send_values[1]:
127
+ raise ValueError('Conflicting max_send_message_length options.')
128
+ options['grpc.max_send_message_length'] = send_values[0] if send_values else -1
109
129
  # Authority overrides can disable meaningful server identity validation.
110
130
  if any(name.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for name in options):
111
131
  raise ValueError('TLS authority overrides are unsupported.')
@@ -385,6 +405,8 @@ class AsyncConnection:
385
405
 
386
406
  self._cluster_manager = AsyncClusterManager(
387
407
  self.host, self.port, user=self.username or '', password=self.password or '',
408
+ grpc_options={**self.grpc_options,
409
+ 'grpc.max_receive_message_length': self.max_receive_message_bytes},
388
410
  secure_channel=self.secure, cluster_uuid=self.cluster_name,
389
411
  ssl_cert=self.ssl_cert, metadata_provider=metadata_provider if self._oauth_enabled else None,
390
412
  initial_strategy=self.strategy, auto_resume_timeout=self.auto_resume_timeout,
@@ -189,6 +189,8 @@ class AsyncConnectionPool:
189
189
  from .async_cluster_manager import AsyncClusterManager
190
190
  self._cluster_manager = AsyncClusterManager(
191
191
  config.host, config.port, user=config.username or '', password=config.password or '',
192
+ grpc_options={**config.grpc_options,
193
+ 'grpc.max_receive_message_length': config.max_receive_message_bytes},
192
194
  secure_channel=config.secure, ssl_cert=config.ssl_cert,
193
195
  cluster_uuid=config.cluster_name, initial_strategy='blue',
194
196
  metadata_provider=self._resume_metadata if config._oauth_enabled else None,
@@ -129,7 +129,7 @@ class AsyncCursor:
129
129
  if self._state == 'CLOSED':
130
130
  raise ProgrammingError('Cursor is closed.')
131
131
  if fetch and self._failure is not None:
132
- raise self._failure
132
+ raise IncompleteResultError(self._failure.reason, query_id=self._failure.query_id)
133
133
  if fetch and self._state in ('EMPTY', 'SUBMISSION_UNKNOWN'):
134
134
  raise ProgrammingError('No complete query is available for fetching.')
135
135
 
@@ -172,12 +172,13 @@ class AsyncCursor:
172
172
  self._retire_prefetch()
173
173
  self._cancel_decode()
174
174
  if self._failure is None:
175
+ # Never raise this template: its traceback would retain decoded inputs.
175
176
  self._failure = IncompleteResultError(reason, query_id=self.query_id)
176
177
  if self._state != 'CLOSED':
177
178
  self._state = 'RESULT_FAILED'
178
179
  self._rows.clear()
179
180
  self._result_batches.clear()
180
- return self._failure
181
+ return IncompleteResultError(self._failure.reason, query_id=self._failure.query_id)
181
182
 
182
183
  def _accept_rows(self, rows):
183
184
  self._rows.extend(rows)
@@ -205,9 +205,10 @@ class ClusterManager:
205
205
  self._timeout = time.time() + timeout
206
206
  self._secure_channel = secure_channel
207
207
  self.cluster_uuid = cluster_uuid
208
- self._grpc_options = grpc_options
209
- if grpc_options is None:
210
- self._grpc_options = dict()
208
+ options = dict(grpc_options or ())
209
+ options.setdefault('grpc.max_receive_message_length', -1)
210
+ options.setdefault('grpc.max_send_message_length', -1)
211
+ self._grpc_options = list(options.items())
211
212
  self._debug = debug
212
213
  self._ssl_cert = ssl_cert
213
214
  self._metadata_provider = metadata_provider
@@ -478,6 +478,8 @@ def read_values_from_array(query_columns_description: list, dis: DataInputStream
478
478
  try:
479
479
  if dtype == "LONG":
480
480
  value_array.append(dis.read_long())
481
+ elif dtype == "INTERVAL_DAY_SECOND":
482
+ value_array.append(timedelta(microseconds=dis.read_long()))
481
483
  elif dtype == "DATE":
482
484
  value_array.append(format_iso_date_from_epoch_micros(dis.read_long()))
483
485
  elif dtype == "DATETIME":
@@ -582,6 +584,15 @@ def get_column_from_chunk(vector: Vector, strict=False) -> list:
582
584
  continue
583
585
  value_array.append(vector.data.int64Data.data[
584
586
  row] if not vector.isConstantVector else vector.data.numericConstantData.data)
587
+ elif d_type == VectorType.INTERVAL_DAY_SECOND:
588
+ for row in range(vector.size):
589
+ if get_null(vector, row):
590
+ value_array.append(None)
591
+ continue
592
+ microseconds = (vector.data.int64Data.data[row]
593
+ if not vector.isConstantVector
594
+ else vector.data.numericConstantData.data)
595
+ value_array.append(timedelta(microseconds=microseconds))
585
596
  elif d_type == VectorType.DATE:
586
597
  # Use the JDBC-parity formatter so years > 9999 emit "+YYYYY-MM-DD"
587
598
  # instead of raising. Per-row try/except keeps a single bad value
@@ -521,17 +521,18 @@ class Connection(object):
521
521
  if enable_result_batch_v2:
522
522
  from .result_decode import validate_decode_runtime
523
523
  validate_decode_runtime()
524
- normalized = {}
525
- for key, value in (grpc_options or {}).items():
526
- key = key[5:] if key.startswith('grpc.') else key
527
- if key in normalized and normalized[key] != value:
528
- raise ValueError('Conflicting gRPC options after prefix normalization.')
529
- normalized[key] = value
530
- limit = normalized.get('max_receive_message_length', 64 * 1024 * 1024)
531
- if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
532
- raise ValueError('V2 receive limit must be a finite positive integer.')
524
+ normalized = {}
525
+ for key, value in (grpc_options or {}).items():
526
+ key = key[5:] if key.startswith('grpc.') else key
527
+ if key in normalized and normalized[key] != value:
528
+ raise ValueError('Conflicting gRPC options after prefix normalization.')
529
+ normalized[key] = value
530
+ grpc_options = normalized
531
+ if enable_result_batch_v2:
532
+ limit = normalized.get('max_receive_message_length', -1)
533
+ if isinstance(limit, bool) or not isinstance(limit, int) or (limit != -1 and limit <= 0):
534
+ raise ValueError('V2 receive limit must be a positive integer or -1 for unlimited.')
533
535
  normalized['max_receive_message_length'] = limit
534
- grpc_options = normalized
535
536
  if enable_result_batch_v2 and grpc_options is not None and 'grpc_prepare_timeout' in grpc_options:
536
537
  timeout = grpc_options['grpc_prepare_timeout']
537
538
  if (isinstance(timeout, bool) or not isinstance(timeout, (int, float))
@@ -696,7 +697,7 @@ class Connection(object):
696
697
  default_options = {
697
698
  "keepalive_timeout_ms": 900000, # Time in milliseconds to keep the connection alive.
698
699
  "max_receive_message_length": -1, # Maximum size of received messages.
699
- "max_send_message_length": 300 * 1024 * 1024, # Maximum size of sent messages (300 MB).
700
+ "max_send_message_length": -1, # Unlimited sent message size.
700
701
  "grpc_prepare_timeout": self.grpc_prepare_timeout, # Timeout for prepare statement API call.
701
702
  "keepalive_time_ms": 30000, # Time in milliseconds between keep-alive pings.
702
703
  "keepalive_permit_without_calls": 1, # Allow keep-alives with no active RPCs.
@@ -1032,6 +1033,7 @@ class Connection(object):
1032
1033
  cluster_uuid=self.cluster_name,
1033
1034
  timeout=self.grpc_auto_resume_timeout_seconds,
1034
1035
  debug=self._debug,
1036
+ grpc_options=self._get_grpc_options,
1035
1037
  ssl_cert=self._ssl_cert
1036
1038
  ).resume()
1037
1039
  return status # Return boolean status directly
@@ -1709,7 +1711,9 @@ class Cursor(DBAPICursor):
1709
1711
  self._result_failure = IncompleteResultError(reason, query_id=self._query_id)
1710
1712
  self._data = None
1711
1713
  self._result_batches.clear()
1712
- return self._result_failure
1714
+ # Keep the cached template free of tracebacks and response frames.
1715
+ return IncompleteResultError(
1716
+ self._result_failure.reason, query_id=self._result_failure.query_id)
1713
1717
 
1714
1718
  def _check_result(self):
1715
1719
  if self._result_batch_v2_enabled and (self._result_pid != os.getpid() or
@@ -1720,7 +1724,8 @@ class Cursor(DBAPICursor):
1720
1724
  if self._closed:
1721
1725
  raise ProgrammingError('Cursor is closed.')
1722
1726
  if self._result_failure is not None:
1723
- raise self._result_failure
1727
+ raise IncompleteResultError(
1728
+ self._result_failure.reason, query_id=self._result_failure.query_id)
1724
1729
 
1725
1730
  @property
1726
1731
  def query_id(self):
@@ -35,6 +35,7 @@ class VectorType(object):
35
35
  NULL = 14
36
36
  TIMESTAMP_TZ = 15
37
37
  DECIMAL128 = 16
38
+ INTERVAL_DAY_SECOND = 17
38
39
 
39
40
  _VALUES_TO_NAMES = {
40
41
  0: "BINARY",
@@ -54,6 +55,7 @@ class VectorType(object):
54
55
  14: "NULL",
55
56
  15: "TIMESTAMP_TZ",
56
57
  16: "DECIMAL128",
58
+ 17: "INTERVAL_DAY_SECOND",
57
59
  }
58
60
 
59
61
  _NAMES_TO_VALUES = {
@@ -74,6 +76,7 @@ class VectorType(object):
74
76
  "NULL": 14,
75
77
  "TIMESTAMP_TZ": 15,
76
78
  "DECIMAL128": 16,
79
+ "INTERVAL_DAY_SECOND": 17,
77
80
  }
78
81
 
79
82